diff --git a/F-Droid/res/drawable-hdpi/ic_clear.png b/F-Droid/res/drawable-hdpi/ic_clear.png new file mode 100644 index 000000000..1a9cd75a0 Binary files /dev/null and b/F-Droid/res/drawable-hdpi/ic_clear.png differ diff --git a/F-Droid/res/drawable-mdpi/ic_clear.png b/F-Droid/res/drawable-mdpi/ic_clear.png new file mode 100644 index 000000000..40a1a84e3 Binary files /dev/null and b/F-Droid/res/drawable-mdpi/ic_clear.png differ diff --git a/F-Droid/res/drawable-xhdpi/ic_clear.png b/F-Droid/res/drawable-xhdpi/ic_clear.png new file mode 100644 index 000000000..6bc437298 Binary files /dev/null and b/F-Droid/res/drawable-xhdpi/ic_clear.png differ diff --git a/F-Droid/res/drawable-xxhdpi/ic_clear.png b/F-Droid/res/drawable-xxhdpi/ic_clear.png new file mode 100644 index 000000000..51b4401ca Binary files /dev/null and b/F-Droid/res/drawable-xxhdpi/ic_clear.png differ diff --git a/F-Droid/res/drawable-xxxhdpi/ic_clear.png b/F-Droid/res/drawable-xxxhdpi/ic_clear.png new file mode 100644 index 000000000..df42feecb Binary files /dev/null and b/F-Droid/res/drawable-xxxhdpi/ic_clear.png differ diff --git a/F-Droid/res/layout/app_details_header.xml b/F-Droid/res/layout/app_details_header.xml index e36319c54..99f2cb936 100644 --- a/F-Droid/res/layout/app_details_header.xml +++ b/F-Droid/res/layout/app_details_header.xml @@ -16,7 +16,7 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --> - @@ -109,7 +110,50 @@ - + + + + + + + + + diff --git a/F-Droid/res/layout/repodetails.xml b/F-Droid/res/layout/repodetails.xml index 86cb9b1a2..921612d11 100644 --- a/F-Droid/res/layout/repodetails.xml +++ b/F-Droid/res/layout/repodetails.xml @@ -1,32 +1,32 @@ - - + + + + - + android:textStyle="bold" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" /> - + + + + + diff --git a/F-Droid/res/menu/main.xml b/F-Droid/res/menu/main.xml index 881e99869..9c8a76dc0 100644 --- a/F-Droid/res/menu/main.xml +++ b/F-Droid/res/menu/main.xml @@ -7,11 +7,6 @@ android:icon="@drawable/ic_search_white" android:title="@string/menu_search" app:showAsAction="always"/> - + + + + + + + + \ No newline at end of file diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml index 8fae272f7..770a1b222 100644 --- a/F-Droid/res/values/strings.xml +++ b/F-Droid/res/values/strings.xml @@ -210,6 +210,7 @@ - Percentage complete (int between 0-100) --> Downloading\n%2$s / %3$s (%4$d%%) from\n%1$s + Updating repositories Processing %2$s / %3$s (%4$d%%) from %1$s Connecting to\n%1$s Checking apps compatibility with your device… @@ -372,4 +373,13 @@ NEW: Provided by %1$s. + Downloading... + + + B + KiB + MiB + GiB + TiB + diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index 76d17a2a0..6500551b0 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -21,28 +21,30 @@ package org.fdroid.fdroid; import android.app.Activity; -import android.app.ProgressDialog; import android.bluetooth.BluetoothAdapter; import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; import android.database.ContentObserver; import android.graphics.Bitmap; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v4.app.ListFragment; import android.support.v4.app.NavUtils; +import android.support.v4.content.LocalBroadcastManager; import android.support.v4.view.MenuItemCompat; -import android.support.v7.app.ActionBarActivity; import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; import android.text.Html; import android.text.Layout; import android.text.Selection; @@ -63,9 +65,11 @@ import android.view.Window; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.FrameLayout; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; +import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; @@ -90,6 +94,7 @@ import org.fdroid.fdroid.net.Downloader; import java.io.File; import java.security.NoSuchAlgorithmException; +import java.text.DecimalFormat; import java.util.Iterator; import java.util.List; @@ -113,7 +118,7 @@ interface AppInstallListener { void removeApk(String packageName); } -public class AppDetails extends ActionBarActivity implements ProgressListener, AppDetailsData, AppInstallListener { +public class AppDetails extends AppCompatActivity implements ProgressListener, AppDetailsData, AppInstallListener { private static final String TAG = "AppDetails"; @@ -124,7 +129,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A private FDroidApp fdroidApp; private ApkListAdapter adapter; - private ProgressDialog progressDialog; private static class ViewHolder { TextView version; @@ -316,6 +320,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A private App app; private PackageManager mPm; private ApkDownloader downloadHandler; + private LocalBroadcastManager localBroadcastManager; private boolean startingIgnoreAll; private int startingIgnoreThis; @@ -323,11 +328,14 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A private final Context mctx = this; private Installer installer; + + private AppDetailsHeaderFragment mHeaderFragment; + /** * Stores relevant data that we want to keep track of when destroying the activity * with the expectation of it being recreated straight away (e.g. after an * orientation change). One of the major things is that we want the download thread - * to stay active, but for it not to trigger any UI stuff (e.g. progress dialogs) + * to stay active, but for it not to trigger any UI stuff (e.g. progress bar) * between the activity being destroyed and recreated. */ private static class ConfigurationChangeHelper { @@ -418,13 +426,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A listFragment.removeSummaryHeader(); } - // Spinner seems to default to visible on Android 4.0.3 and 4.0.4 - // https://gitlab.com/fdroid/fdroidclient/issues/75 - // Can't put this in onResume(), because that is called on return from asking - // the user permission to use su (in which case we still want to show the - // progress indicator after returning from that prompt). - setSupportProgressBarIndeterminateVisibility(false); - + localBroadcastManager = LocalBroadcastManager.getInstance(this); } // The signature of the installed version. @@ -442,39 +444,35 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A myAppObserver); } - @Override - protected void onResume() { - super.onResume(); - if (downloadHandler != null) { - if (downloadHandler.isComplete()) { - downloadCompleteInstallApk(); - } else { - downloadHandler.setProgressListener(this); - - // Show the progress dialog, if for no other reason than to prevent them attempting - // to download again (i.e. we force them to touch 'cancel' before they can access - // the rest of the activity). - Log.d(TAG, "Showing dialog to user after resuming app details view, because a download was previously in progress"); - updateProgressDialog(); - } - } - } - @Override protected void onResumeFragments() { super.onResumeFragments(); refreshApkList(); refreshHeader(); supportInvalidateOptionsMenu(); + if (downloadHandler != null) { + if (downloadHandler.isComplete()) { + downloadCompleteInstallApk(); + } else { + localBroadcastManager.registerReceiver(downloaderProgressReceiver, + new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS)); + downloadHandler.setProgressListener(this); + + if (downloadHandler.getTotalBytes() == 0) + mHeaderFragment.startProgress(); + else + mHeaderFragment.updateProgress(downloadHandler.getBytesRead(), downloadHandler.getTotalBytes()); + } + } } /** - * Remove progress listener, suppress progress dialog, set downloadHandler to null. + * Remove progress listener, suppress progress bar, set downloadHandler to null. */ private void cleanUpFinishedDownload() { if (downloadHandler != null) { downloadHandler.removeProgressListener(); - removeProgressDialog(); + mHeaderFragment.removeProgress(); downloadHandler = null; } } @@ -485,7 +483,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A */ private void downloadCompleteInstallApk() { if (downloadHandler != null) { - installApk(downloadHandler.localFile(), downloadHandler.getApk().id); + installApk(downloadHandler.localFile()); cleanUpFinishedDownload(); } } @@ -504,13 +502,23 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate); } + localBroadcastManager.unregisterReceiver(downloaderProgressReceiver); if (downloadHandler != null) { downloadHandler.removeProgressListener(); } - removeProgressDialog(); + mHeaderFragment.removeProgress(); } + private final BroadcastReceiver downloaderProgressReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mHeaderFragment != null) + mHeaderFragment.updateProgress(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1), + intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1)); + } + }; + private void onAppChanged() { if (!reset(app.id)) { AppDetails.this.finish(); @@ -553,13 +561,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A super.onDestroy(); } - private void removeProgressDialog() { - if (progressDialog != null) { - progressDialog.dismiss(); - progressDialog = null; - } - } - // Reset the display and list contents. Used when entering the activity, and // also when something has been installed/uninstalled. // Return true if the app was found, false otherwise. @@ -619,9 +620,9 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } private void refreshHeader() { - AppDetailsHeaderFragment headerFragment = (AppDetailsHeaderFragment) + mHeaderFragment = (AppDetailsHeaderFragment) getSupportFragmentManager().findFragmentById(R.id.header); - headerFragment.refresh(); + mHeaderFragment.updateViews(); } @Override @@ -725,8 +726,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } @Override - public boolean onTouchEvent(TextView widget, Spannable buffer, - MotionEvent event) { + public boolean onTouchEvent(@NonNull TextView widget, @NonNull Spannable buffer, + @NonNull MotionEvent event) { try { return super.onTouchEvent(widget, buffer, event); } catch (ActivityNotFoundException ex) { @@ -805,6 +806,10 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A // Install the version of this app denoted by 'app.curApk'. @Override public void install(final Apk apk) { + // Ignore call if another download is running. + if (downloadHandler != null && !downloadHandler.isComplete()) + return; + final String[] projection = { RepoProvider.DataColumns.ADDRESS }; Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection); if (repo == null || repo.address == null) { @@ -854,32 +859,28 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A private void startDownload(Apk apk, String repoAddress) { downloadHandler = new ApkDownloader(getBaseContext(), apk, repoAddress); + localBroadcastManager.registerReceiver(downloaderProgressReceiver, + new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS)); downloadHandler.setProgressListener(this); if (downloadHandler.download()) { - updateProgressDialog(); + mHeaderFragment.startProgress(); } } - private void installApk(File file, String packageName) { - setSupportProgressBarIndeterminateVisibility(true); - + private void installApk(File file) { try { installer.installPackage(file); } catch (AndroidNotCompatibleException e) { Log.e(TAG, "Android not compatible with this Installer!", e); - setSupportProgressBarIndeterminateVisibility(false); } } @Override public void removeApk(String packageName) { - setSupportProgressBarIndeterminateVisibility(true); - try { installer.deletePackage(packageName); } catch (AndroidNotCompatibleException e) { Log.e(TAG, "Android not compatible with this Installer!", e); - setSupportProgressBarIndeterminateVisibility(false); } } @@ -894,7 +895,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A PackageManagerCompat.setInstaller(mPm, app.id); } - setSupportProgressBarIndeterminateVisibility(false); onAppChanged(); } }); @@ -906,7 +906,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A runOnUiThread(new Runnable() { @Override public void run() { - setSupportProgressBarIndeterminateVisibility(false); onAppChanged(); } }); @@ -914,7 +913,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A runOnUiThread(new Runnable() { @Override public void run() { - setSupportProgressBarIndeterminateVisibility(false); onAppChanged(); Log.e(TAG, "Installer aborted with errorCode: " + errorCode); @@ -945,79 +943,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share))); } - private ProgressDialog getProgressDialog(String file) { - if (progressDialog == null) { - final ProgressDialog pd = new ProgressDialog(this); - pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - if (Build.VERSION.SDK_INT >= 11) { - pd.setProgressNumberFormat("%1d/%2d KiB"); - } - pd.setMessage(getString(R.string.download_server) + ":\n " + file); - pd.setCancelable(true); - pd.setCanceledOnTouchOutside(false); - - // The indeterminate-ness will get overridden on the first progress event we receive. - pd.setIndeterminate(true); - - pd.setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - Log.d(TAG, "User clicked 'cancel' on download, attempting to interrupt download thread."); - if (downloadHandler != null) { - downloadHandler.cancel(); - cleanUpFinishedDownload(); - } else { - Log.e(TAG, "Tried to cancel, but the downloadHandler doesn't exist."); - } - progressDialog = null; - Toast.makeText(AppDetails.this, getString(R.string.download_cancelled), Toast.LENGTH_LONG).show(); - } - }); - pd.setButton(DialogInterface.BUTTON_NEUTRAL, - getString(R.string.cancel), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - pd.cancel(); - } - } - ); - progressDialog = pd; - } - return progressDialog; - } - - /** - * Looks at the current downloadHandler and finds it's size and progress. - * This is in comparison to {@link org.fdroid.fdroid.AppDetails#updateProgressDialog(int, int)}, - * which is used when you have the details from a freshly received - * {@link org.fdroid.fdroid.ProgressListener.Event}. - */ - private void updateProgressDialog() { - if (downloadHandler != null) { - updateProgressDialog(downloadHandler.getProgress(), downloadHandler.getTotalSize()); - } - } - - private void updateProgressDialog(int progress, int total) { - if (downloadHandler != null) { - ProgressDialog pd = getProgressDialog(downloadHandler.getRemoteAddress()); - if (total > 0) { - pd.setIndeterminate(false); - pd.setProgress(progress/1024); - pd.setMax(total/1024); - } else { - pd.setIndeterminate(true); - pd.setProgress(progress/1024); - pd.setMax(0); - } - if (!pd.isShowing()) { - Log.d(TAG, "Showing progress dialog for download."); - pd.show(); - } - } - } - @Override public void onProgress(Event event) { if (downloadHandler == null || !downloadHandler.isEventFromThis(event)) { @@ -1034,9 +959,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A boolean finished = false; switch (event.type) { - case Downloader.EVENT_PROGRESS: - updateProgressDialog(event.progress, event.total); - break; case ApkDownloader.EVENT_ERROR: final String text; if (event.getData().getInt(ApkDownloader.EVENT_DATA_ERROR_TYPE) == ApkDownloader.ERROR_HASH_MISMATCH) @@ -1054,7 +976,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } if (finished) { - removeProgressDialog(); + if (mHeaderFragment != null) + mHeaderFragment.removeProgress(); downloadHandler = null; } } @@ -1447,9 +1370,14 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } } - public static class AppDetailsHeaderFragment extends Fragment { + public static class AppDetailsHeaderFragment extends Fragment implements View.OnClickListener { private AppDetailsData data; + private Button btMain; + private ProgressBar progressBar; + private TextView progressSize; + private TextView progressPercent; + private ImageButton cancelButton; protected final DisplayImageOptions displayImageOptions; public static boolean installed = false; public static boolean updateWanted = false; @@ -1493,35 +1421,120 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A TextView tv = (TextView) view.findViewById(R.id.title); tv.setText(getApp().name); + btMain = (Button) view.findViewById(R.id.btn_main); + progressBar = (ProgressBar) view.findViewById(R.id.progress_bar); + progressSize = (TextView) view.findViewById(R.id.progress_size); + progressPercent = (TextView) view.findViewById(R.id.progress_percentage); + cancelButton = (ImageButton) view.findViewById(R.id.cancel); + progressBar.setIndeterminate(false); + cancelButton.setOnClickListener(this); + updateViews(view); } @Override public void onResume() { super.onResume(); - refresh(); + updateViews(); } - public void refresh() { + /** + * Displays empty, indeterminate progress bar and related views. + */ + public void startProgress() { + setProgressVisible(true); + progressBar.setIndeterminate(true); + progressSize.setText(""); + progressPercent.setText(""); + updateViews(); + } + + /** + * Updates progress bar and captions to new values (in bytes). + */ + public void updateProgress(long progress, long total) { + long percent = progress * 100 / total; + setProgressVisible(true); + progressBar.setIndeterminate(false); + progressBar.setProgress((int) percent); + progressBar.setMax(100); + progressSize.setText(readableFileSize(progress) + " / " + readableFileSize(total)); + progressPercent.setText(Long.toString(percent) + " %"); + } + + /** + * Converts a number of bytes to a human readable file size (eg 3.5 GiB). + * + * Based on http://stackoverflow.com/a/5599842 + */ + public String readableFileSize(long bytes) { + final String[] units = getResources().getStringArray(R.array.file_size_units); + if (bytes <= 0) return "0 " + units[0]; + int digitGroups = (int) (Math.log10(bytes) / Math.log10(1024)); + return new DecimalFormat("#,##0.#") + .format(bytes / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; + } + + /** + * Shows or hides progress bar and related views. + */ + private void setProgressVisible(boolean visible) { + int state = (visible) ? View.VISIBLE : View.GONE; + progressBar.setVisibility(state); + progressSize.setVisibility(state); + progressPercent.setVisibility(state); + cancelButton.setVisibility(state); + } + + /** + * Removes progress bar and related views, invokes {@link #updateViews()}. + */ + public void removeProgress() { + setProgressVisible(false); + updateViews(); + } + + /** + * Cancels download and hides progress bar. + */ + @Override + public void onClick(View view) { + AppDetails activity = (AppDetails) getActivity(); + if (activity == null || activity.downloadHandler == null) + return; + + activity.downloadHandler.cancel(); + activity.cleanUpFinishedDownload(); + setProgressVisible(false); + updateViews(); + } + + public void updateViews() { updateViews(getView()); } public void updateViews(View view) { TextView statusView = (TextView) view.findViewById(R.id.status); - Button btMain = (Button) view.findViewById(R.id.btn_main); btMain.setVisibility(View.VISIBLE); + AppDetails activity = (AppDetails) getActivity(); + if (activity.downloadHandler != null) { + btMain.setText(R.string.downloading); + btMain.setEnabled(false); + } /* Check count > 0 due to incompatible apps resulting in an empty list. If App isn't installed */ - if (!getApp().isInstalled() && getApp().suggestedVercode > 0 && ((AppDetails)getActivity()).adapter.getCount() > 0) { + else if (!getApp().isInstalled() && getApp().suggestedVercode > 0 && + ((AppDetails)getActivity()).adapter.getCount() > 0) { installed = false; statusView.setText(getString(R.string.details_notinstalled)); NfcHelper.disableAndroidBeam(getActivity()); // Set Install button and hide second button btMain.setText(R.string.menu_install); btMain.setOnClickListener(mOnClickListener); + btMain.setEnabled(true); } // If App is installed else if (getApp().isInstalled()) { @@ -1541,6 +1554,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } } btMain.setOnClickListener(mOnClickListener); + btMain.setEnabled(true); } TextView currentVersion = (TextView) view.findViewById(R.id.current_version); if (!getApks().isEmpty()) { @@ -1574,6 +1588,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A // If not installed, install else if (getApp().suggestedVercode > 0) { + btMain.setEnabled(false); + btMain.setText(R.string.system_install_installing); final Apk apkToInstall = ApkProvider.Helper.find(getActivity(), getApp().id, getApp().suggestedVercode); ((AppDetails)getActivity()).install(apkToInstall); } diff --git a/F-Droid/src/org/fdroid/fdroid/FDroid.java b/F-Droid/src/org/fdroid/fdroid/FDroid.java index 4a525cbf7..92c8cbd8a 100644 --- a/F-Droid/src/org/fdroid/fdroid/FDroid.java +++ b/F-Droid/src/org/fdroid/fdroid/FDroid.java @@ -55,7 +55,6 @@ public class FDroid extends ActionBarActivity { private static final String TAG = "FDroid"; - public static final int REQUEST_MANAGEREPOS = 0; public static final int REQUEST_PREFS = 1; public static final int REQUEST_ENABLE_BLUETOOTH = 2; public static final int REQUEST_SWAP = 3; @@ -244,12 +243,11 @@ public class FDroid extends ActionBarActivity { switch (item.getItemId()) { case R.id.action_update_repo: - updateRepos(); + UpdateService.updateNow(this); return true; case R.id.action_manage_repos: - Intent i = new Intent(this, ManageReposActivity.class); - startActivityForResult(i, REQUEST_MANAGEREPOS); + startActivity(new Intent(this, ManageReposActivity.class)); return true; case R.id.action_settings: @@ -318,31 +316,6 @@ public class FDroid extends ActionBarActivity { protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { - case REQUEST_MANAGEREPOS: - if (data != null && data.hasExtra(ManageReposActivity.REQUEST_UPDATE)) { - AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); - ask_alrt.setTitle(getString(R.string.repo_update_title)); - ask_alrt.setMessage(getString(R.string.repo_alrt)); - ask_alrt.setPositiveButton(getString(R.string.yes), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - updateRepos(); - } - }); - ask_alrt.setNegativeButton(getString(R.string.no), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - // do nothing - } - }); - AlertDialog alert = ask_alrt.create(); - alert.show(); - } - break; case REQUEST_PREFS: // The automatic update settings may have changed, so reschedule (or // unschedule) the service accordingly. It's cheap, so no need to @@ -377,13 +350,6 @@ public class FDroid extends ActionBarActivity { }); } - // Force a repo update now. A progress dialog is shown and the UpdateService - // is told to do the update, which will result in the database changing. The - // UpdateReceiver class should get told when this is finished. - public void updateRepos() { - UpdateService.updateNow(this); - } - private TabManager getTabManager() { if (tabManager == null) { tabManager = new TabManager(this, viewPager); diff --git a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java index f3cff0b8b..936d2e963 100644 --- a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java +++ b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java @@ -3,7 +3,6 @@ package org.fdroid.fdroid; import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageManager; -import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; @@ -22,6 +21,8 @@ import org.xml.sax.XMLReader; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; import java.security.CodeSigner; import java.security.cert.Certificate; import java.security.cert.X509Certificate; @@ -52,16 +53,14 @@ public class RepoUpdater { /** * Updates an app repo as read out of the database into a {@link Repo} instance. - * - * @param context - * @param repo a {@link Repo} read out of the local database + * @param repo A {@link Repo} read out of the local database */ public RepoUpdater(@NonNull Context context, @NonNull Repo repo) { this.context = context; this.repo = repo; } - public void setProgressListener(ProgressListener progressListener) { + public void setProgressListener(@Nullable ProgressListener progressListener) { this.progressListener = progressListener; } @@ -71,29 +70,23 @@ public class RepoUpdater { public List getApks() { return apks; } - protected String getIndexAddress() { + protected URL getIndexAddress() throws MalformedURLException { + String urlString = repo.address + "/index.jar"; try { String versionName = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; - return repo.address + "/index.jar?client_version=" + versionName; + urlString += "?client_version=" + versionName; } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } - return repo.address + "/index.jar"; + return new URL(urlString); } Downloader downloadIndex() throws UpdateException { Downloader downloader = null; try { - downloader = DownloaderFactory.create( + downloader = DownloaderFactory.create(context, getIndexAddress(), File.createTempFile("index-", "-downloaded", context.getCacheDir())); downloader.setCacheTag(repo.lastetag); - - if (progressListener != null) { // interactive session, show progress - Bundle data = new Bundle(1); - data.putString(PROGRESS_DATA_REPO_ADDRESS, repo.address); - downloader.setProgressListener(progressListener, data); - } - downloader.downloadUninterrupted(); if (downloader.isCached()) { diff --git a/F-Droid/src/org/fdroid/fdroid/UpdateService.java b/F-Droid/src/org/fdroid/fdroid/UpdateService.java index ec63a7d49..3e75ff7db 100644 --- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java +++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java @@ -22,11 +22,12 @@ import android.app.AlarmManager; import android.app.IntentService; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.ProgressDialog; +import android.content.BroadcastReceiver; import android.content.ContentProviderOperation; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.OperationApplicationException; import android.content.SharedPreferences; import android.database.Cursor; @@ -34,14 +35,12 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; -import android.os.Bundle; -import android.os.Handler; import android.os.RemoteException; -import android.os.ResultReceiver; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; +import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; @@ -63,30 +62,28 @@ public class UpdateService extends IntentService implements ProgressListener { private static final String TAG = "UpdateService"; - public static final String RESULT_MESSAGE = "msg"; - public static final String RESULT_EVENT = "event"; - public static final String RESULT_REPO_ERRORS = "repoErrors"; + public static final String LOCAL_ACTION_STATUS = "status"; + + public static final String EXTRA_MESSAGE = "msg"; + public static final String EXTRA_REPO_ERRORS = "repoErrors"; + public static final String EXTRA_STATUS_CODE = "status"; + public static final String EXTRA_ADDRESS = "address"; + public static final String EXTRA_MANUAL_UPDATE = "manualUpdate"; public static final int STATUS_COMPLETE_WITH_CHANGES = 0; - public static final int STATUS_COMPLETE_AND_SAME = 1; - public static final int STATUS_ERROR_GLOBAL = 2; - public static final int STATUS_ERROR_LOCAL = 3; - public static final int STATUS_ERROR_LOCAL_SMALL = 4; - public static final int STATUS_INFO = 5; + public static final int STATUS_COMPLETE_AND_SAME = 1; + public static final int STATUS_ERROR_GLOBAL = 2; + public static final int STATUS_ERROR_LOCAL = 3; + public static final int STATUS_ERROR_LOCAL_SMALL = 4; + public static final int STATUS_INFO = 5; - // I don't like that I've had to dupliacte the statuses above with strings here, however - // one method of communication/notification is using ResultReceiver (int status codes) - // while the other uses progress events (string event types). - public static final String EVENT_COMPLETE_WITH_CHANGES = "repoUpdateComplete (changed)"; - public static final String EVENT_COMPLETE_AND_SAME = "repoUpdateComplete (not changed)"; - public static final String EVENT_FINISHED = "repoUpdateFinished"; - public static final String EVENT_ERROR = "repoUpdateError"; - public static final String EVENT_INFO = "repoUpdateInfo"; + private LocalBroadcastManager localBroadcastManager; - public static final String EXTRA_RECEIVER = "receiver"; - public static final String EXTRA_ADDRESS = "address"; + private static final int NOTIFY_ID_UPDATING = 0; + private static final int NOTIFY_ID_UPDATES_AVAILABLE = 1; - private ResultReceiver receiver = null; + private NotificationManager notificationManager; + private NotificationCompat.Builder notificationBuilder; public UpdateService() { super("UpdateService"); @@ -106,130 +103,17 @@ public class UpdateService extends IntentService implements ProgressListener { AppProvider.DataColumns.IGNORE_THISUPDATE }; - // For receiving results from the UpdateService when we've told it to - // update in response to a user request. - public static class UpdateReceiver extends ResultReceiver { - - private Context context; - private ProgressDialog dialog; - private ProgressListener listener; - private String lastShownMessage = null; - - public UpdateReceiver(Handler handler) { - super(handler); - } - - public UpdateReceiver setDialog(ProgressDialog dialog) { - this.dialog = dialog; - return this; - } - - public UpdateReceiver setListener(ProgressListener listener) { - this.listener = listener; - return this; - } - - private void forwardEvent(String type) { - if (listener != null) { - listener.onProgress(new Event(type)); - } - } - - private void ensureDialog() { - if (dialog == null) { - String title = context.getString(R.string.process_wait_title); - String message = lastShownMessage == null ? context.getString(R.string.process_update_msg) : lastShownMessage; - dialog = ProgressDialog.show(context, title, message, true, true); - dialog.setIcon(android.R.drawable.ic_dialog_info); - dialog.setCanceledOnTouchOutside(false); - } - } - - public UpdateReceiver showDialog(Context context) { - this.context = context; - ensureDialog(); - dialog.show(); - return this; - } - - public void hideDialog() { - dialog.hide(); - dialog.dismiss(); - dialog = null; - } - - @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - final String message = resultData.getString(RESULT_MESSAGE); - boolean finished = false; - switch (resultCode) { - case STATUS_ERROR_GLOBAL: - forwardEvent(EVENT_ERROR); - Toast.makeText(context, context.getString(R.string.global_error_updating_repos) + " " + message, Toast.LENGTH_LONG).show(); - finished = true; - break; - case STATUS_ERROR_LOCAL: - case STATUS_ERROR_LOCAL_SMALL: - StringBuilder msgB = new StringBuilder(); - List repoErrors = resultData.getCharSequenceArrayList(RESULT_REPO_ERRORS); - for (CharSequence error : repoErrors) { - if (msgB.length() > 0) msgB.append('\n'); - msgB.append(error); - } - if (resultCode == STATUS_ERROR_LOCAL_SMALL) { - msgB.append('\n').append(context.getString(R.string.all_other_repos_fine)); - } - Toast.makeText(context, msgB.toString(), Toast.LENGTH_LONG).show(); - finished = true; - break; - case STATUS_COMPLETE_WITH_CHANGES: - forwardEvent(EVENT_COMPLETE_WITH_CHANGES); - finished = true; - break; - case STATUS_COMPLETE_AND_SAME: - forwardEvent(EVENT_COMPLETE_AND_SAME); - Toast.makeText(context, context.getString(R.string.repos_unchanged), Toast.LENGTH_LONG).show(); - finished = true; - break; - case STATUS_INFO: - forwardEvent(EVENT_INFO); - if (dialog != null) { - lastShownMessage = message; - dialog.setMessage(message); - } - break; - } - - if (finished) { - forwardEvent(EVENT_FINISHED); - if (dialog != null && dialog.isShowing()) { - try { - dialog.dismiss(); - } catch (IllegalArgumentException e) { - // sometimes dialog.isShowing() doesn't work :( - // https://stackoverflow.com/questions/19538282/view-not-attached-to-window-manager-dialog-dismiss - e.printStackTrace(); - } - } - } - } + public static void updateNow(Context context) { + updateRepoNow(null, context); } - public static UpdateReceiver updateNow(Context context) { - return updateRepoNow(null, context); - } - - public static UpdateReceiver updateRepoNow(String address, Context context) { + public static void updateRepoNow(String address, Context context) { Intent intent = new Intent(context, UpdateService.class); - UpdateReceiver receiver = new UpdateReceiver(new Handler()); - receiver.showDialog(context); - intent.putExtra(EXTRA_RECEIVER, receiver); + intent.putExtra(EXTRA_MANUAL_UPDATE, true); if (!TextUtils.isEmpty(address)) { intent.putExtra(EXTRA_ADDRESS, address); } context.startService(intent); - - return receiver; } // Schedule (or cancel schedule for) this service, according to the @@ -259,36 +143,135 @@ public class UpdateService extends IntentService implements ProgressListener { } + @Override + public void onCreate() { + super.onCreate(); + + localBroadcastManager = LocalBroadcastManager.getInstance(this); + localBroadcastManager.registerReceiver(downloadProgressReceiver, + new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS)); + localBroadcastManager.registerReceiver(updateStatusReceiver, + new IntentFilter(LOCAL_ACTION_STATUS)); + + notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + notificationBuilder = new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.ic_refresh_white) + .setOngoing(true) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setContentTitle(getString(R.string.update_notification_title)); + notificationManager.notify(NOTIFY_ID_UPDATING, notificationBuilder.build()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + notificationManager.cancel(NOTIFY_ID_UPDATING); + localBroadcastManager.unregisterReceiver(downloadProgressReceiver); + localBroadcastManager.unregisterReceiver(updateStatusReceiver); + } + protected void sendStatus(int statusCode) { sendStatus(statusCode, null); } protected void sendStatus(int statusCode, String message) { - if (receiver != null) { - Bundle resultData = new Bundle(); - if (!TextUtils.isEmpty(message)) { - resultData.putString(RESULT_MESSAGE, message); - } - receiver.send(statusCode, resultData); - } + Intent intent = new Intent(LOCAL_ACTION_STATUS); + intent.putExtra(EXTRA_STATUS_CODE, statusCode); + if (!TextUtils.isEmpty(message)) + intent.putExtra(EXTRA_MESSAGE, message); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } protected void sendRepoErrorStatus(int statusCode, ArrayList repoErrors) { - if (receiver != null) { - Bundle resultData = new Bundle(); - resultData.putCharSequenceArrayList(RESULT_REPO_ERRORS, repoErrors); - receiver.send(statusCode, resultData); - } + Intent intent = new Intent(LOCAL_ACTION_STATUS); + intent.putExtra(EXTRA_STATUS_CODE, statusCode); + intent.putExtra(EXTRA_REPO_ERRORS, repoErrors.toArray(new CharSequence[repoErrors.size()])); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } - /** - * We might be doing a scheduled run, or we might have been launched by the - * app in response to a user's request. If we have a receiver, it's the - * latter... - */ - private boolean isScheduledRun() { - return receiver == null; - } + private final BroadcastReceiver downloadProgressReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (TextUtils.isEmpty(action)) + return; + + if (!action.equals(Downloader.LOCAL_ACTION_PROGRESS)) + return; + + String repoAddress = intent.getStringExtra(Downloader.EXTRA_ADDRESS); + int downloadedSize = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1); + int totalSize = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1); + int percent = (int) ((double) downloadedSize / totalSize * 100); + sendStatus(STATUS_INFO, + getString(R.string.status_download, repoAddress, + Utils.getFriendlySize(downloadedSize), + Utils.getFriendlySize(totalSize), percent)); + } + }; + + // For receiving results from the UpdateService when we've told it to + // update in response to a user request. + private final BroadcastReceiver updateStatusReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (TextUtils.isEmpty(action)) + return; + + if (!action.equals(LOCAL_ACTION_STATUS)) + return; + + final String message = intent.getStringExtra(EXTRA_MESSAGE); + int resultCode = intent.getIntExtra(EXTRA_STATUS_CODE, -1); + + String text; + switch (resultCode) { + case STATUS_INFO: + notificationBuilder.setContentText(message) + .setProgress(0, 0, true) + .setCategory(NotificationCompat.CATEGORY_SERVICE); + notificationManager.notify(NOTIFY_ID_UPDATING, notificationBuilder.build()); + break; + case STATUS_ERROR_GLOBAL: + text = context.getString(R.string.global_error_updating_repos) + " " + message; + notificationBuilder.setContentText(text) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setSmallIcon(android.R.drawable.ic_dialog_alert); + notificationManager.notify(NOTIFY_ID_UPDATING, notificationBuilder.build()); + Toast.makeText(context, text, Toast.LENGTH_LONG).show(); + break; + case STATUS_ERROR_LOCAL: + case STATUS_ERROR_LOCAL_SMALL: + StringBuilder msgBuilder = new StringBuilder(); + CharSequence[] repoErrors = intent.getCharSequenceArrayExtra(EXTRA_REPO_ERRORS); + for (CharSequence error : repoErrors) { + if (msgBuilder.length() > 0) msgBuilder.append('\n'); + msgBuilder.append(error); + } + if (resultCode == STATUS_ERROR_LOCAL_SMALL) { + msgBuilder.append('\n').append(context.getString(R.string.all_other_repos_fine)); + } + text = msgBuilder.toString(); + notificationBuilder.setContentText(text) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setSmallIcon(android.R.drawable.ic_dialog_info); + notificationManager.notify(NOTIFY_ID_UPDATING, notificationBuilder.build()); + Toast.makeText(context, text, Toast.LENGTH_LONG).show(); + break; + case STATUS_COMPLETE_WITH_CHANGES: + break; + case STATUS_COMPLETE_AND_SAME: + text = context.getString(R.string.repos_unchanged); + notificationBuilder.setContentText(text) + .setCategory(NotificationCompat.CATEGORY_SERVICE); + notificationManager.notify(NOTIFY_ID_UPDATING, notificationBuilder.build()); + break; + } + } + }; /** * Check whether it is time to run the scheduled update. @@ -332,15 +315,14 @@ public class UpdateService extends IntentService implements ProgressListener { @Override protected void onHandleIntent(Intent intent) { - receiver = intent.getParcelableExtra(EXTRA_RECEIVER); String address = intent.getStringExtra(EXTRA_ADDRESS); + boolean manualUpdate = intent.getBooleanExtra(EXTRA_MANUAL_UPDATE, false); - long startTime = System.currentTimeMillis(); try { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); // See if it's time to actually do anything yet... - if (!isScheduledRun()) { + if (manualUpdate) { Log.d(TAG, "Unscheduled (manually requested) update"); } else if (!verifyIsTimeForScheduledRun()) { return; @@ -460,11 +442,6 @@ public class UpdateService extends IntentService implements ProgressListener { "Exception during update processing:\n" + Log.getStackTraceString(e)); sendStatus(STATUS_ERROR_GLOBAL, e.getMessage()); - } finally { - Log.d(TAG, "Update took " - + ((System.currentTimeMillis() - startTime) / 1000) - + " seconds."); - receiver = null; } } @@ -557,8 +534,7 @@ public class UpdateService extends IntentService implements ProgressListener { .setContentText(contentText) .setStyle(createNotificationBigStyle(hasUpdates)); - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(1, builder.build()); + notificationManager.notify(NOTIFY_ID_UPDATES_AVAILABLE, builder.build()); } private List getKnownAppIds(List apps) { @@ -783,9 +759,6 @@ public class UpdateService extends IntentService implements ProgressListener { String totalSize = Utils.getFriendlySize(event.total); int percent = (int) ((double) event.progress / event.total * 100); switch (event.type) { - case Downloader.EVENT_PROGRESS: - message = getString(R.string.status_download, repoAddress, downloadedSize, totalSize, percent); - break; case RepoUpdater.PROGRESS_TYPE_PROCESS_XML: message = getString(R.string.status_processing_xml_percent, repoAddress, downloadedSize, totalSize, percent); break; diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index 185f55bb1..98e76f0ba 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -52,7 +52,6 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { public static final int ERROR_HASH_MISMATCH = 101; public static final int ERROR_DOWNLOAD_FAILED = 102; - public static final int ERROR_UNKNOWN = 103; private static final String EVENT_SOURCE_ID = "sourceId"; private static long downloadIdCounter = 0; @@ -63,14 +62,13 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { public static final String EVENT_DATA_ERROR_TYPE = "apkDownloadErrorType"; @NonNull private final Apk curApk; + @NonNull private final Context context; @NonNull private final String repoAddress; @NonNull private final SanitizedFile localFile; @NonNull private final SanitizedFile potentiallyCachedFile; private ProgressListener listener; private AsyncDownloadWrapper dlWrapper = null; - private int progress = 0; - private int totalSize = 0; private boolean isComplete = false; private final long id = ++downloadIdCounter; @@ -84,6 +82,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { } public ApkDownloader(@NonNull final Context context, @NonNull final Apk apk, @NonNull final String repoAddress) { + this.context = context; curApk = apk; this.repoAddress = repoAddress; localFile = new SanitizedFile(Utils.getApkDownloadDir(context), apk.apkName); @@ -191,7 +190,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { Log.d(TAG, "Downloading apk from " + remoteAddress + " to " + localFile); try { - Downloader downloader = DownloaderFactory.create(remoteAddress, localFile); + Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile); dlWrapper = new AsyncDownloadWrapper(downloader, this); dlWrapper.download(); return true; @@ -214,14 +213,6 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { } private void sendProgressEvent(Event event) { - switch (event.type) { - case Downloader.EVENT_PROGRESS: - // Keep a copy of these ourselves, so people can interrogate us for the - // info (in addition to receiving events with the info). - totalSize = event.total; - progress = event.progress; - break; - } event.getData().putLong(EVENT_SOURCE_ID, id); @@ -230,18 +221,6 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { } } - @Override - public void onReceiveTotalDownloadSize(int size) { - // Do nothing... - // Rather, we will obtain the total download size from the progress events - // when they start coming through. - } - - @Override - public void onReceiveCacheTag(String cacheTag) { - // Do nothing... - } - @Override public void onErrorDownloading(String localisedExceptionDetails) { Log.e(TAG, "Download failed: " + localisedExceptionDetails); @@ -292,7 +271,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { public Apk getApk() { return curApk; } - public int getProgress() { return progress; } + public int getBytesRead() { return dlWrapper != null ? dlWrapper.getBytesRead() : 0; } - public int getTotalSize() { return totalSize; } + public int getTotalBytes() { return dlWrapper != null ? dlWrapper.getTotalBytes() : 0; } } diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java index d113f93f3..3588343b8 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java @@ -21,7 +21,6 @@ public class AsyncDownloadWrapper extends Handler { private static final String TAG = "AsyncDownloadWrapper"; - private static final int MSG_PROGRESS = 1; private static final int MSG_DOWNLOAD_COMPLETE = 2; private static final int MSG_DOWNLOAD_CANCELLED = 3; private static final int MSG_ERROR = 4; @@ -43,16 +42,6 @@ public class AsyncDownloadWrapper extends Handler { this.listener = listener; } - public void fetchTotalDownloadSize() { - int size = downloader.totalDownloadSize(); - listener.onReceiveTotalDownloadSize(size); - } - - public void fetchCacheTag() { - String cacheTag = downloader.getCacheTag(); - listener.onReceiveCacheTag(cacheTag); - } - public void download() { downloadThread = new DownloadThread(); downloadThread.start(); @@ -64,22 +53,6 @@ public class AsyncDownloadWrapper extends Handler { } } - public static class NotDownloadingException extends Exception { - public NotDownloadingException(String message) { - super(message); - } - } - - public void cancelDownload() throws NotDownloadingException { - if (downloadThread == null) { - throw new RuntimeException("Can't cancel download, it hasn't started yet."); - } else if (!downloadThread.isAlive()) { - throw new RuntimeException("Can't cancel download, it is already finished."); - } - - downloadThread.interrupt(); - } - /** * Receives "messages" from the download thread, and passes them onto the * relevant {@link org.fdroid.fdroid.net.AsyncDownloadWrapper.Listener} @@ -87,11 +60,6 @@ public class AsyncDownloadWrapper extends Handler { */ public void handleMessage(Message message) { switch (message.arg1) { - case MSG_PROGRESS: - Bundle data = message.getData(); - ProgressListener.Event event = data.getParcelable(MSG_DATA); - listener.onProgress(event); - break; case MSG_DOWNLOAD_COMPLETE: listener.onDownloadComplete(); break; @@ -104,19 +72,24 @@ public class AsyncDownloadWrapper extends Handler { } } + public int getBytesRead() { + return downloader.getBytesRead(); + } + + public int getTotalBytes() { + return downloader.getTotalBytes(); + } + public interface Listener extends ProgressListener { - void onReceiveTotalDownloadSize(int size); - void onReceiveCacheTag(String cacheTag); void onErrorDownloading(String localisedExceptionDetails); void onDownloadComplete(); void onDownloadCancelled(); } - private class DownloadThread extends Thread implements ProgressListener { + private class DownloadThread extends Thread { public void run() { try { - downloader.setProgressListener(this); downloader.download(); sendMessage(MSG_DOWNLOAD_COMPLETE); } catch (InterruptedException e) { @@ -137,16 +110,5 @@ public class AsyncDownloadWrapper extends Handler { message.arg1 = messageType; AsyncDownloadWrapper.this.sendMessage(message); } - - @Override - public void onProgress(Event event) { - Message message = new Message(); - Bundle data = new Bundle(); - data.putParcelable(MSG_DATA, event); - message.setData(data); - message.arg1 = MSG_PROGRESS; - AsyncDownloadWrapper.this.sendMessage(message); - } } - } diff --git a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java index cf9dfa170..4802f39f2 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java @@ -1,11 +1,10 @@ package org.fdroid.fdroid.net; import android.content.Context; -import android.os.Bundle; -import android.support.annotation.NonNull; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; import android.util.Log; -import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.Utils; import java.io.File; @@ -20,49 +19,31 @@ import java.net.URL; public abstract class Downloader { private static final String TAG = "Downloader"; - private OutputStream outputStream; - private ProgressListener progressListener = null; - private Bundle eventData = null; + public static final String LOCAL_ACTION_PROGRESS = "Downloader.PROGRESS"; + + public static final String EXTRA_ADDRESS = "extraAddress"; + public static final String EXTRA_BYTES_READ = "extraBytesRead"; + public static final String EXTRA_TOTAL_BYTES = "extraTotalBytes"; + + private final OutputStream outputStream; + + private final LocalBroadcastManager localBroadcastManager; private final File outputFile; - protected URL sourceUrl; + protected final URL sourceUrl; protected String cacheTag = null; - - public static final String EVENT_PROGRESS = "downloadProgress"; + protected int bytesRead = 0; + protected int totalBytes = 0; public abstract InputStream getInputStream() throws IOException; - // The context is required for opening the file to write to. - Downloader(String destFile, @NonNull Context ctx) - throws FileNotFoundException, MalformedURLException { - this(new File(ctx.getFilesDir() + File.separator + destFile)); - } - - // The context is required for opening the file to write to. - Downloader(@NonNull Context ctx) throws IOException { - this(File.createTempFile("dl-", "", ctx.getCacheDir())); - } - - Downloader(File destFile) + Downloader(Context context, URL url, File destFile) throws FileNotFoundException, MalformedURLException { + this.sourceUrl = url; outputFile = destFile; outputStream = new FileOutputStream(outputFile); - } - - Downloader(OutputStream output) - throws MalformedURLException { - outputStream = output; - outputFile = null; - } - - public void setProgressListener(ProgressListener listener) { - setProgressListener(listener, null); - } - - public void setProgressListener(ProgressListener listener, Bundle eventData) { - this.progressListener = listener; - this.eventData = eventData; + localBroadcastManager = LocalBroadcastManager.getInstance(context); } /** @@ -157,11 +138,16 @@ public abstract class Downloader { } } + /** + * This copies the downloaded data from the InputStream to the OutputStream, + * keeping track of the number of bytes that have flowed through for the + * progress counter. + */ protected void copyInputToOutputStream(InputStream input) throws IOException, InterruptedException { byte[] buffer = new byte[Utils.BUFFER_SIZE]; int bytesRead = 0; - int totalBytes = totalDownloadSize(); + this.totalBytes = totalDownloadSize(); // Getting the total download size could potentially take time, depending on how // it is implemented, so we may as well check this before we proceed. @@ -185,13 +171,19 @@ public abstract class Downloader { } protected void sendProgress(int bytesRead, int totalBytes) { - sendProgress(new ProgressListener.Event(EVENT_PROGRESS, bytesRead, totalBytes, eventData)); + this.bytesRead = bytesRead; + Intent intent = new Intent(LOCAL_ACTION_PROGRESS); + intent.putExtra(EXTRA_ADDRESS, sourceUrl.toString()); + intent.putExtra(EXTRA_BYTES_READ, bytesRead); + intent.putExtra(EXTRA_TOTAL_BYTES, totalBytes); + localBroadcastManager.sendBroadcast(intent); } - protected void sendProgress(ProgressListener.Event event) { - if (progressListener != null) { - progressListener.onProgress(event); - } + public int getBytesRead() { + return bytesRead; } + public int getTotalBytes() { + return totalBytes; + } } diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java index a6cae1438..fdc7b3255 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java @@ -4,26 +4,46 @@ import android.content.Context; import java.io.File; import java.io.IOException; +import java.net.URL; public class DownloaderFactory { - public static Downloader create(String url, Context context) + /** + * Downloads to a temporary file, which *you must delete yourself when + * you are done. It is stored in {@link Context#getCacheDir()} and starts + * with the prefix {@code dl-}. + */ + public static Downloader create(Context context, String urlString) throws IOException { - if (isOnionAddress(url)) { - return new TorHttpDownloader(url, context); - } - return new HttpDownloader(url, context); + return create(context, new URL(urlString)); } - public static Downloader create(String url, File destFile) + /** + * Downloads to a temporary file, which *you must delete yourself when + * you are done. It is stored in {@link Context#getCacheDir()} and starts + * with the prefix {@code dl-}. + */ + public static Downloader create(Context context, URL url) throws IOException { - if (isOnionAddress(url)) { - return new TorHttpDownloader(url, destFile); - } - return new HttpDownloader(url, destFile); + File destFile = File.createTempFile("dl-", "", context.getCacheDir()); + destFile.deleteOnExit(); // this probably does nothing, but maybe... + return create(context, url, destFile); } - private static boolean isOnionAddress(String url) { - return url.matches("^[a-zA-Z0-9]+://[^/]+\\.onion/.*"); + public static Downloader create(Context context, String urlString, File destFile) + throws IOException { + return create(context, new URL(urlString), destFile); + } + + public static Downloader create(Context context, URL url, File destFile) + throws IOException { + if (isOnionAddress(url)) { + return new TorHttpDownloader(context, url, destFile); + } + return new HttpDownloader(context, url, destFile); + } + + private static boolean isOnionAddress(URL url) { + return url.getHost().endsWith(".onion"); } } diff --git a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java index c1a6a3650..d134c95d0 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java @@ -27,21 +27,9 @@ public class HttpDownloader extends Downloader { protected HttpURLConnection connection; private int statusCode = -1; - // The context is required for opening the file to write to. - HttpDownloader(String source, File destFile) + HttpDownloader(Context context, URL url, File destFile) throws FileNotFoundException, MalformedURLException { - super(destFile); - sourceUrl = new URL(source); - } - - /** - * Downloads to a temporary file, which *you must delete yourself when - * you are done*. - * @see org.fdroid.fdroid.net.Downloader#getFile() - */ - HttpDownloader(String source, Context ctx) throws IOException { - super(ctx); - sourceUrl = new URL(source); + super(context, url, destFile); } @Override diff --git a/F-Droid/src/org/fdroid/fdroid/net/IconDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/IconDownloader.java index 0ba464755..b90876df8 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/IconDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/IconDownloader.java @@ -22,7 +22,7 @@ public class IconDownloader extends BaseImageDownloader { switch (Scheme.ofUri(imageUri)) { case HTTP: case HTTPS: - Downloader downloader = DownloaderFactory.create(imageUri, context); + Downloader downloader = DownloaderFactory.create(context, imageUri); return downloader.getInputStream(); default: return super.getStream(imageUri, extra); diff --git a/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java index 70848ac66..c0eaf4d45 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java @@ -10,16 +10,13 @@ import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.Proxy; import java.net.SocketAddress; +import java.net.URL; public class TorHttpDownloader extends HttpDownloader { - TorHttpDownloader(String url, Context ctx) throws IOException { - super(url, ctx); - } - - TorHttpDownloader(String url, File destFile) + TorHttpDownloader(Context context, URL url, File destFile) throws FileNotFoundException, MalformedURLException { - super(url, destFile); + super(context, url, destFile); } @Override diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java index b895f6a14..20ae144bd 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java @@ -64,7 +64,6 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.fdroid.fdroid.FDroid; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; -import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.compat.ClipboardCompat; @@ -74,7 +73,6 @@ import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.net.MDnsHelper; import org.fdroid.fdroid.net.MDnsHelper.DiscoveredRepo; import org.fdroid.fdroid.net.MDnsHelper.RepoScanListAdapter; -import org.fdroid.fdroid.views.fragments.RepoDetailsFragment; import java.io.IOException; import java.net.MalformedURLException; @@ -87,13 +85,6 @@ import java.util.Locale; import javax.jmdns.ServiceInfo; public class ManageReposActivity extends ActionBarActivity { - - /** - * If we have a new repo added, or the address of a repo has changed, then - * we when we're finished, we'll set this boolean to true in the intent that - * we finish with, to signify that we want the main list of apps updated. - */ - public static final String REQUEST_UPDATE = "update"; private static final String TAG = "ManageReposActivity"; private static final String DEFAULT_NEW_REPO_TEXT = "https://"; @@ -104,10 +95,6 @@ public class ManageReposActivity extends ActionBarActivity { IS_SWAP } - private UpdateService.UpdateReceiver updateHandler = null; - - private static boolean changed = false; - private RepoListFragment listFragment; /** @@ -152,21 +139,11 @@ public class ManageReposActivity extends ActionBarActivity { @Override protected void onResume() { super.onResume(); - if (updateHandler != null) { - updateHandler.showDialog(this); - } + /* let's see if someone is trying to send us a new repo */ addRepoFromIntent(getIntent()); } - @Override - protected void onPause() { - super.onPause(); - if (updateHandler != null) { - updateHandler.hideDialog(); - } - } - @Override protected void onNewIntent(Intent intent) { setIntent(intent); @@ -175,22 +152,10 @@ public class ManageReposActivity extends ActionBarActivity { @Override public void finish() { Intent ret = new Intent(); - markChangedIfRequired(ret); setResult(RESULT_OK, ret); super.finish(); } - private boolean hasChanged() { - return changed; - } - - private void markChangedIfRequired(Intent intent) { - if (hasChanged()) { - Log.i(TAG, "Repo details have changed, prompting for update."); - intent.putExtra(REQUEST_UPDATE, true); - } - } - @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.manage_repos, menu); @@ -202,7 +167,6 @@ public class ManageReposActivity extends ActionBarActivity { switch (item.getItemId()) { case android.R.id.home: Intent destIntent = new Intent(this, FDroid.class); - markChangedIfRequired(destIntent); setResult(RESULT_OK, destIntent); NavUtils.navigateUpTo(this, destIntent); return true; @@ -210,7 +174,7 @@ public class ManageReposActivity extends ActionBarActivity { showAddRepo(); return true; case R.id.action_update_repo: - updateRepos(); + UpdateService.updateNow(this); return true; case R.id.action_find_local_repos: scanForRepos(); @@ -219,26 +183,6 @@ public class ManageReposActivity extends ActionBarActivity { return super.onOptionsItemSelected(item); } - private void updateRepos() { - updateHandler = UpdateService.updateNow(this).setListener( - new ProgressListener() { - @Override - public void onProgress(Event event) { - switch (event.type) { - case UpdateService.EVENT_COMPLETE_AND_SAME: - case UpdateService.EVENT_COMPLETE_WITH_CHANGES: - // No need to prompt to update any more, we just - // did it! - changed = false; - break; - case UpdateService.EVENT_FINISHED: - updateHandler = null; - break; - } - } - }); - } - private void scanForRepos() { final RepoScanListAdapter adapter = new RepoScanListAdapter(this); final MDnsHelper mDnsHelper = new MDnsHelper(this, adapter); @@ -706,11 +650,11 @@ public class ManageReposActivity extends ActionBarActivity { /** * If started by an intent that expects a result (e.g. QR codes) then we - * will set a result and finish. Otherwise, we'll refresh the list of repos + * will set a result and finish. Otherwise, we'll updateViews the list of repos * to reflect the newly created repo. */ private void finishedAddingRepo() { - changed = true; + UpdateService.updateNow(ManageReposActivity.this); if (addRepoDialog.isShowing()) { addRepoDialog.dismiss(); } @@ -783,7 +727,7 @@ public class ManageReposActivity extends ActionBarActivity { /** * NOTE: If somebody toggles a repo off then on again, it will have * removed all apps from the index when it was toggled off, so when it - * is toggled on again, then it will require a refresh. Previously, I + * is toggled on again, then it will require a updateViews. Previously, I * toyed with the idea of remembering whether they had toggled on or * off, and then only actually performing the function when the activity * stopped, but I think that will be problematic. What about when they @@ -803,7 +747,7 @@ public class ManageReposActivity extends ActionBarActivity { RepoProvider.Helper.update(getActivity(), repo, values); if (isEnabled) { - changed = true; + UpdateService.updateNow(getActivity()); } else { FDroidApp app = (FDroidApp) getActivity().getApplication(); RepoProvider.Helper.purgeApps(getActivity(), repo, app); @@ -888,7 +832,7 @@ public class ManageReposActivity extends ActionBarActivity { public void editRepo(Repo repo) { Intent intent = new Intent(getActivity(), RepoDetailsActivity.class); - intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.getId()); + intent.putExtra(RepoDetailsActivity.ARG_REPO_ID, repo.getId()); startActivityForResult(intent, SHOW_REPO_DETAILS); } diff --git a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 51e965828..c49325c45 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -1,33 +1,91 @@ package org.fdroid.fdroid.views; import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; import android.nfc.NdefMessage; import android.nfc.NfcAdapter; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; +import android.support.v7.app.AlertDialog; import android.support.v4.app.NavUtils; +import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.ActionBarActivity; +import android.text.TextUtils; import android.util.Log; +import android.view.Menu; import android.view.MenuItem; -import android.widget.LinearLayout; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; import android.widget.Toast; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NfcHelper; +import org.fdroid.fdroid.NfcNotEnabledActivity; +import org.fdroid.fdroid.QrGenAsyncTask; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.views.fragments.RepoDetailsFragment; + +import java.util.Locale; public class RepoDetailsActivity extends ActionBarActivity { private static final String TAG = "RepoDetailsActivity"; - private Repo repo; + public static final String MIME_TYPE = "application/vnd.org.fdroid.fdroid.repo"; + public static final String ARG_REPO_ID = "repo_id"; - static final String MIME_TYPE = "application/vnd.org.fdroid.fdroid.repo"; + /** + * If the repo has been updated at least once, then we will show + * all of this info, otherwise they will be hidden. + */ + private static final int[] SHOW_IF_EXISTS = { + R.id.label_repo_name, + R.id.text_repo_name, + R.id.label_description, + R.id.text_description, + R.id.label_num_apps, + R.id.text_num_apps, + R.id.label_last_update, + R.id.text_last_update, + R.id.label_repo_fingerprint, + R.id.text_repo_fingerprint, + R.id.text_repo_fingerprint_description + }; + /** + * If the repo has not been updated yet, then we only show + * these, otherwise they are hidden. + */ + private static final int[] HIDE_IF_EXISTS = { + R.id.text_not_yet_updated, + R.id.btn_update + }; + private Repo repo; + private long repoId; + private View repoView; + + /** + * Help function to make switching between two view states easier. + * Perhaps there is a better way to do this. I recall that using Adobe + * Flex, there was a thing called "ViewStates" for exactly this. Wonder if + * that exists in Android? + */ + private static void setMultipleViewVisibility(View parent, + int[] viewIds, + int visibility) { + for (int viewId : viewIds) { + parent.findViewById(viewId).setVisibility(visibility); + } + } @Override protected void onCreate(Bundle savedInstanceState) { @@ -35,27 +93,11 @@ public class RepoDetailsActivity extends ActionBarActivity { ((FDroidApp) getApplication()).applyTheme(this); super.onCreate(savedInstanceState); - long repoId = getIntent().getLongExtra(RepoDetailsFragment.ARG_REPO_ID, 0); - - if (savedInstanceState == null) { - - // Need to set a dummy view (which will get overridden by the fragment manager - // below) so that we can call setContentView(). This is a work around for - // a (bug?) thing in 3.0, 3.1 which requires setContentView to be invoked before - // the actionbar is played with: - // http://blog.perpetumdesign.com/2011/08/strange-case-of-dr-action-and-mr-bar.html - if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT <= 13) { - setContentView(new LinearLayout(this)); - } - - RepoDetailsFragment fragment = new RepoDetailsFragment(); - fragment.setArguments(getIntent().getExtras()); - getSupportFragmentManager() - .beginTransaction() - .add(android.R.id.content, fragment) - .commit(); - } + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.repodetails); + repoView = findViewById(R.id.repoView); + repoId = getIntent().getLongExtra(ARG_REPO_ID, 0); final String[] projection = { RepoProvider.DataColumns.NAME, RepoProvider.DataColumns.ADDRESS, @@ -63,8 +105,23 @@ public class RepoDetailsActivity extends ActionBarActivity { }; repo = RepoProvider.Helper.findById(this, repoId, projection); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setTitle(repo.getName()); + setTitle(repo.name); + + TextView inputUrl = (TextView) findViewById(R.id.input_repo_url); + inputUrl.setText(repo.address); + + Button update = (Button) repoView.findViewById(R.id.btn_update); + update.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + performUpdate(); + } + }); + + Uri uri = Uri.parse(repo.address); + uri = uri.buildUpon().appendQueryParameter("fingerprint", repo.fingerprint).build(); + String qrUriString = uri.toString().toUpperCase(Locale.ENGLISH); + new QrGenAsyncTask(this, R.id.qr_code).execute(uri.toString()); } @TargetApi(14) @@ -82,6 +139,19 @@ public class RepoDetailsActivity extends ActionBarActivity { @Override public void onResume() { super.onResume(); + + /* + * After, for example, a repo update, the details will have changed in the + * database. However, or local reference to the Repo object will not + * have been updated. The safest way to deal with this is to reload the + * repo object directly from the database. + */ + repo = RepoProvider.Helper.findById(this, repoId); + updateRepoView(); + + LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, + new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); + // FDroid.java and AppDetails set different NFC actions, so reset here setNfc(); processIntent(getIntent()); @@ -105,21 +175,189 @@ public class RepoDetailsActivity extends ActionBarActivity { Log.i(TAG, "Got this URL: " + url); Toast.makeText(this, "Got this URL: " + url, Toast.LENGTH_LONG).show(); Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - String packageName = getPackageName(); - intent.setClassName(packageName, packageName + ".ManageRepo"); + intent.setClass(this, ManageReposActivity.class); startActivity(intent); finish(); } } + private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int statusCode = intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1); + if (statusCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES) + updateRepoView(); + } + }; + + @Override + protected void onPause() { + super.onPause(); + LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.repo_details_activity, menu); + return true; + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + case R.id.menu_update: + performUpdate(); + return true; + case R.id.menu_delete: + promptForDelete(); + return true; + case R.id.menu_enable_nfc: + Intent intent = new Intent(this, NfcNotEnabledActivity.class); + startActivity(intent); + return true; } + return super.onOptionsItemSelected(item); } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if (Build.VERSION.SDK_INT >= 14) { + prepareNfcMenuItems(menu); + } + return true; + } + + @TargetApi(16) + private void prepareNfcMenuItems(Menu menu) { + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (nfcAdapter == null) { + return; + } + + boolean needsEnableNfcMenuItem; + if (Build.VERSION.SDK_INT < 16) { + needsEnableNfcMenuItem = !nfcAdapter.isEnabled(); + } else { + needsEnableNfcMenuItem = !nfcAdapter.isNdefPushEnabled(); + } + + MenuItem menuItem = menu.findItem(R.id.menu_enable_nfc); + menuItem.setVisible(needsEnableNfcMenuItem); + } + + private void setupDescription(View parent, Repo repo) { + + TextView descriptionLabel = (TextView) parent.findViewById(R.id.label_description); + TextView description = (TextView) parent.findViewById(R.id.text_description); + + if (TextUtils.isEmpty(repo.description)) { + descriptionLabel.setVisibility(View.GONE); + description.setVisibility(View.GONE); + description.setText(""); + } else { + descriptionLabel.setVisibility(View.VISIBLE); + description.setVisibility(View.VISIBLE); + description.setText(repo.description.replaceAll("\n", " ")); + } + } + + private void setupRepoFingerprint(View parent, Repo repo) { + TextView repoFingerprintView = (TextView) parent.findViewById(R.id.text_repo_fingerprint); + TextView repoFingerprintDescView = (TextView) parent.findViewById(R.id.text_repo_fingerprint_description); + + String repoFingerprint; + int repoFingerprintColor; + + // TODO show the current state of the signature check, not just whether there is a key or not + if (TextUtils.isEmpty(repo.fingerprint) && TextUtils.isEmpty(repo.pubkey)) { + repoFingerprint = getResources().getString(R.string.unsigned); + repoFingerprintColor = getResources().getColor(R.color.unsigned); + repoFingerprintDescView.setVisibility(View.VISIBLE); + repoFingerprintDescView.setText(getResources().getString(R.string.unsigned_description)); + } else { + // this is based on repo.fingerprint always existing, which it should + repoFingerprint = Utils.formatFingerprint(this, repo.fingerprint); + repoFingerprintColor = getResources().getColor(R.color.signed); + repoFingerprintDescView.setVisibility(View.GONE); + } + + repoFingerprintView.setText(repoFingerprint); + repoFingerprintView.setTextColor(repoFingerprintColor); + } + + private void updateRepoView() { + + if (repo.hasBeenUpdated()) { + updateViewForExistingRepo(repoView); + } else { + updateViewForNewRepo(repoView); + } + + } + + private void updateViewForNewRepo(View repoView) { + setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.VISIBLE); + setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.GONE); + } + + private void updateViewForExistingRepo(View repoView) { + setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.VISIBLE); + setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.GONE); + + TextView name = (TextView) repoView.findViewById(R.id.text_repo_name); + TextView numApps = (TextView) repoView.findViewById(R.id.text_num_apps); + TextView lastUpdated = (TextView) repoView.findViewById(R.id.text_last_update); + + name.setText(repo.name); + + int appCount = RepoProvider.Helper.countAppsForRepo(this, repoId); + numApps.setText(Integer.toString(appCount)); + + setupDescription(repoView, repo); + setupRepoFingerprint(repoView, repo); + + // Repos that existed before this feature was supported will have an + // "Unknown" last update until next time they update... + String lastUpdate = repo.lastUpdated != null + ? repo.lastUpdated.toString() : getString(R.string.unknown); + lastUpdated.setText(lastUpdate); + } + + /** + * When an update is performed, notify the listener so that the repo + * list can be updated. We will perform the update ourselves though. + */ + private void performUpdate() { + // Ensure repo is enabled before updating... + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, 1); + RepoProvider.Helper.update(this, repo, values); + + UpdateService.updateRepoNow(repo.address, this); + } + + private void promptForDelete() { + new AlertDialog.Builder(this) + .setTitle(R.string.repo_confirm_delete_title) + .setMessage(R.string.repo_confirm_delete_body) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + RepoProvider.Helper.remove(getApplicationContext(), repoId); + finish(); + } + }).setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Do nothing... + } + } + ).show(); + } + } diff --git a/F-Droid/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/F-Droid/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java deleted file mode 100644 index 9986b9149..000000000 --- a/F-Droid/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ /dev/null @@ -1,392 +0,0 @@ -package org.fdroid.fdroid.views.fragments; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.ContentValues; -import android.content.DialogInterface; -import android.content.Intent; -import android.nfc.NfcAdapter; -import android.os.Build; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.view.MenuItemCompat; -import android.support.v7.app.AlertDialog; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; - -import org.fdroid.fdroid.NfcNotEnabledActivity; -import org.fdroid.fdroid.ProgressListener; -import org.fdroid.fdroid.R; -import org.fdroid.fdroid.UpdateService; -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; - -public class RepoDetailsFragment extends Fragment { - - public static final String ARG_REPO_ID = "repo_id"; - - /** - * If the repo has been updated at least once, then we will show - * all of this info, otherwise they will be hidden. - */ - private static final int[] SHOW_IF_EXISTS = { - R.id.label_repo_name, - R.id.text_repo_name, - R.id.label_description, - R.id.text_description, - R.id.label_num_apps, - R.id.text_num_apps, - R.id.label_last_update, - R.id.text_last_update, - R.id.label_repo_fingerprint, - R.id.text_repo_fingerprint, - R.id.text_repo_fingerprint_description - }; - - /** - * If the repo has not been updated yet, then we only show - * these, otherwise they are hidden. - */ - private static final int[] HIDE_IF_EXISTS = { - R.id.text_not_yet_updated, - R.id.btn_update - }; - - private static final int DELETE = 0; - private static final int UPDATE = 1; - private static final int ENABLE_NFC = 2; - - private static final String TAG = "RepoDetailsFragment"; - - private MenuItem enableNfc = null; - private UpdateService.UpdateReceiver updateHandler = null; - - // TODO: Currently initialised in onCreateView. Not sure if that is the - // best way to go about this... - private Repo repo; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - if (updateHandler != null) { - updateHandler.showDialog(getActivity()); - } - } - - @Override - public void onDetach() { - super.onDetach(); - if (updateHandler != null) { - updateHandler.hideDialog(); - } - } - - private long getRepoId() { - return getArguments().getLong(RepoDetailsFragment.ARG_REPO_ID, 0); - } - - /** - * After, for example, a repo update, the details will have changed in the - * database. However, or local reference to the Repo object will not - * have been updated. The safest way to deal with this is to reload the - * repo object directly from the database. - */ - private Repo loadRepoDetails() { - return RepoProvider.Helper.findById(getActivity(), getRepoId()); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - - repo = loadRepoDetails(); - - if (repo == null) { - Log.e(TAG, "Error showing details for repo '" + getRepoId() + "'"); - return new LinearLayout(container.getContext()); - } - - ViewGroup repoView = (ViewGroup)inflater.inflate(R.layout.repodetails, null); - updateView(repoView); - - // Setup listeners here, rather than in updateView(...), - // because otherwise we will end up adding multiple listeners with - // subsequent calls to updateView(). - EditText inputUrl = (EditText)repoView.findViewById(R.id.input_repo_url); - inputUrl.addTextChangedListener(new UrlWatcher()); - - Button update = (Button)repoView.findViewById(R.id.btn_update); - update.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - performUpdate(); - } - }); - - return repoView; - } - - /** - * Populates relevant views with properties from the current repository. - * Decides which views to show and hide depending on the state of the - * repository. - */ - private void updateView(ViewGroup repoView) { - - EditText inputUrl = (EditText)repoView.findViewById(R.id.input_repo_url); - inputUrl.setText(repo.address); - - if (repo.hasBeenUpdated()) { - updateViewForExistingRepo(repoView); - } else { - updateViewForNewRepo(repoView); - } - - } - - /** - * Help function to make switching between two view states easier. - * Perhaps there is a better way to do this. I recall that using Adobe - * Flex, there was a thing called "ViewStates" for exactly this. Wonder if - * that exists in Android? - */ - private static void setMultipleViewVisibility(ViewGroup parent, - int[] viewIds, - int visibility) { - for (int viewId : viewIds) { - parent.findViewById(viewId).setVisibility(visibility); - } - } - - private void updateViewForNewRepo(ViewGroup repoView) { - setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.VISIBLE); - setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.GONE); - } - - private void updateViewForExistingRepo(ViewGroup repoView) { - setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.VISIBLE); - setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.GONE); - - TextView name = (TextView)repoView.findViewById(R.id.text_repo_name); - TextView numApps = (TextView)repoView.findViewById(R.id.text_num_apps); - TextView lastUpdated = (TextView)repoView.findViewById(R.id.text_last_update); - - name.setText(repo.getName()); - - int appCount = RepoProvider.Helper.countAppsForRepo(getActivity(), repo.getId()); - numApps.setText(Integer.toString(appCount)); - - setupDescription(repoView, repo); - setupRepoFingerprint(repoView, repo); - - // Repos that existed before this feature was supported will have an - // "Unknown" last update until next time they update... - String lastUpdate = repo.lastUpdated != null - ? repo.lastUpdated.toString() : getString(R.string.unknown); - lastUpdated.setText(lastUpdate); - } - - private void setupDescription(ViewGroup parent, Repo repo) { - - TextView descriptionLabel = (TextView)parent.findViewById(R.id.label_description); - TextView description = (TextView)parent.findViewById(R.id.text_description); - - if (repo.description == null || repo.description.length() == 0) { - descriptionLabel.setVisibility(View.GONE); - description.setVisibility(View.GONE); - } else { - descriptionLabel.setVisibility(View.VISIBLE); - description.setVisibility(View.VISIBLE); - } - - String descriptionText = repo.description == null - ? "" : repo.description.replaceAll("\n", " "); - description.setText(descriptionText); - - } - - /** - * When an update is performed, notify the listener so that the repo - * list can be updated. We will perform the update ourselves though. - */ - private void performUpdate() { - // Ensure repo is enabled before updating... - ContentValues values = new ContentValues(1); - values.put(RepoProvider.DataColumns.IN_USE, 1); - RepoProvider.Helper.update(getActivity(), repo, values); - - updateHandler = UpdateService.updateRepoNow(repo.address, getActivity()).setListener(new ProgressListener() { - @Override - public void onProgress(Event event) { - switch (event.type) { - case UpdateService.EVENT_COMPLETE_WITH_CHANGES: - repo = loadRepoDetails(); - updateView((ViewGroup)getView()); - break; - case UpdateService.EVENT_FINISHED: - updateHandler = null; - break; - } - } - }); - } - - /** - * When the URL is changed, notify the repoChangeListener. - */ - class UrlWatcher implements TextWatcher { - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void afterTextChanged(Editable s) {} - - @Override - // TODO: This is called each character change, resulting in a DB query. - // Doesn't exactly cause performance problems, - // but seems silly not to go for more of a "focus out" event then - // this "text changed" event. - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (!repo.address.equals(s.toString())) { - ContentValues values = new ContentValues(1); - values.put(RepoProvider.DataColumns.ADDRESS, s.toString()); - RepoProvider.Helper.update(getActivity(), repo, values); - } - } - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - menu.clear(); - - MenuItem update = menu.add(Menu.NONE, UPDATE, 0, R.string.repo_update); - update.setIcon(R.drawable.ic_refresh_white); - MenuItemCompat.setShowAsAction(update, - MenuItemCompat.SHOW_AS_ACTION_ALWAYS | - MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); - - MenuItem delete = menu.add(Menu.NONE, DELETE, 0, R.string.delete); - delete.setIcon(R.drawable.ic_delete_white); - MenuItemCompat.setShowAsAction(delete, - MenuItemCompat.SHOW_AS_ACTION_IF_ROOM | - MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - if (Build.VERSION.SDK_INT >= 14) - prepareNfcMenuItems(menu); - } - - @TargetApi(16) - private void prepareNfcMenuItems(Menu menu) { - boolean needsEnableNfcMenuItem; - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); - if (nfcAdapter == null) { - return; - } - if (Build.VERSION.SDK_INT < 16) - needsEnableNfcMenuItem = !nfcAdapter.isEnabled(); - else - needsEnableNfcMenuItem = !nfcAdapter.isNdefPushEnabled(); - if (needsEnableNfcMenuItem) { - if (enableNfc != null) - return; // already created - enableNfc = menu.add(Menu.NONE, ENABLE_NFC, 0, R.string.enable_nfc_send); - enableNfc.setIcon(R.drawable.ic_nfc_white); - MenuItemCompat.setShowAsAction(enableNfc, - MenuItemCompat.SHOW_AS_ACTION_IF_ROOM | - MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); - } else if (enableNfc != null) { - // remove the existing MenuItem since NFC is now enabled - menu.removeItem(enableNfc.getItemId()); - enableNfc = null; - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - switch (item.getItemId()) { - case DELETE: - promptForDelete(); - return true; - case UPDATE: - performUpdate(); - return true; - case ENABLE_NFC: - Intent intent = new Intent(getActivity(), NfcNotEnabledActivity.class); - startActivity(intent); - return true; - } - - return false; - } - - private void promptForDelete() { - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.repo_confirm_delete_title) - .setMessage(R.string.repo_confirm_delete_body) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Repo repo = RepoDetailsFragment.this.repo; - RepoProvider.Helper.remove(getActivity(), repo.getId()); - getActivity().finish(); - } - }).setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // Do nothing... - } - } - ).show(); - } - - private void setupRepoFingerprint(ViewGroup parent, Repo repo) { - TextView repoFingerprintView = (TextView)parent.findViewById(R.id.text_repo_fingerprint); - TextView repoFingerprintDescView = (TextView)parent.findViewById(R.id.text_repo_fingerprint_description); - - String repoFingerprint; - int repoFingerprintColor; - - // TODO show the current state of the signature check, not just whether there is a key or not - if (TextUtils.isEmpty(repo.fingerprint) && TextUtils.isEmpty(repo.pubkey)) { - repoFingerprint = getResources().getString(R.string.unsigned); - repoFingerprintColor = getResources().getColor(R.color.unsigned); - repoFingerprintDescView.setVisibility(View.VISIBLE); - repoFingerprintDescView.setText(getResources().getString(R.string.unsigned_description)); - } else { - // this is based on repo.fingerprint always existing, which it should - repoFingerprint = Utils.formatFingerprint(getActivity(), repo.fingerprint); - repoFingerprintColor = getResources().getColor(R.color.signed); - repoFingerprintDescView.setVisibility(View.GONE); - } - - repoFingerprintView.setText(repoFingerprint); - repoFingerprintView.setTextColor(repoFingerprintColor); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - setRetainInstance(true); - } - -} diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java index 3eebca77c..2cb2afa6d 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java @@ -1,13 +1,17 @@ package org.fdroid.fdroid.views.swap; import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.ContentValues; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; import android.net.http.AndroidHttpClient; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.ActionBarActivity; import android.util.Log; import android.view.View; @@ -20,7 +24,6 @@ import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.message.BasicNameValuePair; import org.fdroid.fdroid.FDroidApp; -import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; @@ -33,7 +36,7 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; -public class ConnectSwapActivity extends ActionBarActivity implements ProgressListener { +public class ConnectSwapActivity extends ActionBarActivity { private static final String TAG = "ConnectSwapActivity"; private static final String STATE_CONFIRM = "startSwap"; @@ -79,6 +82,10 @@ public class ConnectSwapActivity extends ActionBarActivity implements ProgressLi @Override protected void onResume() { super.onResume(); + + LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, + new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); + // Only confirm the action, and then return a result... newRepoConfig = new NewRepoConfig(this, getIntent()); if (newRepoConfig.isValidRepo()) { @@ -91,40 +98,48 @@ public class ConnectSwapActivity extends ActionBarActivity implements ProgressLi } @Override - @SuppressWarnings("fallthrough") - public void onProgress(Event event) { - // TODO: Show progress, but we can worry about that later. - // Might be nice to have it nicely embedded in the UI, rather than as - // an additional dialog. E.g. White text on blue, letting the user - // know what we are up to. + protected void onPause() { + super.onPause(); + LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); + } - switch (event.type) { - case UpdateService.EVENT_COMPLETE_AND_SAME: - Log.i(TAG, "EVENT_COMPLETE_AND_SAME"); - case UpdateService.EVENT_COMPLETE_WITH_CHANGES: - Log.i(TAG, "EVENT_COMPLETE_WITH_CHANGES"); - Intent intent = new Intent(this, SwapAppListActivity.class); - intent.putExtra(SwapAppListActivity.EXTRA_REPO_ID, repo.getId()); - startActivity(intent); - finish(); + BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // TODO: Show progress, but we can worry about that later. + // Might be nice to have it nicely embedded in the UI, rather than as + // an additional dialog. E.g. White text on blue, letting the user + // know what we are up to. + int statusCode = intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1); + + switch (statusCode) { + case UpdateService.STATUS_COMPLETE_AND_SAME: + Log.i(TAG, "STATUS_COMPLETE_AND_SAME"); + case UpdateService.STATUS_COMPLETE_WITH_CHANGES: + Log.i(TAG, "STATUS_COMPLETE_WITH_CHANGES"); + Intent salIntent = new Intent(getBaseContext(), SwapAppListActivity.class); + salIntent.putExtra(SwapAppListActivity.EXTRA_REPO_ID, repo.getId()); + startActivity(salIntent); + finish(); /* // TODO: Load repo from database to get proper name. This is what the category we want to select will be called. intent.putExtra("category", newRepoConfig.getHost()); getActivity().setResult(Activity.RESULT_OK, intent); */ - break; - case UpdateService.EVENT_ERROR: - // TODO: Show message on this screen (with a big "okay" button that goes back to F-Droid activity) - // rather than finishing directly. - finish(); - break; + break; + case UpdateService.STATUS_ERROR_GLOBAL: + // TODO: Show message on this screen (with a big "okay" button that goes back to F-Droid activity) + // rather than finishing directly. + finish(); + break; + } } - } + }; private void confirm() { repo = ensureRepoExists(); if (repo != null) { - UpdateService.updateRepoNow(repo.address, this).setListener(this); + UpdateService.updateRepoNow(repo.address, this); } }