diff --git a/F-Droid/res/drawable-hdpi/ic_clear.png b/F-Droid/res/drawable-hdpi/ic_clear.png
new file mode 100644
index 000000000..1a9cd75a0
Binary files /dev/null and b/F-Droid/res/drawable-hdpi/ic_clear.png differ
diff --git a/F-Droid/res/drawable-mdpi/ic_clear.png b/F-Droid/res/drawable-mdpi/ic_clear.png
new file mode 100644
index 000000000..40a1a84e3
Binary files /dev/null and b/F-Droid/res/drawable-mdpi/ic_clear.png differ
diff --git a/F-Droid/res/drawable-xhdpi/ic_clear.png b/F-Droid/res/drawable-xhdpi/ic_clear.png
new file mode 100644
index 000000000..6bc437298
Binary files /dev/null and b/F-Droid/res/drawable-xhdpi/ic_clear.png differ
diff --git a/F-Droid/res/drawable-xxhdpi/ic_clear.png b/F-Droid/res/drawable-xxhdpi/ic_clear.png
new file mode 100644
index 000000000..51b4401ca
Binary files /dev/null and b/F-Droid/res/drawable-xxhdpi/ic_clear.png differ
diff --git a/F-Droid/res/drawable-xxxhdpi/ic_clear.png b/F-Droid/res/drawable-xxxhdpi/ic_clear.png
new file mode 100644
index 000000000..df42feecb
Binary files /dev/null and b/F-Droid/res/drawable-xxxhdpi/ic_clear.png differ
diff --git a/F-Droid/res/layout/app_details_header.xml b/F-Droid/res/layout/app_details_header.xml
index e36319c54..99f2cb936 100644
--- a/F-Droid/res/layout/app_details_header.xml
+++ b/F-Droid/res/layout/app_details_header.xml
@@ -16,7 +16,7 @@
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-->
-
@@ -109,7 +110,50 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml
index 293f1ace8..770a1b222 100644
--- a/F-Droid/res/values/strings.xml
+++ b/F-Droid/res/values/strings.xml
@@ -373,4 +373,13 @@
NEW:
Provided by %1$s.
+ Downloading...
+
+
+ - B
+ - KiB
+ - MiB
+ - GiB
+ - TiB
+
diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java
index 5da4473d0..ecf187e8b 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.BroadcastReceiver;
@@ -36,7 +35,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;
@@ -66,9 +64,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;
@@ -93,6 +93,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;
@@ -127,7 +128,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
private FDroidApp fdroidApp;
private ApkListAdapter adapter;
- private ProgressDialog progressDialog;
private static class ViewHolder {
TextView version;
@@ -326,13 +326,15 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
private final Context mctx = this;
private Installer installer;
- private static Button mainButton;
+
+
+ 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 {
@@ -449,8 +451,11 @@ 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();
@@ -459,30 +464,22 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
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;
}
}
@@ -517,13 +514,14 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
downloadHandler.removeProgressListener();
}
- removeProgressDialog();
+ mHeaderFragment.removeProgress();
}
private final BroadcastReceiver downloaderProgressReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- updateProgressDialog(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1),
+ if (mHeaderFragment != null)
+ mHeaderFragment.updateProgress(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1),
intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1));
}
};
@@ -570,13 +568,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.
@@ -636,9 +627,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
@@ -822,6 +813,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) {
@@ -875,7 +870,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
downloadHandler.setProgressListener(this);
if (downloadHandler.download()) {
- updateProgressDialog();
+ mHeaderFragment.startProgress();
}
}
@@ -964,81 +959,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.");
- }
- if (mainButton != null)
- mainButton.setEnabled(true);
- 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)) {
@@ -1072,7 +992,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
}
if (finished) {
- removeProgressDialog();
+ if (mHeaderFragment != null)
+ mHeaderFragment.removeProgress();
downloadHandler = null;
}
}
@@ -1465,9 +1386,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;
@@ -1490,7 +1416,6 @@ 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;
}
@@ -1512,35 +1437,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);
- mainButton.setVisibility(View.VISIBLE);
- mainButton.setEnabled(true);
+ 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
- mainButton.setText(R.string.menu_install);
- mainButton.setOnClickListener(mOnClickListener);
+ btMain.setText(R.string.menu_install);
+ btMain.setOnClickListener(mOnClickListener);
+ btMain.setEnabled(true);
}
// If App is installed
else if (getApp().isInstalled()) {
@@ -1549,24 +1559,25 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
NfcHelper.setAndroidBeam(getActivity(), getApp().id);
if (getApp().canAndWantToUpdate()) {
updateWanted = true;
- mainButton.setText(R.string.menu_upgrade);
+ btMain.setText(R.string.menu_upgrade);
}else {
updateWanted = false;
if (((AppDetails)getActivity()).mPm.getLaunchIntentForPackage(getApp().id) != null){
- mainButton.setText(R.string.menu_launch);
+ btMain.setText(R.string.menu_launch);
}
else {
- mainButton.setText(R.string.menu_uninstall);
+ btMain.setText(R.string.menu_uninstall);
}
}
- mainButton.setOnClickListener(mOnClickListener);
+ btMain.setOnClickListener(mOnClickListener);
+ btMain.setEnabled(true);
}
TextView currentVersion = (TextView) view.findViewById(R.id.current_version);
if (!getApks().isEmpty()) {
currentVersion.setText(getApks().getItem(0).version);
}else {
currentVersion.setVisibility(View.GONE);
- mainButton.setVisibility(View.GONE);
+ btMain.setVisibility(View.GONE);
}
}
@@ -1593,8 +1604,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);
+ btMain.setEnabled(false);
+ btMain.setText(R.string.system_install_installing);
final Apk apkToInstall = ApkProvider.Helper.find(getActivity(), getApp().id, getApp().suggestedVercode);
((AppDetails)getActivity()).install(apkToInstall);
}
diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
index dd1c289bc..20ae144bd 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
@@ -650,7 +650,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() {
@@ -727,7 +727,7 @@ public class ManageReposActivity extends ActionBarActivity {
/**
* NOTE: If somebody toggles a repo off then on again, it will have
* removed all apps from the index when it was toggled off, so when it
- * is toggled on again, then it will require a refresh. Previously, I
+ * is toggled on again, then it will require a updateViews. Previously, I
* toyed with the idea of remembering whether they had toggled on or
* off, and then only actually performing the function when the activity
* stopped, but I think that will be problematic. What about when they