Replaced download dialog with embedded progress bar (fixes #270).

This commit is contained in:
Felix Ableitner 2015-07-10 02:51:43 +02:00
parent f3e08bdc68
commit 8c25134031
9 changed files with 187 additions and 119 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

View File

@ -16,7 +16,7 @@
along with this program; if not, write to the Free Software along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/icon_and_title" android:id="@+id/icon_and_title"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -40,6 +40,7 @@
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:baselineAligned="false" android:baselineAligned="false"
android:orientation="vertical" android:orientation="vertical"
android:layout_toRightOf="@id/icon"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingStart="16dp"> android:paddingStart="16dp">
@ -109,7 +110,50 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> <LinearLayout
android:id="@+id/holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/icon"
android:gravity="center">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal" />
<TextView
android:id="@+id/progress_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_below="@id/progress_bar"
android:textSize="12sp"/>
<TextView
android:id="@+id/progress_percentage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@id/progress_bar"
android:textSize="12sp"/>
</RelativeLayout>
<ImageButton
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_weight="0"
android:visibility="gone"
android:src="@drawable/ic_clear"
android:background="@null"/>
</LinearLayout>
</RelativeLayout>

View File

@ -375,4 +375,13 @@
<string name="perms_new_perm_prefix"><font size="12" fgcolor="#ff33b5e5">NEW: </font></string> <string name="perms_new_perm_prefix"><font size="12" fgcolor="#ff33b5e5">NEW: </font></string>
<string name="perms_description_app">Provided by %1$s.</string> <string name="perms_description_app">Provided by %1$s.</string>
<string name="downloading">Downloading...</string>
<string-array name="file_size_units">
<item>B</item>
<item>KiB</item>
<item>MiB</item>
<item>GiB</item>
<item>TiB</item>
</string-array>
</resources> </resources>

View File

@ -21,7 +21,6 @@
package org.fdroid.fdroid; package org.fdroid.fdroid;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.ContentValues; import android.content.ContentValues;
@ -34,7 +33,6 @@ import android.content.pm.Signature;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
@ -63,9 +61,11 @@ import android.view.Window;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ListView; import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -90,6 +90,7 @@ import org.fdroid.fdroid.net.Downloader;
import java.io.File; import java.io.File;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -124,7 +125,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
private FDroidApp fdroidApp; private FDroidApp fdroidApp;
private ApkListAdapter adapter; private ApkListAdapter adapter;
private ProgressDialog progressDialog;
private static class ViewHolder { private static class ViewHolder {
TextView version; TextView version;
@ -323,11 +323,14 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
private final Context mctx = this; private final Context mctx = this;
private Installer installer; private Installer installer;
private AppDetailsHeaderFragment mHeaderFragment;
/** /**
* Stores relevant data that we want to keep track of when destroying the activity * 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 * 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 * 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. * between the activity being destroyed and recreated.
*/ */
private static class ConfigurationChangeHelper { 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). // chosen based on more than just orientation (e.g. large screen sizes).
View onlyInLandscape = findViewById(R.id.app_summary_container); View onlyInLandscape = findViewById(R.id.app_summary_container);
AppDetailsListFragment listFragment = AppDetailsListFragment mListFragment =
(AppDetailsListFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_app_list); (AppDetailsListFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_app_list);
if (onlyInLandscape == null) { if (onlyInLandscape == null) {
listFragment.setupSummaryHeader(); mListFragment.setupSummaryHeader();
} else { } else {
listFragment.removeSummaryHeader(); mListFragment.removeSummaryHeader();
} }
// Spinner seems to default to visible on Android 4.0.3 and 4.0.4 // 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 @Override
protected void onResume() { protected void onResumeFragments() {
super.onResume(); super.onResumeFragments();
refreshApkList();
refreshHeader();
supportInvalidateOptionsMenu();
if (downloadHandler != null) { if (downloadHandler != null) {
if (downloadHandler.isComplete()) { if (downloadHandler.isComplete()) {
downloadCompleteInstallApk(); downloadCompleteInstallApk();
} else { } else {
downloadHandler.setProgressListener(this); downloadHandler.setProgressListener(this);
// Show the progress dialog, if for no other reason than to prevent them attempting if (downloadHandler.getTotalSize() == 0)
// to download again (i.e. we force them to touch 'cancel' before they can access mHeaderFragment.startProgress();
// the rest of the activity). else
Log.d(TAG, "Showing dialog to user after resuming app details view, because a download was previously in progress"); mHeaderFragment.updateProgress(downloadHandler.getProgress(),
updateProgressDialog(); 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() { private void cleanUpFinishedDownload() {
if (downloadHandler != null) { if (downloadHandler != null) {
downloadHandler.removeProgressListener(); downloadHandler.removeProgressListener();
removeProgressDialog(); mHeaderFragment.removeProgress();
downloadHandler = null; downloadHandler = null;
} }
} }
@ -508,7 +506,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
downloadHandler.removeProgressListener(); downloadHandler.removeProgressListener();
} }
removeProgressDialog(); mHeaderFragment.removeProgress();
} }
private void onAppChanged() { private void onAppChanged() {
@ -553,13 +551,6 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
super.onDestroy(); super.onDestroy();
} }
private void removeProgressDialog() {
if (progressDialog != null) {
progressDialog.dismiss();
progressDialog = null;
}
}
// Reset the display and list contents. Used when entering the activity, and // Reset the display and list contents. Used when entering the activity, and
// also when something has been installed/uninstalled. // also when something has been installed/uninstalled.
// Return true if the app was found, false otherwise. // Return true if the app was found, false otherwise.
@ -619,9 +610,9 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
} }
private void refreshHeader() { private void refreshHeader() {
AppDetailsHeaderFragment headerFragment = (AppDetailsHeaderFragment) mHeaderFragment = (AppDetailsHeaderFragment)
getSupportFragmentManager().findFragmentById(R.id.header); getSupportFragmentManager().findFragmentById(R.id.header);
headerFragment.refresh(); mHeaderFragment.updateViews();
} }
@Override @Override
@ -805,6 +796,10 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
// Install the version of this app denoted by 'app.curApk'. // Install the version of this app denoted by 'app.curApk'.
@Override @Override
public void install(final Apk apk) { 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 }; final String[] projection = { RepoProvider.DataColumns.ADDRESS };
Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection); Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
if (repo == null || repo.address == null) { 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 = new ApkDownloader(getBaseContext(), apk, repoAddress);
downloadHandler.setProgressListener(this); downloadHandler.setProgressListener(this);
if (downloadHandler.download()) { 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))); 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 <code>downloadHandler</code> 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 @Override
public void onProgress(Event event) { public void onProgress(Event event) {
if (downloadHandler == null || !downloadHandler.isEventFromThis(event)) { if (downloadHandler == null || !downloadHandler.isEventFromThis(event)) {
@ -1035,7 +957,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
boolean finished = false; boolean finished = false;
switch (event.type) { switch (event.type) {
case Downloader.EVENT_PROGRESS: case Downloader.EVENT_PROGRESS:
updateProgressDialog(event.progress, event.total); if (mHeaderFragment != null)
mHeaderFragment.updateProgress(event.progress, event.total);
break; break;
case ApkDownloader.EVENT_ERROR: case ApkDownloader.EVENT_ERROR:
final String text; final String text;
@ -1054,7 +977,8 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
} }
if (finished) { if (finished) {
removeProgressDialog(); if (mHeaderFragment != null)
mHeaderFragment.removeProgress();
downloadHandler = null; 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 AppDetailsData data;
private Button btMain;
private ProgressBar progressBar;
private TextView progressSize;
private TextView progressPercent;
private ImageButton cancelButton;
protected final DisplayImageOptions displayImageOptions; protected final DisplayImageOptions displayImageOptions;
public static boolean installed = false; public static boolean installed = false;
public static boolean updateWanted = 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); TextView tv = (TextView) view.findViewById(R.id.title);
tv.setText(getApp().name); 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); updateViews(view);
} }
@Override @Override
public void onResume() { public void onResume() {
super.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()); updateViews(getView());
} }
public void updateViews(View view) { public void updateViews(View view) {
TextView statusView = (TextView) view.findViewById(R.id.status); TextView statusView = (TextView) view.findViewById(R.id.status);
Button btMain = (Button) view.findViewById(R.id.btn_main);
btMain.setVisibility(View.VISIBLE); 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. Check count > 0 due to incompatible apps resulting in an empty list.
If App isn't installed 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; installed = false;
statusView.setText(getString(R.string.details_notinstalled)); statusView.setText(getString(R.string.details_notinstalled));
NfcHelper.disableAndroidBeam(getActivity()); NfcHelper.disableAndroidBeam(getActivity());
// Set Install button and hide second button // Set Install button and hide second button
btMain.setText(R.string.menu_install); btMain.setText(R.string.menu_install);
btMain.setOnClickListener(mOnClickListener); btMain.setOnClickListener(mOnClickListener);
btMain.setEnabled(true);
} }
// If App is installed // If App is installed
else if (getApp().isInstalled()) { else if (getApp().isInstalled()) {
@ -1541,6 +1555,7 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
} }
} }
btMain.setOnClickListener(mOnClickListener); btMain.setOnClickListener(mOnClickListener);
btMain.setEnabled(true);
} }
TextView currentVersion = (TextView) view.findViewById(R.id.current_version); TextView currentVersion = (TextView) view.findViewById(R.id.current_version);
if (!getApks().isEmpty()) { if (!getApks().isEmpty()) {

View File

@ -706,7 +706,7 @@ public class ManageReposActivity extends ActionBarActivity {
/** /**
* If started by an intent that expects a result (e.g. QR codes) then we * 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. * to reflect the newly created repo.
*/ */
private void finishedAddingRepo() { 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 * 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 * 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 * toyed with the idea of remembering whether they had toggled on or
* off, and then only actually performing the function when the activity * off, and then only actually performing the function when the activity
* stopped, but I think that will be problematic. What about when they * stopped, but I think that will be problematic. What about when they