From 8c251340315410d9f99b1a681d308f62f317cf11 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 10 Jul 2015 02:51:43 +0200 Subject: [PATCH] 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