From 8c251340315410d9f99b1a681d308f62f317cf11 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 10 Jul 2015 02:51:43 +0200 Subject: [PATCH 01/18] Replaced download dialog with embedded progress bar (fixes #270). --- F-Droid/res/drawable-hdpi/ic_clear.png | Bin 0 -> 207 bytes F-Droid/res/drawable-mdpi/ic_clear.png | Bin 0 -> 164 bytes F-Droid/res/drawable-xhdpi/ic_clear.png | Bin 0 -> 235 bytes F-Droid/res/drawable-xxhdpi/ic_clear.png | Bin 0 -> 309 bytes F-Droid/res/drawable-xxxhdpi/ic_clear.png | Bin 0 -> 377 bytes F-Droid/res/layout/app_details_header.xml | 48 +++- F-Droid/res/values/strings.xml | 9 + F-Droid/src/org/fdroid/fdroid/AppDetails.java | 245 ++++++++++-------- .../fdroid/views/ManageReposActivity.java | 4 +- 9 files changed, 187 insertions(+), 119 deletions(-) create mode 100644 F-Droid/res/drawable-hdpi/ic_clear.png create mode 100644 F-Droid/res/drawable-mdpi/ic_clear.png create mode 100644 F-Droid/res/drawable-xhdpi/ic_clear.png create mode 100644 F-Droid/res/drawable-xxhdpi/ic_clear.png create mode 100644 F-Droid/res/drawable-xxxhdpi/ic_clear.png 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 0000000000000000000000000000000000000000..1a9cd75a0d2692fa380f367bdb41c2420df310b0 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8LpG*1`DkP61PSG>8J9RwIIzPh7b zJflYMsk(bE^P@Beu4=(eGmA7oyR_x~a$D?{a5p37g|Cs|p&E~Q4@@WZu3cW0RzKfl zOG4W1`7gJ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..40a1a84e34f4fb9c31b1dccf1b2bd7f5a50ac00d GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+iY)==*kP619lQs%6EAp^x=L~Fl z``+Hk)n=Dm!pL5PtJwWQDwD%^7mKX72vfc*YT)xac;A{}-v*=ps zr9JO5s)Jrvt$1~wJ<6?Hv?}9?*Yy)i*G<&jSt@L~vF4xM{qD^_8RvC6y^4F&G81Sm NgQu&X%Q~loCIAp8KMVi> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6bc437298ab7bfbfbf128acdf5849e304b3c6903 GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DO`a}}Ar*{or)}gqq9DL3|6^j{ z=hBm&C)~N@b{5Sp4w$_m*}geiNBekToKL{TWpQP5xqRwZuPjq53pI^Z3A|FbUaRcY z{O^J0fmhZ3y?R?;$@JP$iP2W!Cd>8DEZ3V|*s2`1aV~kzx#D@}&nn~61{Xzi+b(jKntiPo%%5fP`-@uPgu|~7wTA~r iR9eY#70Eo`TEl2=&Qt5V_=GOd^$eb_elF{r5}E*W1LhEy=VysgJ$~x+g}6OvsxLimbaeN-!xQItoH=3nd`|H@dF!kYp_dChV`b9AudQLRn7yg2 zv~UiON9*MGOYAy6D=_*g@;c3(5`S(Hi^X;wk8Ms*3SMP^!WTf z-E_DAd$kLMWY`Z`);MuKF9=d$?_mzLoFj7Xp~|_3ODg!(3;AT&wLoS^O+0tsvcZJ& zD(m_$dNm(!9@n+CTyDa$w$*a^-$!>qK3P{4end8+qbAwoFEAV!JYD@<);T3K0RXh> Be)#|Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..df42feecb812b02df59b016f4d9ba995be1da82a GIT binary patch literal 377 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7!06!V;uuoF`1aP_Or}5)*N1l- zmd#l`*P1uuRG{<>g~}cLi3NSnFL zkGQYSP&@m;*IK6Q-#O#%t5^JG`{BMqf8+n2Z|={&zE4otx%%IU37m2NTYWzKGCBDA zGRyk&LVN6HtbDhUpKVe#i0?1F$Iko1uY_g0mfJ~qyju(6`+^n!Z*4iz5hh+Bv?p(-%zt$M-+!?Iw6Q#P16<$f#A@_7oVT44$rjF6*2U FngA8*uUr5C literal 0 HcmV?d00001 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/values/strings.xml b/F-Droid/res/values/strings.xml index f6e316951..2d4952d0c 100644 --- a/F-Droid/res/values/strings.xml +++ b/F-Droid/res/values/strings.xml @@ -375,4 +375,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..34ed9dad0 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -21,7 +21,6 @@ package org.fdroid.fdroid; import android.app.Activity; -import android.app.ProgressDialog; import android.bluetooth.BluetoothAdapter; import android.content.ActivityNotFoundException; import android.content.ContentValues; @@ -34,7 +33,6 @@ 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.v4.app.Fragment; @@ -63,9 +61,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 +90,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; @@ -124,7 +125,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A private FDroidApp fdroidApp; private ApkListAdapter adapter; - private ProgressDialog progressDialog; private static class ViewHolder { TextView version; @@ -323,11 +323,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 { @@ -410,12 +413,12 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A // chosen based on more than just orientation (e.g. large screen sizes). View onlyInLandscape = findViewById(R.id.app_summary_container); - AppDetailsListFragment listFragment = + AppDetailsListFragment mListFragment = (AppDetailsListFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_app_list); if (onlyInLandscape == null) { - listFragment.setupSummaryHeader(); + mListFragment.setupSummaryHeader(); } else { - listFragment.removeSummaryHeader(); + mListFragment.removeSummaryHeader(); } // Spinner seems to default to visible on Android 4.0.3 and 4.0.4 @@ -443,38 +446,33 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } @Override - protected void onResume() { - super.onResume(); + protected void onResumeFragments() { + super.onResumeFragments(); + refreshApkList(); + refreshHeader(); + supportInvalidateOptionsMenu(); 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(); + if (downloadHandler.getTotalSize() == 0) + mHeaderFragment.startProgress(); + else + mHeaderFragment.updateProgress(downloadHandler.getProgress(), + downloadHandler.getTotalSize()); } } } - @Override - protected void onResumeFragments() { - super.onResumeFragments(); - refreshApkList(); - refreshHeader(); - supportInvalidateOptionsMenu(); - } - /** - * 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; } } @@ -508,7 +506,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A downloadHandler.removeProgressListener(); } - removeProgressDialog(); + mHeaderFragment.removeProgress(); } private void onAppChanged() { @@ -553,13 +551,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 +610,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 @@ -805,6 +796,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) { @@ -856,7 +851,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A downloadHandler = new ApkDownloader(getBaseContext(), apk, repoAddress); downloadHandler.setProgressListener(this); if (downloadHandler.download()) { - updateProgressDialog(); + mHeaderFragment.startProgress(); } } @@ -945,79 +940,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)) { @@ -1035,7 +957,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A boolean finished = false; switch (event.type) { case Downloader.EVENT_PROGRESS: - updateProgressDialog(event.progress, event.total); + if (mHeaderFragment != null) + mHeaderFragment.updateProgress(event.progress, event.total); break; case ApkDownloader.EVENT_ERROR: final String text; @@ -1054,7 +977,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } if (finished) { - removeProgressDialog(); + if (mHeaderFragment != null) + mHeaderFragment.removeProgress(); downloadHandler = null; } } @@ -1447,9 +1371,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 +1422,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 +1555,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()) { diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java index b895f6a14..6a279de59 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java @@ -706,7 +706,7 @@ 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() { @@ -783,7 +783,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 From 7ea294bb668ad9f485d2a9f7a342ad1cd27c1ba9 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 18 Jun 2015 13:19:03 -0400 Subject: [PATCH 02/18] switch RepoDetailsActivity to plain structure, purge RepoDetailsFragment No need for a reusable Fragment here, its only used in one place. This changes the structure to be a regular Activity, with all View and Menu setup in XML files loaded in onCreate(). This also converts the URL to a TextView. Having it editable in this Activity makes for a confusing user experience. Instead, the "Add Repo" input should validate the URL and not allow creating repos that don't work. This also purges the use of UpdateService.UpdateReceiver, it will be going away in the upcoming commits. --- F-Droid/res/layout/repodetails.xml | 16 +- F-Droid/res/menu/repo_details_activity.xml | 21 + .../fdroid/views/ManageReposActivity.java | 3 +- .../fdroid/views/RepoDetailsActivity.java | 277 +++++++++++-- .../views/fragments/RepoDetailsFragment.java | 392 ------------------ 5 files changed, 275 insertions(+), 434 deletions(-) create mode 100644 F-Droid/res/menu/repo_details_activity.xml delete mode 100644 F-Droid/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java diff --git a/F-Droid/res/layout/repodetails.xml b/F-Droid/res/layout/repodetails.xml index 86cb9b1a2..7a7f1f9ef 100644 --- a/F-Droid/res/layout/repodetails.xml +++ b/F-Droid/res/layout/repodetails.xml @@ -1,6 +1,7 @@ - - + android:textStyle="bold" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" /> + + + + + + + \ No newline at end of file diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java index b895f6a14..5dc2ba9b1 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java @@ -74,7 +74,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; @@ -888,7 +887,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..4c87a4ff5 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -1,6 +1,8 @@ package org.fdroid.fdroid.views; import android.annotation.TargetApi; +import android.content.ContentValues; +import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.nfc.NdefMessage; @@ -8,26 +10,76 @@ 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.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.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; -import org.fdroid.fdroid.views.fragments.RepoDetailsFragment; 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 +87,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 +99,18 @@ 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(); + } + }); } @TargetApi(14) @@ -82,6 +128,16 @@ 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(); + // FDroid.java and AppDetails set different NFC actions, so reset here setNfc(); processIntent(getIntent()); @@ -112,14 +168,177 @@ public class RepoDetailsActivity extends ActionBarActivity { } } + @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).setListener(new ProgressListener() { + @Override + public void onProgress(Event event) { + switch (event.type) { + case UpdateService.EVENT_COMPLETE_WITH_CHANGES: + updateRepoView(); + break; + } + } + }); + } + + 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); - } - -} From 46978e1086d33bb7fb45e8260171a996cf0d940e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 18 Jun 2015 14:35:13 -0400 Subject: [PATCH 03/18] make RepoDetails scrollable and add a QR code to the bottom Having a QR Code of the repo makes it easy for people to share the repo to someone else. --- F-Droid/res/layout/repodetails.xml | 30 ++++++++++++++----- .../fdroid/views/RepoDetailsActivity.java | 8 +++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/F-Droid/res/layout/repodetails.xml b/F-Droid/res/layout/repodetails.xml index 7a7f1f9ef..921612d11 100644 --- a/F-Droid/res/layout/repodetails.xml +++ b/F-Droid/res/layout/repodetails.xml @@ -1,18 +1,24 @@ - + + + + - + + + + + diff --git a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 4c87a4ff5..8f7cd4216 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -26,12 +26,15 @@ import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NfcHelper; import org.fdroid.fdroid.NfcNotEnabledActivity; import org.fdroid.fdroid.ProgressListener; +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 java.util.Locale; + public class RepoDetailsActivity extends ActionBarActivity { private static final String TAG = "RepoDetailsActivity"; @@ -111,6 +114,11 @@ public class RepoDetailsActivity extends ActionBarActivity { 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) From 1d263b2aee7d21c395fa5b6a0686f23ec93ac768 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 18 Jun 2015 15:20:54 -0400 Subject: [PATCH 04/18] fix sending repo via NFC This was my stupid mistake: using a String for a class, instead of a .class so that when ManageRepo was renamed to ManageRepoActivity, this broke. --- F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 8f7cd4216..6153492ca 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -169,8 +169,7 @@ 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(); } From 4fd914ac7a9902620ec94a8c7858459234164ef2 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 19 Jun 2015 14:34:16 -0400 Subject: [PATCH 05/18] convert repo update dialog to a notification Since the repo updates are happening in an IntentService, they are already running in a separate thread. Ironically, the dialog was showing in spite of that. This removes the dialog entirely and instead puts up a Notification with the same messages. Ultimately, the "refresh" button should go away, the repos should be updated whenever someone goes to install an app, and all APK downloads should also show up in the same Notification. This removes UpdateReceiver entirely and replaces it with local broadcasts, since that is a common pattern in FDroid and Android. It also reduces the amount of code here. refs #103 https://gitlab.com/fdroid/fdroidclient/issues/103 --- F-Droid/res/values/strings.xml | 1 + .../src/org/fdroid/fdroid/UpdateService.java | 290 ++++++++---------- .../fdroid/views/ManageReposActivity.java | 48 ++- .../fdroid/views/RepoDetailsActivity.java | 34 +- .../views/swap/ConnectSwapActivity.java | 65 ++-- 5 files changed, 203 insertions(+), 235 deletions(-) diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml index 8fae272f7..293f1ace8 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… diff --git a/F-Droid/src/org/fdroid/fdroid/UpdateService.java b/F-Droid/src/org/fdroid/fdroid/UpdateService.java index ec63a7d49..2aa6f2b63 100644 --- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java +++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java @@ -20,13 +20,15 @@ package org.fdroid.fdroid; import android.app.AlarmManager; import android.app.IntentService; +import android.app.Notification; 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; @@ -35,13 +37,12 @@ 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 +64,26 @@ 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 static final int NOTIFY_ID_UPDATING = 0; + private static final int NOTIFY_ID_UPDATES_AVAILABLE = 1; - public static final String EXTRA_RECEIVER = "receiver"; - public static final String EXTRA_ADDRESS = "address"; - - 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,109 @@ public class UpdateService extends IntentService implements ProgressListener { } + @Override + public void onCreate() { + super.onCreate(); + + LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); + lbm.registerReceiver(localBroadcastReceiver, 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); + } + 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); + 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; - } + // For receiving results from the UpdateService when we've told it to + // update in response to a user request. + private BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action == null) + 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(Notification.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(Notification.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 +289,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 +416,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 +508,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) { diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java index 5dc2ba9b1..fed38e4ef 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java @@ -19,10 +19,12 @@ package org.fdroid.fdroid.views; +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.SharedPreferences; import android.content.res.ColorStateList; import android.database.Cursor; @@ -40,6 +42,7 @@ import android.support.v4.app.LoaderManager; import android.support.v4.app.NavUtils; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; +import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.ActionBarActivity; import android.support.v7.app.AlertDialog; import android.text.Editable; @@ -64,7 +67,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; @@ -103,8 +105,6 @@ public class ManageReposActivity extends ActionBarActivity { IS_SWAP } - private UpdateService.UpdateReceiver updateHandler = null; - private static boolean changed = false; private RepoListFragment listFragment; @@ -151,9 +151,10 @@ public class ManageReposActivity extends ActionBarActivity { @Override protected void onResume() { super.onResume(); - if (updateHandler != null) { - updateHandler.showDialog(this); - } + + LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, + new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); + /* let's see if someone is trying to send us a new repo */ addRepoFromIntent(getIntent()); } @@ -161,9 +162,7 @@ public class ManageReposActivity extends ActionBarActivity { @Override protected void onPause() { super.onPause(); - if (updateHandler != null) { - updateHandler.hideDialog(); - } + LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); } @Override @@ -209,7 +208,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(); @@ -218,25 +217,16 @@ 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; - } - } - }); - } + 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_AND_SAME + || statusCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES) + // No need to prompt to update any more, we just did it! + changed = false; + } + }; private void scanForRepos() { final RepoScanListAdapter adapter = new RepoScanListAdapter(this); diff --git a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index 6153492ca..c49325c45 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -1,9 +1,12 @@ 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; @@ -12,6 +15,7 @@ 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; @@ -25,7 +29,6 @@ import android.widget.Toast; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NfcHelper; import org.fdroid.fdroid.NfcNotEnabledActivity; -import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.QrGenAsyncTask; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; @@ -146,6 +149,9 @@ public class RepoDetailsActivity extends ActionBarActivity { 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()); @@ -175,6 +181,21 @@ public class RepoDetailsActivity extends ActionBarActivity { } } + 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); @@ -316,16 +337,7 @@ public class RepoDetailsActivity extends ActionBarActivity { values.put(RepoProvider.DataColumns.IN_USE, 1); RepoProvider.Helper.update(this, repo, values); - UpdateService.updateRepoNow(repo.address, this).setListener(new ProgressListener() { - @Override - public void onProgress(Event event) { - switch (event.type) { - case UpdateService.EVENT_COMPLETE_WITH_CHANGES: - updateRepoView(); - break; - } - } - }); + UpdateService.updateRepoNow(repo.address, this); } private void promptForDelete() { 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); } } From 80063eb7861c321024200ad7f53b81485f27986d Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 19 Jun 2015 15:10:50 -0400 Subject: [PATCH 06/18] simplify Downloader constructors Since all Downloaders are created via the DownloaderFactory, put the temp file creation there, then we can remove lots of constructors. --- .../src/org/fdroid/fdroid/net/Downloader.java | 18 ------------------ .../fdroid/fdroid/net/DownloaderFactory.java | 9 +++++++-- .../org/fdroid/fdroid/net/HttpDownloader.java | 11 ----------- .../fdroid/fdroid/net/TorHttpDownloader.java | 6 ------ 4 files changed, 7 insertions(+), 37 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java index cf9dfa170..7daea82c4 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java @@ -2,7 +2,6 @@ package org.fdroid.fdroid.net; import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; import android.util.Log; import org.fdroid.fdroid.ProgressListener; @@ -33,29 +32,12 @@ public abstract class Downloader { 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) throws FileNotFoundException, MalformedURLException { outputFile = destFile; outputStream = new FileOutputStream(outputFile); } - Downloader(OutputStream output) - throws MalformedURLException { - outputStream = output; - outputFile = null; - } - public void setProgressListener(ProgressListener listener) { setProgressListener(listener, null); } diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java index a6cae1438..bf2223b4b 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java @@ -7,12 +7,17 @@ import java.io.IOException; public class DownloaderFactory { + /** + * Downloads to a temporary file, which *you must delete yourself when + * you are done + */ public static Downloader create(String url, Context context) throws IOException { + File destFile = File.createTempFile("dl-", "", context.getCacheDir()); if (isOnionAddress(url)) { - return new TorHttpDownloader(url, context); + return new TorHttpDownloader(url, destFile); } - return new HttpDownloader(url, context); + return new HttpDownloader(url, destFile); } public static Downloader create(String url, File destFile) diff --git a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java index c1a6a3650..1ed22174f 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java @@ -1,6 +1,5 @@ package org.fdroid.fdroid.net; -import android.content.Context; import android.util.Log; import org.fdroid.fdroid.Preferences; @@ -34,16 +33,6 @@ public class HttpDownloader extends Downloader { 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); - } - @Override public InputStream getInputStream() throws IOException { setupConnection(); diff --git a/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java index 70848ac66..fc7cd08a8 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java @@ -1,7 +1,5 @@ package org.fdroid.fdroid.net; -import android.content.Context; - import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -13,10 +11,6 @@ import java.net.SocketAddress; public class TorHttpDownloader extends HttpDownloader { - TorHttpDownloader(String url, Context ctx) throws IOException { - super(url, ctx); - } - TorHttpDownloader(String url, File destFile) throws FileNotFoundException, MalformedURLException { super(url, destFile); From 9a9bf921262c805e0de460e143520ccdb791f2f3 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 19 Jun 2015 15:18:00 -0400 Subject: [PATCH 07/18] make all Downloaders get a Context so the base class can use those tricks Having a Context in Downloader means that the communications can be changed to a LocalBroadcastManager, following the pattern that is in a lot of this app already. --- F-Droid/src/org/fdroid/fdroid/RepoUpdater.java | 2 +- F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java | 4 +++- F-Droid/src/org/fdroid/fdroid/net/Downloader.java | 4 +++- .../src/org/fdroid/fdroid/net/DownloaderFactory.java | 12 ++++++------ .../src/org/fdroid/fdroid/net/HttpDownloader.java | 6 +++--- .../src/org/fdroid/fdroid/net/IconDownloader.java | 2 +- .../src/org/fdroid/fdroid/net/TorHttpDownloader.java | 6 ++++-- 7 files changed, 21 insertions(+), 15 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java index f3cff0b8b..b67915f73 100644 --- a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java +++ b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java @@ -84,7 +84,7 @@ public class RepoUpdater { 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); diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index 185f55bb1..630a913c7 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -63,6 +63,7 @@ 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; @@ -84,6 +85,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 +193,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; diff --git a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java index 7daea82c4..751f51b96 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java @@ -23,6 +23,7 @@ public abstract class Downloader { private ProgressListener progressListener = null; private Bundle eventData = null; + private final Context context; private final File outputFile; protected URL sourceUrl; @@ -32,8 +33,9 @@ public abstract class Downloader { public abstract InputStream getInputStream() throws IOException; - Downloader(File destFile) + Downloader(Context context, File destFile) throws FileNotFoundException, MalformedURLException { + this.context = context; outputFile = destFile; outputStream = new FileOutputStream(outputFile); } diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java index bf2223b4b..765723b4b 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java @@ -11,21 +11,21 @@ public class DownloaderFactory { * Downloads to a temporary file, which *you must delete yourself when * you are done */ - public static Downloader create(String url, Context context) + public static Downloader create(Context context, String url) throws IOException { File destFile = File.createTempFile("dl-", "", context.getCacheDir()); if (isOnionAddress(url)) { - return new TorHttpDownloader(url, destFile); + return new TorHttpDownloader(context, url, destFile); } - return new HttpDownloader(url, destFile); + return new HttpDownloader(context, url, destFile); } - public static Downloader create(String url, File destFile) + public static Downloader create(Context context, String url, File destFile) throws IOException { if (isOnionAddress(url)) { - return new TorHttpDownloader(url, destFile); + return new TorHttpDownloader(context, url, destFile); } - return new HttpDownloader(url, destFile); + return new HttpDownloader(context, url, destFile); } private static boolean isOnionAddress(String url) { diff --git a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java index 1ed22174f..3a93de690 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.net; +import android.content.Context; import android.util.Log; import org.fdroid.fdroid.Preferences; @@ -26,10 +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, String source, File destFile) throws FileNotFoundException, MalformedURLException { - super(destFile); + super(context, destFile); sourceUrl = new URL(source); } 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 fc7cd08a8..8a781a18d 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java @@ -1,5 +1,7 @@ package org.fdroid.fdroid.net; +import android.content.Context; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -11,9 +13,9 @@ import java.net.SocketAddress; public class TorHttpDownloader extends HttpDownloader { - TorHttpDownloader(String url, File destFile) + TorHttpDownloader(Context context, String url, File destFile) throws FileNotFoundException, MalformedURLException { - super(url, destFile); + super(context, url, destFile); } @Override From 5d4bdf61391b6252f4f367a1b8a5e80330180273 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 19 Jun 2015 16:16:06 -0400 Subject: [PATCH 08/18] all Downloaders have a URL, so make the base class store it for use This lets the local broadcasts include the URL as an extra, and other tricks. --- .../src/org/fdroid/fdroid/RepoUpdater.java | 9 +++-- .../src/org/fdroid/fdroid/net/Downloader.java | 3 +- .../fdroid/fdroid/net/DownloaderFactory.java | 33 ++++++++++++++----- .../org/fdroid/fdroid/net/HttpDownloader.java | 5 ++- .../fdroid/fdroid/net/TorHttpDownloader.java | 3 +- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java index b67915f73..a38c7fa1d 100644 --- a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java +++ b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java @@ -22,6 +22,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; @@ -71,14 +73,15 @@ 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 { diff --git a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java index 751f51b96..4f8f8bc2c 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java @@ -33,9 +33,10 @@ public abstract class Downloader { public abstract InputStream getInputStream() throws IOException; - Downloader(Context context, File destFile) + Downloader(Context context, URL url, File destFile) throws FileNotFoundException, MalformedURLException { this.context = context; + this.sourceUrl = url; outputFile = destFile; outputStream = new FileOutputStream(outputFile); } diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java index 765723b4b..fdc7b3255 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java @@ -4,23 +4,38 @@ import android.content.Context; import java.io.File; import java.io.IOException; +import java.net.URL; public class DownloaderFactory { /** * Downloads to a temporary file, which *you must delete yourself when - * you are done + * you are done. It is stored in {@link Context#getCacheDir()} and starts + * with the prefix {@code dl-}. */ - public static Downloader create(Context context, String url) + public static Downloader create(Context context, String urlString) + throws IOException { + return create(context, new URL(urlString)); + } + + /** + * 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 { File destFile = File.createTempFile("dl-", "", context.getCacheDir()); - if (isOnionAddress(url)) { - return new TorHttpDownloader(context, url, destFile); - } - return new HttpDownloader(context, url, destFile); + destFile.deleteOnExit(); // this probably does nothing, but maybe... + return create(context, url, destFile); } - public static Downloader create(Context context, String url, File destFile) + 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); @@ -28,7 +43,7 @@ public class DownloaderFactory { return new HttpDownloader(context, url, destFile); } - private static boolean isOnionAddress(String url) { - return url.matches("^[a-zA-Z0-9]+://[^/]+\\.onion/.*"); + 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 3a93de690..d134c95d0 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java @@ -27,10 +27,9 @@ public class HttpDownloader extends Downloader { protected HttpURLConnection connection; private int statusCode = -1; - HttpDownloader(Context context, String source, File destFile) + HttpDownloader(Context context, URL url, File destFile) throws FileNotFoundException, MalformedURLException { - super(context, destFile); - sourceUrl = new URL(source); + super(context, url, destFile); } @Override diff --git a/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java index 8a781a18d..c0eaf4d45 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/TorHttpDownloader.java @@ -10,10 +10,11 @@ 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(Context context, String url, File destFile) + TorHttpDownloader(Context context, URL url, File destFile) throws FileNotFoundException, MalformedURLException { super(context, url, destFile); } From cd0b5b80f938c6646266518f2a16c3f93545e5c7 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 19 Jun 2015 16:24:42 -0400 Subject: [PATCH 09/18] purge unused methods from ApkDownloader and AsyncDownloadWrapper --- .../org/fdroid/fdroid/net/ApkDownloader.java | 12 -------- .../fdroid/net/AsyncDownloadWrapper.java | 28 ------------------- 2 files changed, 40 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index 630a913c7..d71abd5df 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -232,18 +232,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); diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java index d113f93f3..8abf8b4ed 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java @@ -43,16 +43,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 +54,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} @@ -105,8 +79,6 @@ public class AsyncDownloadWrapper extends Handler { } public interface Listener extends ProgressListener { - void onReceiveTotalDownloadSize(int size); - void onReceiveCacheTag(String cacheTag); void onErrorDownloading(String localisedExceptionDetails); void onDownloadComplete(); void onDownloadCancelled(); From e984ed82ef370fdab4d3530cee4509d3b83b627d Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 19 Jun 2015 17:12:35 -0400 Subject: [PATCH 10/18] run index update in the background after adding a new repo That makes that repo automatically ready for use based on user actions like adding a new repo, switching an existing repo on, etc. This also lowers the priority of the "update" menu item since it shouldn't be needed any more. But leave it for now, just in case. --- F-Droid/res/menu/main.xml | 10 ++-- F-Droid/src/org/fdroid/fdroid/FDroid.java | 38 +------------- .../fdroid/views/ManageReposActivity.java | 49 +------------------ 3 files changed, 9 insertions(+), 88 deletions(-) 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"/> - + Date: Fri, 19 Jun 2015 18:03:37 -0400 Subject: [PATCH 11/18] replace Downloader's ProgressListener with local broadcasts This removes lots of boiler plate, makes it much easier to get the info where it is needed, and puts the code in line with rest of FDroid. The ProgressListener pattern was forcing a lot of passing the listener instances around through classes that never used the listener even. --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 21 +++++++-- .../src/org/fdroid/fdroid/RepoUpdater.java | 8 ---- .../src/org/fdroid/fdroid/UpdateService.java | 42 +++++++++++++---- .../org/fdroid/fdroid/net/ApkDownloader.java | 8 ---- .../fdroid/net/AsyncDownloadWrapper.java | 20 +-------- .../src/org/fdroid/fdroid/net/Downloader.java | 45 +++++++++---------- 6 files changed, 73 insertions(+), 71 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index 76d17a2a0..e60843a32 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -24,10 +24,12 @@ 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; @@ -40,6 +42,7 @@ import android.os.Handler; 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; @@ -316,6 +319,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; @@ -425,6 +429,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A // progress indicator after returning from that prompt). setSupportProgressBarIndeterminateVisibility(false); + localBroadcastManager = LocalBroadcastManager.getInstance(this); } // The signature of the installed version. @@ -449,6 +454,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A if (downloadHandler.isComplete()) { downloadCompleteInstallApk(); } else { + localBroadcastManager.registerReceiver(downloaderProgressReceiver, + new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS)); downloadHandler.setProgressListener(this); // Show the progress dialog, if for no other reason than to prevent them attempting @@ -504,6 +511,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate); } + localBroadcastManager.unregisterReceiver(downloaderProgressReceiver); if (downloadHandler != null) { downloadHandler.removeProgressListener(); } @@ -511,6 +519,14 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A removeProgressDialog(); } + private final BroadcastReceiver downloaderProgressReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateProgressDialog(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1), + intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1)); + } + }; + private void onAppChanged() { if (!reset(app.id)) { AppDetails.this.finish(); @@ -854,6 +870,8 @@ 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(); @@ -1034,9 +1052,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) diff --git a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java index a38c7fa1d..e3dabf236 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; @@ -90,13 +89,6 @@ public class RepoUpdater { 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 2aa6f2b63..2b17cbcfd 100644 --- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java +++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java @@ -36,7 +36,6 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; -import android.os.Bundle; import android.os.RemoteException; import android.os.SystemClock; import android.preference.PreferenceManager; @@ -79,6 +78,8 @@ public class UpdateService extends IntentService implements ProgressListener { public static final int STATUS_ERROR_LOCAL_SMALL = 4; public static final int STATUS_INFO = 5; + private LocalBroadcastManager localBroadcastManager; + private static final int NOTIFY_ID_UPDATING = 0; private static final int NOTIFY_ID_UPDATES_AVAILABLE = 1; @@ -147,8 +148,11 @@ public class UpdateService extends IntentService implements ProgressListener { public void onCreate() { super.onCreate(); - LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); - lbm.registerReceiver(localBroadcastReceiver, new IntentFilter(LOCAL_ACTION_STATUS)); + 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); @@ -164,6 +168,8 @@ public class UpdateService extends IntentService implements ProgressListener { public void onDestroy() { super.onDestroy(); notificationManager.cancel(NOTIFY_ID_UPDATING); + localBroadcastManager.unregisterReceiver(downloadProgressReceiver); + localBroadcastManager.unregisterReceiver(updateStatusReceiver); } protected void sendStatus(int statusCode) { @@ -185,14 +191,35 @@ public class UpdateService extends IntentService implements ProgressListener { LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } - // For receiving results from the UpdateService when we've told it to + 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 BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() { + private final BroadcastReceiver updateStatusReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (action == null) + if (TextUtils.isEmpty(action)) return; if (!action.equals(LOCAL_ACTION_STATUS)) @@ -733,9 +760,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 d71abd5df..cb4106e7b 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -216,14 +216,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); diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java index 8abf8b4ed..dc7ac055c 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; @@ -61,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; @@ -84,11 +78,10 @@ public class AsyncDownloadWrapper extends Handler { 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) { @@ -109,16 +102,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 4f8f8bc2c..fa064a3d0 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java @@ -1,10 +1,10 @@ package org.fdroid.fdroid.net; import android.content.Context; -import android.os.Bundle; +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; @@ -19,35 +19,29 @@ import java.net.URL; public abstract class Downloader { private static final String TAG = "Downloader"; + + 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 OutputStream outputStream; - private ProgressListener progressListener = null; - private Bundle eventData = null; - private final Context context; + private LocalBroadcastManager localBroadcastManager; private final File outputFile; protected URL sourceUrl; protected String cacheTag = null; - public static final String EVENT_PROGRESS = "downloadProgress"; - public abstract InputStream getInputStream() throws IOException; Downloader(Context context, URL url, File destFile) throws FileNotFoundException, MalformedURLException { - this.context = context; this.sourceUrl = url; outputFile = destFile; outputStream = new FileOutputStream(outputFile); - } - - 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); } /** @@ -142,6 +136,11 @@ 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]; @@ -170,13 +169,11 @@ public abstract class Downloader { } protected void sendProgress(int bytesRead, int totalBytes) { - sendProgress(new ProgressListener.Event(EVENT_PROGRESS, bytesRead, totalBytes, eventData)); - } - - protected void sendProgress(ProgressListener.Event event) { - if (progressListener != null) { - progressListener.onProgress(event); - } + 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); } } From 5582e906c83d31923b6ce1c721abae748c76b222 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 19 Jun 2015 23:02:35 -0400 Subject: [PATCH 12/18] disable Install/Run/Uninstall button when the install process is running To make things less confusing, this disables the main button on AppDetails when something is running. During install, it also changes the text of the button to "Installing..." --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index e60843a32..5da4473d0 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -326,6 +326,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A private final Context mctx = this; private Installer installer; + private static Button mainButton; /** * Stores relevant data that we want to keep track of when destroying the activity @@ -987,6 +988,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } else { Log.e(TAG, "Tried to cancel, but the downloadHandler doesn't exist."); } + if (mainButton != null) + mainButton.setEnabled(true); progressDialog = null; Toast.makeText(AppDetails.this, getString(R.string.download_cancelled), Toast.LENGTH_LONG).show(); } @@ -1487,6 +1490,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.app_details_header, container, false); + mainButton = (Button) view.findViewById(R.id.btn_main); setupView(view); return view; } @@ -1523,8 +1527,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A 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); + mainButton.setVisibility(View.VISIBLE); + mainButton.setEnabled(true); /* Check count > 0 due to incompatible apps resulting in an empty list. @@ -1535,8 +1539,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A 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); + mainButton.setText(R.string.menu_install); + mainButton.setOnClickListener(mOnClickListener); } // If App is installed else if (getApp().isInstalled()) { @@ -1545,24 +1549,24 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A NfcHelper.setAndroidBeam(getActivity(), getApp().id); if (getApp().canAndWantToUpdate()) { updateWanted = true; - btMain.setText(R.string.menu_upgrade); + mainButton.setText(R.string.menu_upgrade); }else { updateWanted = false; if (((AppDetails)getActivity()).mPm.getLaunchIntentForPackage(getApp().id) != null){ - btMain.setText(R.string.menu_launch); + mainButton.setText(R.string.menu_launch); } else { - btMain.setText(R.string.menu_uninstall); + mainButton.setText(R.string.menu_uninstall); } } - btMain.setOnClickListener(mOnClickListener); + mainButton.setOnClickListener(mOnClickListener); } TextView currentVersion = (TextView) view.findViewById(R.id.current_version); if (!getApks().isEmpty()) { currentVersion.setText(getApks().getItem(0).version); }else { currentVersion.setVisibility(View.GONE); - btMain.setVisibility(View.GONE); + mainButton.setVisibility(View.GONE); } } @@ -1589,6 +1593,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A // If not installed, install else if (getApp().suggestedVercode > 0) { + mainButton.setEnabled(false); + mainButton.setText(R.string.system_install_installing); final Apk apkToInstall = ApkProvider.Helper.find(getActivity(), getApp().id, getApp().suggestedVercode); ((AppDetails)getActivity()).install(apkToInstall); } From 60f02c0181c333b3735cfe5b75ffe3823212a3c2 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 16 Jul 2015 23:02:11 -0700 Subject: [PATCH 13/18] in Downloader, instance vars that are set in constructor should be final --- F-Droid/src/org/fdroid/fdroid/net/Downloader.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java index fa064a3d0..000e26576 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java @@ -26,12 +26,12 @@ public abstract class Downloader { public static final String EXTRA_BYTES_READ = "extraBytesRead"; public static final String EXTRA_TOTAL_BYTES = "extraTotalBytes"; - private OutputStream outputStream; + private final OutputStream outputStream; - private LocalBroadcastManager localBroadcastManager; + private final LocalBroadcastManager localBroadcastManager; private final File outputFile; - protected URL sourceUrl; + protected final URL sourceUrl; protected String cacheTag = null; public abstract InputStream getInputStream() throws IOException; From b8261fcecf09edb2bd8cb0b7bd40c2ceae964a5a Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Sat, 1 Aug 2015 21:32:38 +0200 Subject: [PATCH 14/18] EXTRA_REPO_ERRORS in Intents should be a CharSequence[], not an ArrayList This was including the instance of the ArrayList in the Intent, rather than the expected CharSequence[]. --- F-Droid/src/org/fdroid/fdroid/UpdateService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/F-Droid/src/org/fdroid/fdroid/UpdateService.java b/F-Droid/src/org/fdroid/fdroid/UpdateService.java index 2b17cbcfd..ea1c8341a 100644 --- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java +++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java @@ -187,7 +187,7 @@ public class UpdateService extends IntentService implements ProgressListener { protected void sendRepoErrorStatus(int statusCode, ArrayList repoErrors) { Intent intent = new Intent(LOCAL_ACTION_STATUS); intent.putExtra(EXTRA_STATUS_CODE, statusCode); - intent.putExtra(EXTRA_REPO_ERRORS, repoErrors); + intent.putExtra(EXTRA_REPO_ERRORS, repoErrors.toArray(new CharSequence[repoErrors.size()])); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } From a0bf49d1ea8808a48c511bebbba784f7824ac918 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Mon, 3 Aug 2015 22:37:00 +1000 Subject: [PATCH 15/18] Reinstate code to allow resumed activity to query download status (rather than wait for broadcasts). --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 5 ++--- F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java | 7 ++----- .../org/fdroid/fdroid/net/AsyncDownloadWrapper.java | 8 ++++++++ F-Droid/src/org/fdroid/fdroid/net/Downloader.java | 12 +++++++++++- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index ecf187e8b..984e5420a 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -464,11 +464,10 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS)); downloadHandler.setProgressListener(this); - if (downloadHandler.getTotalSize() == 0) + if (downloadHandler.getTotalBytes() == 0) mHeaderFragment.startProgress(); else - mHeaderFragment.updateProgress(downloadHandler.getProgress(), - downloadHandler.getTotalSize()); + mHeaderFragment.updateProgress(downloadHandler.getBytesRead(), downloadHandler.getTotalBytes()); } } } diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index cb4106e7b..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; @@ -70,8 +69,6 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { private ProgressListener listener; private AsyncDownloadWrapper dlWrapper = null; - private int progress = 0; - private int totalSize = 0; private boolean isComplete = false; private final long id = ++downloadIdCounter; @@ -274,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 dc7ac055c..3588343b8 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java @@ -72,6 +72,14 @@ public class AsyncDownloadWrapper extends Handler { } } + public int getBytesRead() { + return downloader.getBytesRead(); + } + + public int getTotalBytes() { + return downloader.getTotalBytes(); + } + public interface Listener extends ProgressListener { void onErrorDownloading(String localisedExceptionDetails); void onDownloadComplete(); diff --git a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java index 000e26576..4802f39f2 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/Downloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/Downloader.java @@ -33,6 +33,8 @@ public abstract class Downloader { protected final URL sourceUrl; protected String cacheTag = null; + protected int bytesRead = 0; + protected int totalBytes = 0; public abstract InputStream getInputStream() throws IOException; @@ -145,7 +147,7 @@ public abstract class Downloader { 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. @@ -169,6 +171,7 @@ public abstract class Downloader { } protected void sendProgress(int bytesRead, int totalBytes) { + this.bytesRead = bytesRead; Intent intent = new Intent(LOCAL_ACTION_PROGRESS); intent.putExtra(EXTRA_ADDRESS, sourceUrl.toString()); intent.putExtra(EXTRA_BYTES_READ, bytesRead); @@ -176,4 +179,11 @@ public abstract class Downloader { localBroadcastManager.sendBroadcast(intent); } + public int getBytesRead() { + return bytesRead; + } + + public int getTotalBytes() { + return totalBytes; + } } From f3a9581d89e1ce22e961f4b2cfc4d8bea8d13931 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Mon, 3 Aug 2015 17:19:49 +1000 Subject: [PATCH 16/18] Fix warnings in app details (remove progress thingo). AppCompat no longer supports progress indicators in the action bar. So this is not your everyday "Deprecated, but sure, keep using it" job. Rather, it is "deprecated, and no, we wont even let you use it." Also removed unused argument and extended AppCompatActivity. --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index 984e5420a..6500551b0 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -37,13 +37,14 @@ import android.graphics.Bitmap; import android.net.Uri; 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; @@ -117,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"; @@ -425,13 +426,6 @@ 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); } @@ -489,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(); } } @@ -732,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) { @@ -873,26 +867,20 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A } } - 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); } } @@ -907,7 +895,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A PackageManagerCompat.setInstaller(mPm, app.id); } - setSupportProgressBarIndeterminateVisibility(false); onAppChanged(); } }); @@ -919,7 +906,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A runOnUiThread(new Runnable() { @Override public void run() { - setSupportProgressBarIndeterminateVisibility(false); onAppChanged(); } }); @@ -927,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); From 215dcc0dd96e25c502ad454b03af0768b11233d0 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Mon, 3 Aug 2015 17:21:54 +1000 Subject: [PATCH 17/18] Don't use `Notification` class, but rather `NotificationCompat`. --- F-Droid/src/org/fdroid/fdroid/UpdateService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/UpdateService.java b/F-Droid/src/org/fdroid/fdroid/UpdateService.java index ea1c8341a..3e75ff7db 100644 --- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java +++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java @@ -20,7 +20,6 @@ package org.fdroid.fdroid; import android.app.AlarmManager; import android.app.IntentService; -import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; @@ -239,7 +238,7 @@ public class UpdateService extends IntentService implements ProgressListener { case STATUS_ERROR_GLOBAL: text = context.getString(R.string.global_error_updating_repos) + " " + message; notificationBuilder.setContentText(text) - .setCategory(Notification.CATEGORY_ERROR) + .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(); @@ -257,7 +256,7 @@ public class UpdateService extends IntentService implements ProgressListener { } text = msgBuilder.toString(); notificationBuilder.setContentText(text) - .setCategory(Notification.CATEGORY_ERROR) + .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(); From cbd8c977e2b0c2561c5dd8f3e539fa8a7aaad438 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Mon, 3 Aug 2015 17:27:00 +1000 Subject: [PATCH 18/18] Use annotations to remove lint warnings. Also removed undocumented java doc. --- F-Droid/src/org/fdroid/fdroid/RepoUpdater.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java index e3dabf236..936d2e963 100644 --- a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java +++ b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java @@ -53,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; }