Merge branch 'feature/refactor-downloaders-async' of https://gitlab.com/pserwylo/fdroidclient
This commit is contained in:
commit
a08963f0e5
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,6 +6,6 @@
|
||||
/build.xml
|
||||
*~
|
||||
/.idea/
|
||||
/*.iml
|
||||
*.iml
|
||||
out
|
||||
/.settings/
|
||||
|
@ -19,31 +19,24 @@
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.content.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.fdroid.fdroid.data.*;
|
||||
import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException;
|
||||
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
|
||||
import org.xml.sax.XMLReader;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ListActivity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.*;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.Signature;
|
||||
import android.database.ContentObserver;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.view.MenuItemCompat;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.Signature;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.database.ContentObserver;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.Html.TagHandler;
|
||||
@ -58,24 +51,34 @@ import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||
|
||||
import org.fdroid.fdroid.Utils.CommaSeparatedList;
|
||||
import org.fdroid.fdroid.compat.ActionBarCompat;
|
||||
import org.fdroid.fdroid.compat.MenuManager;
|
||||
import org.fdroid.fdroid.compat.PackageManagerCompat;
|
||||
import org.fdroid.fdroid.data.*;
|
||||
import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException;
|
||||
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
|
||||
import org.fdroid.fdroid.net.ApkDownloader;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.xml.sax.XMLReader;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class AppDetails extends ListActivity {
|
||||
private static final String TAG = "AppDetails";
|
||||
public class AppDetails extends ListActivity implements ProgressListener {
|
||||
private static final String TAG = "org.fdroid.fdroid.AppDetails";
|
||||
|
||||
public static final int REQUEST_ENABLE_BLUETOOTH = 2;
|
||||
|
||||
@ -84,6 +87,7 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
private FDroidApp fdroidApp;
|
||||
private ApkListAdapter adapter;
|
||||
private ProgressDialog progressDialog;
|
||||
|
||||
private static class ViewHolder {
|
||||
TextView version;
|
||||
@ -110,7 +114,7 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange, Uri uri) {
|
||||
if (!reset()) {
|
||||
if (!reset(app.id)) {
|
||||
AppDetails.this.finish();
|
||||
return;
|
||||
}
|
||||
@ -267,10 +271,8 @@ public class AppDetails extends ListActivity {
|
||||
private static final int SEND_VIA_BLUETOOTH = Menu.FIRST + 15;
|
||||
|
||||
private App app;
|
||||
private String appid;
|
||||
private PackageManager mPm;
|
||||
private DownloadHandler downloadHandler;
|
||||
private boolean stateRetained;
|
||||
private ApkDownloader downloadHandler;
|
||||
|
||||
private boolean startingIgnoreAll;
|
||||
private int startingIgnoreThis;
|
||||
@ -282,6 +284,68 @@ public class AppDetails extends ListActivity {
|
||||
private DisplayImageOptions displayImageOptions;
|
||||
private Installer installer;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* between the activity being destroyed and recreated.
|
||||
*/
|
||||
private static class ConfigurationChangeHelper {
|
||||
|
||||
public ApkDownloader downloader;
|
||||
public App app;
|
||||
|
||||
public ConfigurationChangeHelper(ApkDownloader downloader, App app) {
|
||||
this.downloader = downloader;
|
||||
this.app = app;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean inProcessOfChangingConfiguration = false;
|
||||
|
||||
/**
|
||||
* Attempt to extract the appId from the intent which launched this activity.
|
||||
* Various different intents could cause us to show this activity, such as:
|
||||
* <ul>
|
||||
* <li>market://details?id=[app_id]</li>
|
||||
* <li>https://f-droid.org/app/[app_id]</li>
|
||||
* <li>fdroid.app:[app_id]</li>
|
||||
* </ul>
|
||||
* @return May return null, if we couldn't find the appId. In this case, you will
|
||||
* probably want to do something drastic like finish the activity and show some
|
||||
* feedback to the user (this method will <em>not</em> do that, it will just return
|
||||
* null).
|
||||
*/
|
||||
private String getAppIdFromIntent() {
|
||||
Intent i = getIntent();
|
||||
Uri data = i.getData();
|
||||
String appId = null;
|
||||
if (data != null) {
|
||||
if (data.isHierarchical()) {
|
||||
if (data.getHost() != null && data.getHost().equals("details")) {
|
||||
// market://details?id=app.id
|
||||
appId = data.getQueryParameter("id");
|
||||
} else {
|
||||
// https://f-droid.org/app/app.id
|
||||
appId = data.getLastPathSegment();
|
||||
if (appId != null && appId.equals("app")) {
|
||||
appId = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// fdroid.app:app.id
|
||||
appId = data.getEncodedSchemeSpecificPart();
|
||||
}
|
||||
Log.d("FDroid", "AppDetails launched from link, for '" + appId + "'");
|
||||
} else if (!i.hasExtra(EXTRA_APPID)) {
|
||||
Log.e("FDroid", "No application ID in AppDetails!?");
|
||||
} else {
|
||||
appId = i.getStringExtra(EXTRA_APPID);
|
||||
}
|
||||
return appId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
|
||||
@ -307,43 +371,27 @@ public class AppDetails extends ListActivity {
|
||||
// for reason why.
|
||||
ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
Intent i = getIntent();
|
||||
Uri data = i.getData();
|
||||
if (data != null) {
|
||||
if (data.isHierarchical()) {
|
||||
if (data.getHost() != null && data.getHost().equals("details")) {
|
||||
// market://details?id=app.id
|
||||
appid = data.getQueryParameter("id");
|
||||
} else {
|
||||
// https://f-droid.org/app/app.id
|
||||
appid = data.getLastPathSegment();
|
||||
if (appid != null && appid.equals("app")) appid = null;
|
||||
}
|
||||
} else {
|
||||
// fdroid.app:app.id
|
||||
appid = data.getEncodedSchemeSpecificPart();
|
||||
}
|
||||
Log.d("FDroid", "AppDetails launched from link, for '" + appid + "'");
|
||||
} else if (!i.hasExtra(EXTRA_APPID)) {
|
||||
Log.d("FDroid", "No application ID in AppDetails!?");
|
||||
} else {
|
||||
appid = i.getStringExtra(EXTRA_APPID);
|
||||
}
|
||||
|
||||
if (i.hasExtra(EXTRA_FROM)) {
|
||||
setTitle(i.getStringExtra(EXTRA_FROM));
|
||||
if (getIntent().hasExtra(EXTRA_FROM)) {
|
||||
setTitle(getIntent().getStringExtra(EXTRA_FROM));
|
||||
}
|
||||
|
||||
mPm = getPackageManager();
|
||||
|
||||
installer = Installer.getActivityInstaller(this, mPm,
|
||||
myInstallerCallback);
|
||||
|
||||
// Get the preferences we're going to use in this Activity...
|
||||
AppDetails old = (AppDetails) getLastNonConfigurationInstance();
|
||||
if (old != null) {
|
||||
copyState(old);
|
||||
ConfigurationChangeHelper previousData = (ConfigurationChangeHelper)getLastNonConfigurationInstance();
|
||||
if (previousData != null) {
|
||||
Log.d(TAG, "Recreating view after configuration change.");
|
||||
downloadHandler = previousData.downloader;
|
||||
if (downloadHandler != null) {
|
||||
Log.d(TAG, "Download was in progress before the configuration change, so we will start to listen to its events again.");
|
||||
}
|
||||
app = previousData.app;
|
||||
setApp(app);
|
||||
} else {
|
||||
if (!reset()) {
|
||||
if (!reset(getAppIdFromIntent())) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
@ -377,7 +425,6 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
Log.d(TAG, "onresume");
|
||||
super.onResume();
|
||||
|
||||
// register observer to know when install status changes
|
||||
@ -386,17 +433,45 @@ public class AppDetails extends ListActivity {
|
||||
AppProvider.getContentUri(app.id),
|
||||
true,
|
||||
myAppObserver);
|
||||
if (downloadHandler != null) {
|
||||
if (downloadHandler.isComplete()) {
|
||||
downloadCompleteInstallApk();
|
||||
} else {
|
||||
downloadHandler.setProgressListener(this);
|
||||
|
||||
if (!reset()) {
|
||||
finish();
|
||||
return;
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
updateViews();
|
||||
|
||||
MenuManager.create(this).invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove progress listener, suppress progress dialog, set downloadHandler to null.
|
||||
*/
|
||||
private void cleanUpFinishedDownload() {
|
||||
if (downloadHandler != null) {
|
||||
downloadHandler.startUpdates();
|
||||
downloadHandler.removeProgressListener();
|
||||
removeProgressDialog();
|
||||
downloadHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the download completes successfully, call this method to start the install process
|
||||
* with the file that was downloaded.
|
||||
*/
|
||||
private void downloadCompleteInstallApk() {
|
||||
if (downloadHandler != null) {
|
||||
assert downloadHandler.isComplete();
|
||||
installApk(downloadHandler.localFile(), downloadHandler.getApk().id);
|
||||
cleanUpFinishedDownload();
|
||||
}
|
||||
}
|
||||
|
||||
@ -405,13 +480,18 @@ public class AppDetails extends ListActivity {
|
||||
if (myAppObserver != null) {
|
||||
getContentResolver().unregisterContentObserver(myAppObserver);
|
||||
}
|
||||
if (downloadHandler != null) {
|
||||
downloadHandler.stopUpdates();
|
||||
}
|
||||
if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
|
||||
|| app.ignoreThisUpdate != startingIgnoreThis)) {
|
||||
Log.d(TAG, "Updating 'ignore updates', as it has changed since we started the activity...");
|
||||
setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate);
|
||||
}
|
||||
|
||||
if (downloadHandler != null) {
|
||||
downloadHandler.removeProgressListener();
|
||||
}
|
||||
|
||||
removeProgressDialog();
|
||||
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@ -430,65 +510,73 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
stateRetained = true;
|
||||
return this;
|
||||
inProcessOfChangingConfiguration = true;
|
||||
return new ConfigurationChangeHelper(downloadHandler, app);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (downloadHandler != null) {
|
||||
if (!stateRetained)
|
||||
if (!inProcessOfChangingConfiguration) {
|
||||
downloadHandler.cancel();
|
||||
downloadHandler.destroy();
|
||||
cleanUpFinishedDownload();
|
||||
}
|
||||
}
|
||||
inProcessOfChangingConfiguration = false;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// Copy all relevant state from an old instance. This is used in
|
||||
// place of reset(), so it must initialize all fields normally set
|
||||
// there.
|
||||
private void copyState(AppDetails old) {
|
||||
if (old.downloadHandler != null)
|
||||
downloadHandler = new DownloadHandler(old.downloadHandler);
|
||||
app = old.app;
|
||||
mInstalledSignature = old.mInstalledSignature;
|
||||
mInstalledSigID = old.mInstalledSigID;
|
||||
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.
|
||||
private boolean reset() {
|
||||
private boolean reset(String appId) {
|
||||
|
||||
Log.d("FDroid", "Getting application details for " + appid);
|
||||
app = null;
|
||||
Log.d("FDroid", "Getting application details for " + appId);
|
||||
App newApp = null;
|
||||
|
||||
if (appid != null && appid.length() > 0) {
|
||||
app = AppProvider.Helper.findById(getContentResolver(), appid);
|
||||
if (appId != null && appId.length() > 0) {
|
||||
newApp = AppProvider.Helper.findById(getContentResolver(), appId);
|
||||
}
|
||||
|
||||
if (app == null) {
|
||||
Toast toast = Toast.makeText(this,
|
||||
getString(R.string.no_such_app), Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
setApp(newApp);
|
||||
|
||||
return this.app != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If passed null, this will show a message to the user ("Could not find app ..." or something
|
||||
* like that) and then finish the activity.
|
||||
*/
|
||||
private void setApp(App newApp) {
|
||||
|
||||
if (newApp == null) {
|
||||
Toast.makeText(this, getString(R.string.no_such_app), Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
app = newApp;
|
||||
|
||||
startingIgnoreAll = app.ignoreAllUpdates;
|
||||
startingIgnoreThis = app.ignoreThisUpdate;
|
||||
|
||||
// Get the signature of the installed package...
|
||||
mInstalledSignature = null;
|
||||
mInstalledSigID = null;
|
||||
|
||||
if (app.isInstalled()) {
|
||||
PackageManager pm = getBaseContext().getPackageManager();
|
||||
PackageManager pm = getPackageManager();
|
||||
try {
|
||||
PackageInfo pi = pm.getPackageInfo(appid,
|
||||
PackageManager.GET_SIGNATURES);
|
||||
PackageInfo pi = pm.getPackageInfo(app.id, PackageManager.GET_SIGNATURES);
|
||||
mInstalledSignature = pi.signatures[0];
|
||||
Hasher hash = new Hasher("MD5", mInstalledSignature
|
||||
.toCharsString().getBytes());
|
||||
Hasher hash = new Hasher("MD5", mInstalledSignature.toCharsString().getBytes());
|
||||
mInstalledSigID = hash.getHash();
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.d("FDroid", "Failed to get installed signature");
|
||||
@ -497,7 +585,6 @@ public class AppDetails extends ListActivity {
|
||||
mInstalledSignature = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startViews() {
|
||||
@ -511,10 +598,10 @@ public class AppDetails extends ListActivity {
|
||||
headerView.removeAllViews();
|
||||
if (landparent != null) {
|
||||
landparent.addView(infoView);
|
||||
Log.d("FDroid", "Setting landparent infoview");
|
||||
Log.d("FDroid", "Setting up landscape view");
|
||||
} else {
|
||||
headerView.addView(infoView);
|
||||
Log.d("FDroid", "Setting header infoview");
|
||||
Log.d("FDroid", "Setting up portrait view");
|
||||
}
|
||||
|
||||
// Set the icon...
|
||||
@ -610,8 +697,7 @@ public class AppDetails extends ListActivity {
|
||||
if (permissionName.equals("ACCESS_SUPERUSER")) {
|
||||
sb.append("\t• Full permissions to all device features and storage\n");
|
||||
} else {
|
||||
Log.d("FDroid", "Permission not yet available: "
|
||||
+permissionName);
|
||||
Log.d("FDroid", "Permission not yet available: " + permissionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -912,6 +998,7 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
// Install the version of this app denoted by 'app.curApk'.
|
||||
private void install(final Apk apk) {
|
||||
final Activity activity = this;
|
||||
String [] projection = { RepoProvider.DataColumns.ADDRESS };
|
||||
Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
|
||||
if (repo == null || repo.address == null) {
|
||||
@ -927,9 +1014,7 @@ public class AppDetails extends ListActivity {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog,
|
||||
int whichButton) {
|
||||
downloadHandler = new DownloadHandler(apk,
|
||||
repoaddress, Utils
|
||||
.getApkCacheDir(getBaseContext()));
|
||||
startDownload(apk, repoaddress);
|
||||
}
|
||||
});
|
||||
ask_alrt.setNegativeButton(getString(R.string.no),
|
||||
@ -958,9 +1043,17 @@ public class AppDetails extends ListActivity {
|
||||
alert.show();
|
||||
return;
|
||||
}
|
||||
downloadHandler = new DownloadHandler(apk, repoaddress,
|
||||
Utils.getApkCacheDir(getBaseContext()));
|
||||
startDownload(apk, repoaddress);
|
||||
}
|
||||
|
||||
private void startDownload(Apk apk, String repoAddress) {
|
||||
downloadHandler = new ApkDownloader(apk, repoAddress, Utils.getApkCacheDir(getBaseContext()));
|
||||
downloadHandler.setProgressListener(this);
|
||||
if (downloadHandler.download()) {
|
||||
updateProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void installApk(File file, String packageName) {
|
||||
setProgressBarIndeterminateVisibility(true);
|
||||
|
||||
@ -989,10 +1082,6 @@ public class AppDetails extends ListActivity {
|
||||
@Override
|
||||
public void run() {
|
||||
if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
|
||||
if (downloadHandler != null) {
|
||||
downloadHandler = null;
|
||||
}
|
||||
|
||||
PackageManagerCompat.setInstaller(mPm, app.id);
|
||||
}
|
||||
|
||||
@ -1044,18 +1133,29 @@ public class AppDetails extends ListActivity {
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share)));
|
||||
}
|
||||
|
||||
private ProgressDialog createProgressDialog(String file, int p, int max) {
|
||||
private ProgressDialog getProgressDialog(String file) {
|
||||
if (progressDialog == null) {
|
||||
final ProgressDialog pd = new ProgressDialog(this);
|
||||
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
pd.setMessage(getString(R.string.download_server) + ":\n " + file);
|
||||
pd.setMax(max);
|
||||
pd.setProgress(p);
|
||||
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,
|
||||
@ -1065,111 +1165,78 @@ public class AppDetails extends ListActivity {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
pd.cancel();
|
||||
}
|
||||
});
|
||||
pd.show();
|
||||
return pd;
|
||||
}
|
||||
);
|
||||
progressDialog = pd;
|
||||
}
|
||||
return progressDialog;
|
||||
}
|
||||
|
||||
// Handler used to update the progress dialog while downloading.
|
||||
private class DownloadHandler extends Handler {
|
||||
private Downloader download;
|
||||
private ProgressDialog pd;
|
||||
private boolean updating;
|
||||
private String id;
|
||||
|
||||
public DownloadHandler(Apk apk, String repoaddress, File destdir) {
|
||||
id = apk.id;
|
||||
download = new Downloader(apk, repoaddress, destdir);
|
||||
download.start();
|
||||
startUpdates();
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadHandler(DownloadHandler oldHandler) {
|
||||
if (oldHandler != null) {
|
||||
download = oldHandler.download;
|
||||
}
|
||||
startUpdates();
|
||||
}
|
||||
|
||||
public boolean updateProgress() {
|
||||
boolean finished = false;
|
||||
switch (download.getStatus()) {
|
||||
case RUNNING:
|
||||
if (pd == null) {
|
||||
pd = createProgressDialog(download.remoteFile(),
|
||||
download.getProgress(), download.getMax());
|
||||
private void updateProgressDialog(int progress, int total) {
|
||||
if (downloadHandler != null) {
|
||||
ProgressDialog pd = getProgressDialog(downloadHandler.getRemoteAddress());
|
||||
if (total > 0) {
|
||||
pd.setIndeterminate(false);
|
||||
pd.setProgress(progress);
|
||||
pd.setMax(total);
|
||||
} else {
|
||||
pd.setProgress(download.getProgress());
|
||||
pd.setIndeterminate(true);
|
||||
pd.setProgress(progress);
|
||||
pd.setMax(0);
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
if (pd != null)
|
||||
pd.dismiss();
|
||||
String text;
|
||||
if (download.getErrorType() == Downloader.Error.CORRUPT)
|
||||
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)) {
|
||||
// Choose not to respond to events from previous downloaders.
|
||||
// We don't even care if we receive "cancelled" events or the like, because
|
||||
// we dealt with cancellations in the onCancel listener of the dialog,
|
||||
// rather than waiting to receive the event here. We try and be careful in
|
||||
// the download thread to make sure that we check for cancellations before
|
||||
// sending events, but it is not possible to be perfect, because the interruption
|
||||
// which triggers the download can happen after the check to see if
|
||||
Log.d(TAG, "Discarding downloader event \"" + event.type + "\" as it is from an old (probably cancelled) downloader.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean finished = false;
|
||||
if (event.type.equals(Downloader.EVENT_PROGRESS)) {
|
||||
updateProgressDialog(event.progress, event.total);
|
||||
} else if (event.type.equals(ApkDownloader.EVENT_ERROR)) {
|
||||
final String text;
|
||||
if (event.getData().getInt(ApkDownloader.EVENT_DATA_ERROR_TYPE) == ApkDownloader.ERROR_HASH_MISMATCH)
|
||||
text = getString(R.string.corrupt_download);
|
||||
else
|
||||
text = download.getErrorMessage();
|
||||
Toast.makeText(AppDetails.this, text, Toast.LENGTH_LONG).show();
|
||||
text = getString(R.string.details_notinstalled);
|
||||
// this must be on the main UI thread
|
||||
Toast.makeText(this, text, Toast.LENGTH_LONG).show();
|
||||
finished = true;
|
||||
break;
|
||||
case DONE:
|
||||
if (pd != null)
|
||||
pd.dismiss();
|
||||
installApk(download.localFile(), id);
|
||||
} else if (event.type.equals(ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE)) {
|
||||
downloadCompleteInstallApk();
|
||||
finished = true;
|
||||
break;
|
||||
case CANCELLED:
|
||||
Toast.makeText(AppDetails.this,
|
||||
getString(R.string.download_cancelled),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
finished = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return finished;
|
||||
}
|
||||
|
||||
public void startUpdates() {
|
||||
if (!updating) {
|
||||
updating = true;
|
||||
sendEmptyMessage(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopUpdates() {
|
||||
updating = false;
|
||||
removeMessages(0);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (download != null)
|
||||
download.interrupt();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
// The dialog can't be dismissed when it's not displayed,
|
||||
// so do it when the activity is being destroyed.
|
||||
if (pd != null) {
|
||||
pd.dismiss();
|
||||
pd = null;
|
||||
}
|
||||
// Cancel any scheduled updates so that we don't
|
||||
// accidentally recreate the progress dialog.
|
||||
stopUpdates();
|
||||
}
|
||||
|
||||
// Repeatedly run updateProgress() until it's finished.
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (download == null)
|
||||
return;
|
||||
boolean finished = updateProgress();
|
||||
if (finished)
|
||||
download = null;
|
||||
else
|
||||
sendMessageDelayed(obtainMessage(), 50);
|
||||
if (finished) {
|
||||
removeProgressDialog();
|
||||
downloadHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,195 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2010-2012 Ciaran Gultnieks <ciaran@ciarang.com>
|
||||
* Copyright (C) 2011 Henrik Tunedal <tunedal@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
|
||||
public class Downloader extends Thread {
|
||||
|
||||
private Apk curapk;
|
||||
private String repoaddress;
|
||||
private String filename;
|
||||
private File destdir;
|
||||
private File localfile;
|
||||
|
||||
public static enum Status {
|
||||
STARTING, RUNNING, ERROR, DONE, CANCELLED
|
||||
}
|
||||
|
||||
public static enum Error {
|
||||
CORRUPT, UNKNOWN
|
||||
}
|
||||
|
||||
private Status status = Status.STARTING;
|
||||
private Error error;
|
||||
private int progress;
|
||||
private int max;
|
||||
private String errorMessage;
|
||||
|
||||
// Constructor - creates a Downloader to download the given Apk,
|
||||
// which must have its detail populated.
|
||||
Downloader(Apk apk, String repoaddress, File destdir) {
|
||||
curapk = apk;
|
||||
this.repoaddress = repoaddress;
|
||||
this.destdir = destdir;
|
||||
}
|
||||
|
||||
public synchronized Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Current progress and maximum value for progress dialog
|
||||
public synchronized int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public synchronized int getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
// Error code and error message, only valid if status is ERROR
|
||||
public synchronized Error getErrorType() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public synchronized String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
// The URL being downloaded or path to a cached file
|
||||
public synchronized String remoteFile() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
// The downloaded APK. Valid only when getStatus() has returned STATUS.DONE.
|
||||
public File localFile() {
|
||||
return localfile;
|
||||
}
|
||||
|
||||
// The APK being downloaded
|
||||
public synchronized Apk getApk() {
|
||||
return curapk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
InputStream input = null;
|
||||
OutputStream output = null;
|
||||
String apkname = curapk.apkName;
|
||||
localfile = new File(destdir, apkname);
|
||||
try {
|
||||
|
||||
// See if we already have this apk cached...
|
||||
if (localfile.exists()) {
|
||||
// We do - if its hash matches, we'll use it...
|
||||
Hasher hash = new Hasher(curapk.hashType, localfile);
|
||||
if (hash.match(curapk.hash)) {
|
||||
Log.d("FDroid", "Using cached apk at " + localfile);
|
||||
synchronized (this) {
|
||||
progress = 1;
|
||||
max = 1;
|
||||
status = Status.DONE;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Log.d("FDroid", "Not using cached apk at " + localfile);
|
||||
localfile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't got the apk locally, we'll have to download it...
|
||||
String remotefile;
|
||||
remotefile = repoaddress + "/" + apkname.replace(" ", "%20");
|
||||
Log.d("FDroid", "Downloading apk from " + remotefile);
|
||||
synchronized (this) {
|
||||
filename = remotefile;
|
||||
progress = 0;
|
||||
max = curapk.size;
|
||||
status = Status.RUNNING;
|
||||
}
|
||||
|
||||
input = new URL(remotefile).openStream();
|
||||
output = new FileOutputStream(localfile);
|
||||
byte data[] = new byte[Utils.BUFFER_SIZE];
|
||||
while (true) {
|
||||
if (isInterrupted()) {
|
||||
Log.d("FDroid", "Download cancelled!");
|
||||
break;
|
||||
}
|
||||
int count = input.read(data);
|
||||
if (count == -1) {
|
||||
break;
|
||||
}
|
||||
output.write(data, 0, count);
|
||||
synchronized (this) {
|
||||
progress += count;
|
||||
}
|
||||
}
|
||||
|
||||
if (isInterrupted()) {
|
||||
localfile.delete();
|
||||
synchronized (this) {
|
||||
status = Status.CANCELLED;
|
||||
}
|
||||
return;
|
||||
}
|
||||
Hasher hash = new Hasher(curapk.hashType, localfile);
|
||||
if (!hash.match(curapk.hash)) {
|
||||
synchronized (this) {
|
||||
Log.d("FDroid", "Downloaded file hash of " + hash.getHash()
|
||||
+ " did not match repo's " + curapk.hash);
|
||||
// No point keeping a bad file, whether we're
|
||||
// caching or not.
|
||||
localfile.delete();
|
||||
error = Error.CORRUPT;
|
||||
errorMessage = null;
|
||||
status = Status.ERROR;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("FDroid", "Download failed:\n" + Log.getStackTraceString(e));
|
||||
synchronized (this) {
|
||||
localfile.delete();
|
||||
error = Error.UNKNOWN;
|
||||
errorMessage = e.toString();
|
||||
status = Status.ERROR;
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
Utils.closeQuietly(output);
|
||||
Utils.closeQuietly(input);
|
||||
}
|
||||
|
||||
Log.d("FDroid", "Download finished: " + localfile);
|
||||
synchronized (this) {
|
||||
status = Status.DONE;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
public interface ProgressListener {
|
||||
|
||||
@ -15,7 +17,7 @@ public interface ProgressListener {
|
||||
|
||||
public static final int NO_VALUE = Integer.MIN_VALUE;
|
||||
|
||||
public final int type;
|
||||
public final String type;
|
||||
public final Bundle data;
|
||||
|
||||
// These two are not final, so that you can create a template Event,
|
||||
@ -25,31 +27,19 @@ public interface ProgressListener {
|
||||
public int progress;
|
||||
public int total;
|
||||
|
||||
public Event(int type) {
|
||||
public Event(String type) {
|
||||
this(type, NO_VALUE, NO_VALUE, null);
|
||||
}
|
||||
|
||||
public Event(int type, Bundle data) {
|
||||
public Event(String type, Bundle data) {
|
||||
this(type, NO_VALUE, NO_VALUE, data);
|
||||
}
|
||||
|
||||
public Event(int type, int progress) {
|
||||
this(type, progress, NO_VALUE, null);
|
||||
}
|
||||
|
||||
public Event(int type, int progress, Bundle data) {
|
||||
this(type, NO_VALUE, NO_VALUE, data);
|
||||
}
|
||||
|
||||
public Event(int type, int progress, int total) {
|
||||
this(type, progress, total, null);
|
||||
}
|
||||
|
||||
public Event(int type, int progress, int total, Bundle data) {
|
||||
public Event(String type, int progress, int total, Bundle data) {
|
||||
this.type = type;
|
||||
this.progress = progress;
|
||||
this.total = total;
|
||||
this.data = data == null ? new Bundle() : data;
|
||||
this.data = (data == null) ? new Bundle() : data;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -59,7 +49,7 @@ public interface ProgressListener {
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(type);
|
||||
dest.writeString(type);
|
||||
dest.writeInt(progress);
|
||||
dest.writeInt(total);
|
||||
dest.writeBundle(data);
|
||||
@ -68,7 +58,7 @@ public interface ProgressListener {
|
||||
public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
|
||||
@Override
|
||||
public Event createFromParcel(Parcel in) {
|
||||
return new Event(in.readInt(), in.readInt(), in.readInt(), in.readBundle());
|
||||
return new Event(in.readString(), in.readInt(), in.readInt(), in.readBundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -77,6 +67,16 @@ public interface ProgressListener {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Can help to provide context to the listener about what process is causing the event.
|
||||
* For example, the repo updater uses one listener to listen to multiple downloaders.
|
||||
* When it receives an event, it doesn't know which repo download is causing the event,
|
||||
* so we pass that through to the downloader when we set the progress listener. This way,
|
||||
* we can ask the event for the name of the repo.
|
||||
*/
|
||||
public Bundle getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -279,12 +279,13 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
} else if (localName.equals("application") && curapp == null) {
|
||||
curapp = new App();
|
||||
curapp.id = attributes.getValue("", "id");
|
||||
Bundle progressData = RepoUpdater.createProgressData(repo.address);
|
||||
progressCounter ++;
|
||||
Bundle data = new Bundle(1);
|
||||
data.putString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS, repo.address);
|
||||
progressListener.onProgress(
|
||||
new ProgressListener.Event(
|
||||
RepoUpdater.PROGRESS_TYPE_PROCESS_XML, progressCounter,
|
||||
totalAppCount, progressData));
|
||||
RepoUpdater.PROGRESS_TYPE_PROCESS_XML,
|
||||
progressCounter, totalAppCount, data));
|
||||
|
||||
} else if (localName.equals("package") && curapp != null && curapk == null) {
|
||||
curapk = new Apk();
|
||||
|
@ -33,6 +33,7 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
import org.fdroid.fdroid.data.*;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.fdroid.fdroid.updater.RepoUpdater;
|
||||
|
||||
import java.util.*;
|
||||
@ -47,6 +48,14 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
public static final int STATUS_ERROR = 2;
|
||||
public static final int STATUS_INFO = 3;
|
||||
|
||||
// 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_ERROR = "repoUpdateError";
|
||||
public static final String EVENT_INFO = "repoUpdateInfo";
|
||||
|
||||
public static final String EXTRA_RECEIVER = "receiver";
|
||||
public static final String EXTRA_ADDRESS = "address";
|
||||
|
||||
@ -97,28 +106,31 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
return this;
|
||||
}
|
||||
|
||||
private void forwardEvent(String type) {
|
||||
if (listener != null) {
|
||||
listener.onProgress(new Event(type));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
String message = resultData.getString(UpdateService.RESULT_MESSAGE);
|
||||
boolean finished = false;
|
||||
if (resultCode == UpdateService.STATUS_ERROR) {
|
||||
forwardEvent(EVENT_ERROR);
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
|
||||
finished = true;
|
||||
} else if (resultCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES
|
||||
|| resultCode == UpdateService.STATUS_COMPLETE_AND_SAME) {
|
||||
} else if (resultCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
|
||||
forwardEvent(EVENT_COMPLETE_WITH_CHANGES);
|
||||
finished = true;
|
||||
} else if (resultCode == UpdateService.STATUS_COMPLETE_AND_SAME) {
|
||||
forwardEvent(EVENT_COMPLETE_AND_SAME);
|
||||
finished = true;
|
||||
} else if (resultCode == UpdateService.STATUS_INFO) {
|
||||
forwardEvent(EVENT_INFO);
|
||||
dialog.setMessage(message);
|
||||
}
|
||||
|
||||
// Forward the progress event on to anybody else who'd like to know.
|
||||
if (listener != null) {
|
||||
Parcelable event = resultData.getParcelable(UpdateService.RESULT_EVENT);
|
||||
if (event != null && event instanceof Event) {
|
||||
listener.onProgress((Event)event);
|
||||
}
|
||||
}
|
||||
|
||||
if (finished && dialog.isShowing())
|
||||
try {
|
||||
dialog.dismiss();
|
||||
@ -185,17 +197,10 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
}
|
||||
|
||||
protected void sendStatus(int statusCode, String message) {
|
||||
sendStatus(statusCode, message, null);
|
||||
}
|
||||
|
||||
protected void sendStatus(int statusCode, String message, Event event) {
|
||||
if (receiver != null) {
|
||||
Bundle resultData = new Bundle();
|
||||
if (message != null && message.length() > 0)
|
||||
resultData.putString(RESULT_MESSAGE, message);
|
||||
if (event == null)
|
||||
event = new Event(statusCode);
|
||||
resultData.putParcelable(RESULT_EVENT, event);
|
||||
receiver.send(statusCode, resultData);
|
||||
}
|
||||
}
|
||||
@ -675,14 +680,15 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
@Override
|
||||
public void onProgress(ProgressListener.Event event) {
|
||||
String message = "";
|
||||
if (event.type == RepoUpdater.PROGRESS_TYPE_DOWNLOAD) {
|
||||
String repoAddress = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
|
||||
// TODO: Switch to passing through Bundles of data with the event, rather than a repo address. They are
|
||||
// now much more general purpose then just repo downloading.
|
||||
String repoAddress = event.getData().getString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS);
|
||||
if (event.type.equals(Downloader.EVENT_PROGRESS)) {
|
||||
String downloadedSize = Utils.getFriendlySize(event.progress);
|
||||
String totalSize = Utils.getFriendlySize(event.total);
|
||||
int percent = (int)((double)event.progress/event.total * 100);
|
||||
message = getString(R.string.status_download, repoAddress, downloadedSize, totalSize, percent);
|
||||
} else if (event.type == RepoUpdater.PROGRESS_TYPE_PROCESS_XML) {
|
||||
String repoAddress = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
|
||||
} else if (event.type.equals(RepoUpdater.PROGRESS_TYPE_PROCESS_XML)) {
|
||||
message = getString(R.string.status_processing_xml, repoAddress, event.progress, event.total);
|
||||
}
|
||||
sendStatus(STATUS_INFO, message);
|
||||
|
277
src/org/fdroid/fdroid/net/ApkDownloader.java
Normal file
277
src/org/fdroid/fdroid/net/ApkDownloader.java
Normal file
@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Copyright (C) 2010-2012 Ciaran Gultnieks <ciaran@ciarang.com>
|
||||
* Copyright (C) 2011 Henrik Tunedal <tunedal@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.Hasher;
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Downloads and verifies (against the Apk.hash) the apk file.
|
||||
* If the file has previously been downloaded, it will make use of that
|
||||
* instead, without going to the network to download a new one.
|
||||
*/
|
||||
public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
||||
|
||||
private static final String TAG = "org.fdroid.fdroid.net.ApkDownloader";
|
||||
|
||||
public static final String EVENT_APK_DOWNLOAD_COMPLETE = "apkDownloadComplete";
|
||||
public static final String EVENT_APK_DOWNLOAD_CANCELLED = "apkDownloadCancelled";
|
||||
public static final String EVENT_ERROR = "apkDownloadError";
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Used as a key to pass data through with an error event, explaining the type of event.
|
||||
*/
|
||||
public static final String EVENT_DATA_ERROR_TYPE = "apkDownloadErrorType";
|
||||
|
||||
private Apk curApk;
|
||||
private String repoAddress;
|
||||
private File localFile;
|
||||
|
||||
private ProgressListener listener;
|
||||
private AsyncDownloadWrapper dlWrapper = null;
|
||||
private int progress = 0;
|
||||
private int totalSize = 0;
|
||||
private boolean isComplete = false;
|
||||
|
||||
private long id = ++downloadIdCounter;
|
||||
|
||||
public void setProgressListener(ProgressListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void removeProgressListener() {
|
||||
setProgressListener(null);
|
||||
}
|
||||
|
||||
public ApkDownloader(Apk apk, String repoAddress, File destDir) {
|
||||
curApk = apk;
|
||||
this.repoAddress = repoAddress;
|
||||
localFile = new File(destDir, curApk.apkName);
|
||||
}
|
||||
|
||||
/**
|
||||
* The downloaded APK. Valid only when getStatus() has returned STATUS.DONE.
|
||||
*/
|
||||
public File localFile() {
|
||||
return localFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* When stopping/starting downloaders multiple times (on different threads), it can
|
||||
* get weird whereby different threads are sending progress events. It is important
|
||||
* to be able to see which downloader these progress events are coming from.
|
||||
*/
|
||||
public boolean isEventFromThis(Event event) {
|
||||
return event.getData().containsKey(EVENT_SOURCE_ID) && event.getData().getLong(EVENT_SOURCE_ID) == id;
|
||||
}
|
||||
|
||||
public String getRemoteAddress() {
|
||||
return repoAddress + "/" + curApk.apkName.replace(" ", "%20");
|
||||
}
|
||||
|
||||
private Hasher createHasher() {
|
||||
Hasher hasher;
|
||||
try {
|
||||
hasher = new Hasher(curApk.hashType, localFile);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.e("FDroid", "Error verifying hash of cached apk at " + localFile + ". " +
|
||||
"I don't understand what the " + curApk.hashType + " hash algorithm is :(");
|
||||
hasher = null;
|
||||
}
|
||||
return hasher;
|
||||
}
|
||||
|
||||
private boolean hashMatches() {
|
||||
if (!localFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
Hasher hasher = createHasher();
|
||||
return hasher != null && hasher.match(curApk.hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* If an existing cached version exists, and matches the hash of the apk we
|
||||
* want to download, then we will return true. Otherwise, we return false
|
||||
* (and remove the cached file - if it exists and didn't match the correct hash).
|
||||
*/
|
||||
private boolean verifyOrDeleteCachedVersion() {
|
||||
if (localFile.exists()) {
|
||||
if (hashMatches()) {
|
||||
Log.d("FDroid", "Using cached apk at " + localFile);
|
||||
return true;
|
||||
} else {
|
||||
Log.d("FDroid", "Not using cached apk at " + localFile);
|
||||
deleteLocalFile();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void deleteLocalFile() {
|
||||
if (localFile != null && localFile.exists()) {
|
||||
localFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendCompleteMessage() {
|
||||
isComplete = true;
|
||||
sendMessage(EVENT_APK_DOWNLOAD_COMPLETE);
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return this.isComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the download successfully spins up a new thread to start downloading, then we return
|
||||
* true, otherwise false. This is useful, e.g. when we use a cached version, and so don't
|
||||
* want to bother with progress dialogs et al.
|
||||
*/
|
||||
public boolean download() {
|
||||
|
||||
// Can we use the cached version?
|
||||
if (verifyOrDeleteCachedVersion()) {
|
||||
sendCompleteMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
String remoteAddress = getRemoteAddress();
|
||||
Log.d(TAG, "Downloading apk from " + remoteAddress);
|
||||
|
||||
try {
|
||||
|
||||
Downloader downloader = new HttpDownloader(remoteAddress, localFile);
|
||||
dlWrapper = new AsyncDownloadWrapper(downloader, this);
|
||||
dlWrapper.download();
|
||||
return true;
|
||||
|
||||
} catch (MalformedURLException e) {
|
||||
onErrorDownloading(e.getLocalizedMessage());
|
||||
} catch (IOException e) {
|
||||
onErrorDownloading(e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void sendMessage(String type) {
|
||||
sendProgressEvent(new ProgressListener.Event(type));
|
||||
}
|
||||
|
||||
private void sendError(int errorType) {
|
||||
Bundle data = new Bundle(1);
|
||||
data.putInt(EVENT_DATA_ERROR_TYPE, errorType);
|
||||
sendProgressEvent(new Event(EVENT_ERROR, data));
|
||||
}
|
||||
|
||||
private void sendProgressEvent(Event event) {
|
||||
if (event.type.equals(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;
|
||||
}
|
||||
|
||||
event.getData().putLong(EVENT_SOURCE_ID, id);
|
||||
|
||||
if (listener != null) {
|
||||
listener.onProgress(event);
|
||||
}
|
||||
}
|
||||
|
||||
@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("FDroid", "Download failed: " + localisedExceptionDetails);
|
||||
sendError(ERROR_DOWNLOAD_FAILED);
|
||||
deleteLocalFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadComplete() {
|
||||
|
||||
if (!verifyOrDeleteCachedVersion()) {
|
||||
sendError(ERROR_HASH_MISMATCH);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d("FDroid", "Download finished: " + localFile);
|
||||
sendCompleteMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadCancelled() {
|
||||
sendMessage(EVENT_APK_DOWNLOAD_CANCELLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(Event event) {
|
||||
sendProgressEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to cancel the download (if in progress) and also removes the progress
|
||||
* listener (to prevent
|
||||
*/
|
||||
public void cancel() {
|
||||
if (dlWrapper != null) {
|
||||
dlWrapper.attemptCancel();
|
||||
}
|
||||
}
|
||||
|
||||
public Apk getApk() {
|
||||
return curApk;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public int getTotalSize() {
|
||||
return totalSize;
|
||||
}
|
||||
}
|
145
src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java
Normal file
145
src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java
Normal file
@ -0,0 +1,145 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Given a {@link org.fdroid.fdroid.net.Downloader}, this wrapper will conduct the download operation on a
|
||||
* separate thread. All progress/status/error/etc events will be forwarded from that thread to the thread
|
||||
* that {@link AsyncDownloadWrapper#download()} was invoked on. If you want to respond with UI feedback
|
||||
* to these events, it is important that you execute the download method of this class from the UI thread.
|
||||
* That way, all forwarded events will be handled on that thread.
|
||||
*/
|
||||
public class AsyncDownloadWrapper extends Handler {
|
||||
|
||||
private static final String TAG = "org.fdroid.fdroid.net.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;
|
||||
private static final String MSG_DATA = "data";
|
||||
|
||||
private Downloader downloader;
|
||||
private Listener listener;
|
||||
private DownloadThread downloadThread = null;
|
||||
|
||||
/**
|
||||
* Normally the listener would be provided using a setListener method.
|
||||
* However for the purposes of this async downloader, it doesn't make
|
||||
* sense to have an async task without any way to notify the outside
|
||||
* world about completion. Therefore, we require the listener as a
|
||||
* parameter to the constructor.
|
||||
*/
|
||||
public AsyncDownloadWrapper(Downloader downloader, Listener listener) {
|
||||
this.downloader = downloader;
|
||||
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();
|
||||
}
|
||||
|
||||
public void attemptCancel() {
|
||||
if (downloadThread != null) {
|
||||
downloadThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
* @param message
|
||||
*/
|
||||
public void handleMessage(Message message) {
|
||||
if (message.arg1 == MSG_PROGRESS) {
|
||||
Bundle data = message.getData();
|
||||
ProgressListener.Event event = data.getParcelable(MSG_DATA);
|
||||
listener.onProgress(event);
|
||||
} else if (message.arg1 == MSG_DOWNLOAD_COMPLETE) {
|
||||
listener.onDownloadComplete();
|
||||
} else if (message.arg1 == MSG_DOWNLOAD_CANCELLED) {
|
||||
listener.onDownloadCancelled();
|
||||
} else if (message.arg1 == MSG_ERROR) {
|
||||
listener.onErrorDownloading(message.getData().getString(MSG_DATA));
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener extends ProgressListener {
|
||||
public void onReceiveTotalDownloadSize(int size);
|
||||
public void onReceiveCacheTag(String cacheTag);
|
||||
public void onErrorDownloading(String localisedExceptionDetails);
|
||||
public void onDownloadComplete();
|
||||
public void onDownloadCancelled();
|
||||
}
|
||||
|
||||
private class DownloadThread extends Thread implements ProgressListener {
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
downloader.setProgressListener(this);
|
||||
downloader.download();
|
||||
sendMessage(MSG_DOWNLOAD_COMPLETE);
|
||||
} catch (InterruptedException e) {
|
||||
sendMessage(MSG_DOWNLOAD_CANCELLED);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, e.getMessage() + ": " + Log.getStackTraceString(e));
|
||||
Bundle data = new Bundle(1);
|
||||
data.putString(MSG_DATA, e.getLocalizedMessage());
|
||||
Message message = new Message();
|
||||
message.arg1 = MSG_ERROR;
|
||||
message.setData(data);
|
||||
AsyncDownloadWrapper.this.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(int messageType) {
|
||||
Message message = new Message();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,55 +1,86 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import android.content.*;
|
||||
import org.fdroid.fdroid.*;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
public class Downloader {
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
private static final String HEADER_IF_NONE_MATCH = "If-None-Match";
|
||||
private static final String HEADER_FIELD_ETAG = "ETag";
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
|
||||
private URL sourceUrl;
|
||||
public abstract class Downloader {
|
||||
|
||||
private static final String TAG = "org.fdroid.fdroid.net.Downloader";
|
||||
private OutputStream outputStream;
|
||||
|
||||
private ProgressListener progressListener = null;
|
||||
private ProgressListener.Event progressEvent = null;
|
||||
private String eTag = null;
|
||||
private final File outputFile;
|
||||
private HttpURLConnection connection;
|
||||
private int statusCode = -1;
|
||||
private Bundle eventData = null;
|
||||
private File outputFile;
|
||||
protected String cacheTag = null;
|
||||
|
||||
public static final String EVENT_PROGRESS = "downloadProgress";
|
||||
|
||||
public abstract InputStream inputStream() throws IOException;
|
||||
|
||||
// The context is required for opening the file to write to.
|
||||
public Downloader(String source, String destFile, Context ctx)
|
||||
public Downloader(String destFile, Context ctx)
|
||||
throws FileNotFoundException, MalformedURLException {
|
||||
sourceUrl = new URL(source);
|
||||
outputStream = ctx.openFileOutput(destFile, Context.MODE_PRIVATE);
|
||||
outputFile = new File(ctx.getFilesDir() + File.separator + destFile);
|
||||
this(new File(ctx.getFilesDir() + File.separator + destFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads to a temporary file, which *you must delete yourself when
|
||||
* you are done*.
|
||||
* @see org.fdroid.fdroid.net.Downloader#getFile()
|
||||
*/
|
||||
public Downloader(String source, Context ctx) throws IOException {
|
||||
// The context is required for opening the file to write to.
|
||||
public Downloader(Context ctx) throws IOException {
|
||||
this(File.createTempFile("dl-", "", ctx.getCacheDir()));
|
||||
}
|
||||
|
||||
public Downloader(File destFile)
|
||||
throws FileNotFoundException, MalformedURLException {
|
||||
// http://developer.android.com/guide/topics/data/data-storage.html#InternalCache
|
||||
outputFile = File.createTempFile("dl-", "", ctx.getCacheDir());
|
||||
outputFile = destFile;
|
||||
outputStream = new FileOutputStream(outputFile);
|
||||
sourceUrl = new URL(source);
|
||||
}
|
||||
|
||||
public Downloader(String source, OutputStream output)
|
||||
public Downloader(OutputStream output)
|
||||
throws MalformedURLException {
|
||||
sourceUrl = new URL(source);
|
||||
outputStream = output;
|
||||
outputFile = null;
|
||||
}
|
||||
|
||||
public void setProgressListener(ProgressListener progressListener,
|
||||
ProgressListener.Event progressEvent) {
|
||||
this.progressListener = progressListener;
|
||||
this.progressEvent = progressEvent;
|
||||
public void setProgressListener(ProgressListener listener) {
|
||||
setProgressListener(listener, null);
|
||||
}
|
||||
|
||||
public void setProgressListener(ProgressListener listener, Bundle eventData) {
|
||||
this.progressListener = listener;
|
||||
this.eventData = eventData;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you ask for the cacheTag before calling download(), you will get the
|
||||
* same one you passed in (if any). If you call it after download(), you
|
||||
* will get the new cacheTag from the server, or null if there was none.
|
||||
*/
|
||||
public String getCacheTag() {
|
||||
return cacheTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this cacheTag matches that returned by the server, then no download will
|
||||
* take place, and a status code of 304 will be returned by download().
|
||||
*/
|
||||
public void setCacheTag(String cacheTag) {
|
||||
this.cacheTag = cacheTag;
|
||||
}
|
||||
|
||||
protected boolean wantToCheckCache() {
|
||||
return cacheTag != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,82 +92,103 @@ public class Downloader {
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available after downloading a file.
|
||||
*/
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
public abstract boolean hasChanged();
|
||||
|
||||
public abstract int totalDownloadSize();
|
||||
|
||||
/**
|
||||
* If you ask for the eTag before calling download(), you will get the
|
||||
* same one you passed in (if any). If you call it after download(), you
|
||||
* will get the new eTag from the server, or null if there was none.
|
||||
* Helper function for synchronous downloads (i.e. those *not* using AsyncDownloadWrapper),
|
||||
* which don't really want to bother dealing with an InterruptedException.
|
||||
* The InterruptedException thrown from download() is there to enable cancelling asynchronous
|
||||
* downloads, but regular synchronous downloads cannot be cancelled because download() will
|
||||
* block until completed.
|
||||
* @throws IOException
|
||||
*/
|
||||
public String getETag() {
|
||||
return eTag;
|
||||
public void downloadUninterrupted() throws IOException {
|
||||
try {
|
||||
download();
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this eTag matches that returned by the server, then no download will
|
||||
* take place, and a status code of 304 will be returned by download().
|
||||
*/
|
||||
public void setETag(String eTag) {
|
||||
this.eTag = eTag;
|
||||
}
|
||||
public abstract void download() throws IOException, InterruptedException;
|
||||
|
||||
// Get a remote file. Returns the HTTP response code.
|
||||
// If 'etag' is not null, it's passed to the server as an If-None-Match
|
||||
// header, in which case expect a 304 response if nothing changed.
|
||||
// In the event of a 200 response ONLY, 'retag' (which should be passed
|
||||
// empty) may contain an etag value for the response, or it may be left
|
||||
// empty if none was available.
|
||||
public int download() throws IOException {
|
||||
connection = (HttpURLConnection)sourceUrl.openConnection();
|
||||
setupCacheCheck();
|
||||
statusCode = connection.getResponseCode();
|
||||
if (statusCode == 200) {
|
||||
setupProgressListener();
|
||||
public abstract boolean isCached();
|
||||
|
||||
protected void downloadFromStream() throws IOException, InterruptedException {
|
||||
Log.d(TAG, "Downloading from stream");
|
||||
InputStream input = null;
|
||||
try {
|
||||
input = connection.getInputStream();
|
||||
Utils.copy(input, outputStream,
|
||||
progressListener, progressEvent);
|
||||
input = inputStream();
|
||||
|
||||
// Getting the input stream is slow(ish) for HTTP downloads, so we'll check if
|
||||
// we were interrupted before proceeding to the download.
|
||||
throwExceptionIfInterrupted();
|
||||
|
||||
copyInputToOutputStream(inputStream());
|
||||
} finally {
|
||||
Utils.closeQuietly(outputStream);
|
||||
Utils.closeQuietly(input);
|
||||
}
|
||||
updateCacheCheck();
|
||||
}
|
||||
return statusCode;
|
||||
|
||||
// Even if we have completely downloaded the file, we should probably respect
|
||||
// the wishes of the user who wanted to cancel us.
|
||||
throwExceptionIfInterrupted();
|
||||
}
|
||||
|
||||
protected void setupCacheCheck() {
|
||||
if (eTag != null) {
|
||||
connection.setRequestProperty(HEADER_IF_NONE_MATCH, eTag);
|
||||
/**
|
||||
* In a synchronous download (the usual usage of the Downloader interface),
|
||||
* you will not be able to interrupt this because the thread will block
|
||||
* after you have called download(). However if you use the AsyncDownloadWrapper,
|
||||
* then it will use this mechanism to cancel the download.
|
||||
*
|
||||
* After every network operation that could take a while, we will check if an
|
||||
* interrupt occured during that blocking operation. The goal is to ensure we
|
||||
* don't move onto another slow, network operation if we have cancelled the
|
||||
* download.
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
private void throwExceptionIfInterrupted() throws InterruptedException {
|
||||
if (Thread.interrupted()) {
|
||||
Log.d(TAG, "Received interrupt, cancelling download");
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateCacheCheck() {
|
||||
eTag = connection.getHeaderField(HEADER_FIELD_ETAG);
|
||||
protected void copyInputToOutputStream(InputStream input) throws IOException, InterruptedException {
|
||||
|
||||
byte[] buffer = new byte[Utils.BUFFER_SIZE];
|
||||
int bytesRead = 0;
|
||||
int 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.
|
||||
throwExceptionIfInterrupted();
|
||||
|
||||
sendProgress(bytesRead, totalBytes);
|
||||
while (true) {
|
||||
|
||||
int count = input.read(buffer);
|
||||
throwExceptionIfInterrupted();
|
||||
|
||||
bytesRead += count;
|
||||
sendProgress(bytesRead, totalBytes);
|
||||
if (count == -1) {
|
||||
Log.d(TAG, "Finished downloading from stream");
|
||||
break;
|
||||
}
|
||||
outputStream.write(buffer, 0, count);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
protected void setupProgressListener() {
|
||||
if (progressListener != null && progressEvent != null) {
|
||||
// Testing in the emulator for me, showed that figuring out the
|
||||
// filesize took about 1 to 1.5 seconds.
|
||||
// To put this in context, downloading a repo of:
|
||||
// - 400k takes ~6 seconds
|
||||
// - 5k takes ~3 seconds
|
||||
// on my connection. I think the 1/1.5 seconds is worth it,
|
||||
// because as the repo grows, the tradeoff will
|
||||
// become more worth it.
|
||||
progressEvent.total = connection.getContentLength();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasChanged() {
|
||||
return this.statusCode == 200;
|
||||
}
|
||||
|
||||
}
|
||||
|
128
src/org/fdroid/fdroid/net/HttpDownloader.java
Normal file
128
src/org/fdroid/fdroid/net/HttpDownloader.java
Normal file
@ -0,0 +1,128 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
public class HttpDownloader extends Downloader {
|
||||
private static final String TAG = "org.fdroid.fdroid.net.HttpDownloader";
|
||||
|
||||
private static final String HEADER_IF_NONE_MATCH = "If-None-Match";
|
||||
private static final String HEADER_FIELD_ETAG = "ETag";
|
||||
|
||||
private URL sourceUrl;
|
||||
private HttpURLConnection connection;
|
||||
private int statusCode = -1;
|
||||
|
||||
// The context is required for opening the file to write to.
|
||||
public HttpDownloader(String source, String destFile, Context ctx)
|
||||
throws FileNotFoundException, MalformedURLException {
|
||||
super(destFile, ctx);
|
||||
sourceUrl = new URL(source);
|
||||
}
|
||||
|
||||
// The context is required for opening the file to write to.
|
||||
public HttpDownloader(String source, File destFile)
|
||||
throws FileNotFoundException, MalformedURLException {
|
||||
super(destFile);
|
||||
sourceUrl = new URL(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads to a temporary file, which *you must delete yourself when
|
||||
* you are done*.
|
||||
* @see org.fdroid.fdroid.net.HttpDownloader#getFile()
|
||||
*/
|
||||
public HttpDownloader(String source, Context ctx) throws IOException {
|
||||
super(ctx);
|
||||
sourceUrl = new URL(source);
|
||||
}
|
||||
|
||||
public HttpDownloader(String source, OutputStream output)
|
||||
throws MalformedURLException {
|
||||
super(output);
|
||||
sourceUrl = new URL(source);
|
||||
}
|
||||
|
||||
public InputStream inputStream() throws IOException {
|
||||
return connection.getInputStream();
|
||||
}
|
||||
|
||||
// Get a remote file. Returns the HTTP response code.
|
||||
// If 'etag' is not null, it's passed to the server as an If-None-Match
|
||||
// header, in which case expect a 304 response if nothing changed.
|
||||
// In the event of a 200 response ONLY, 'retag' (which should be passed
|
||||
// empty) may contain an etag value for the response, or it may be left
|
||||
// empty if none was available.
|
||||
@Override
|
||||
public void download() throws IOException, InterruptedException {
|
||||
try {
|
||||
connection = (HttpURLConnection)sourceUrl.openConnection();
|
||||
|
||||
if (wantToCheckCache()) {
|
||||
setupCacheCheck();
|
||||
Log.i(TAG, "Checking cached status of " + sourceUrl);
|
||||
statusCode = connection.getResponseCode();
|
||||
}
|
||||
|
||||
if (isCached()) {
|
||||
Log.i(TAG, sourceUrl + " is cached, so not downloading (HTTP " + statusCode + ")");
|
||||
} else {
|
||||
Log.i(TAG, "Downloading from " + sourceUrl);
|
||||
downloadFromStream();
|
||||
updateCacheCheck();
|
||||
}
|
||||
} catch (SSLHandshakeException e) {
|
||||
// TODO this should be handled better, it is not internationalised here.
|
||||
throw new IOException(
|
||||
"A problem occurred while establishing an SSL " +
|
||||
"connection. If this problem persists, AND you have a " +
|
||||
"very old device, you could try using http instead of " +
|
||||
"https for the repo URL." + Log.getStackTraceString(e) );
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCached() {
|
||||
return wantToCheckCache() && statusCode == 304;
|
||||
}
|
||||
|
||||
private void setupCacheCheck() {
|
||||
if (cacheTag != null) {
|
||||
connection.setRequestProperty(HEADER_IF_NONE_MATCH, cacheTag);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCacheCheck() {
|
||||
cacheTag = connection.getHeaderField(HEADER_FIELD_ETAG);
|
||||
}
|
||||
|
||||
// Testing in the emulator for me, showed that figuring out the
|
||||
// filesize took about 1 to 1.5 seconds.
|
||||
// To put this in context, downloading a repo of:
|
||||
// - 400k takes ~6 seconds
|
||||
// - 5k takes ~3 seconds
|
||||
// on my connection. I think the 1/1.5 seconds is worth it,
|
||||
// because as the repo grows, the tradeoff will
|
||||
// become more worth it.
|
||||
@Override
|
||||
public int totalDownloadSize() {
|
||||
return connection.getContentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChanged() {
|
||||
return this.statusCode != 304;
|
||||
}
|
||||
|
||||
}
|
@ -4,6 +4,7 @@ import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.RepoXMLHandler;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
@ -12,24 +13,29 @@ import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.fdroid.fdroid.net.HttpDownloader;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.XMLReader;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
import java.io.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
|
||||
abstract public class RepoUpdater {
|
||||
|
||||
public static final int PROGRESS_TYPE_DOWNLOAD = 1;
|
||||
public static final int PROGRESS_TYPE_PROCESS_XML = 2;
|
||||
public static final String PROGRESS_DATA_REPO = "repo";
|
||||
public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml";
|
||||
|
||||
public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress";
|
||||
|
||||
public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) {
|
||||
if (repo.fingerprint == null && repo.pubkey == null) {
|
||||
@ -68,10 +74,6 @@ abstract public class RepoUpdater {
|
||||
return apks;
|
||||
}
|
||||
|
||||
public boolean isInteractive() {
|
||||
return progressListener != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* For example, you may want to unzip a jar file to get the index inside,
|
||||
* or if the file is not compressed, you can just return a reference to
|
||||
@ -85,47 +87,26 @@ abstract public class RepoUpdater {
|
||||
protected abstract String getIndexAddress();
|
||||
|
||||
protected Downloader downloadIndex() throws UpdateException {
|
||||
Bundle progressData = createProgressData(repo.address);
|
||||
Downloader downloader = null;
|
||||
try {
|
||||
downloader = new Downloader(getIndexAddress(), context);
|
||||
downloader.setETag(repo.lastetag);
|
||||
downloader = new HttpDownloader(getIndexAddress(), context);
|
||||
downloader.setCacheTag(repo.lastetag);
|
||||
|
||||
if (isInteractive()) {
|
||||
ProgressListener.Event event =
|
||||
new ProgressListener.Event(
|
||||
RepoUpdater.PROGRESS_TYPE_DOWNLOAD, progressData);
|
||||
downloader.setProgressListener(progressListener, event);
|
||||
if (progressListener != null) { // interactive session, show progress
|
||||
Bundle data = new Bundle(1);
|
||||
data.putString(PROGRESS_DATA_REPO_ADDRESS, repo.address);
|
||||
downloader.setProgressListener(progressListener, data);
|
||||
}
|
||||
|
||||
int status = downloader.download();
|
||||
downloader.downloadUninterrupted();
|
||||
|
||||
if (status == 304) {
|
||||
if (downloader.isCached()) {
|
||||
// The index is unchanged since we last read it. We just mark
|
||||
// everything that came from this repo as being updated.
|
||||
Log.d("FDroid", "Repo index for " + repo.address
|
||||
Log.d("FDroid", "Repo index for " + getIndexAddress()
|
||||
+ " is up to date (by etag)");
|
||||
} else if (status == 200) {
|
||||
// Nothing needed to be done here...
|
||||
} else {
|
||||
// Is there any code other than 200 which still returns
|
||||
// content? Just in case, lets try to clean up.
|
||||
if (downloader.getFile() != null) {
|
||||
downloader.getFile().delete();
|
||||
}
|
||||
throw new UpdateException(
|
||||
repo,
|
||||
"Failed to update repo " + repo.address +
|
||||
" - HTTP response " + status);
|
||||
}
|
||||
} catch (SSLHandshakeException e) {
|
||||
throw new UpdateException(
|
||||
repo,
|
||||
"A problem occurred while establishing an SSL " +
|
||||
"connection. If this problem persists, AND you have a " +
|
||||
"very old device, you could try using http instead of " +
|
||||
"https for the repo URL.",
|
||||
e );
|
||||
|
||||
} catch (IOException e) {
|
||||
if (downloader != null && downloader.getFile() != null) {
|
||||
downloader.getFile().delete();
|
||||
@ -138,12 +119,6 @@ abstract public class RepoUpdater {
|
||||
return downloader;
|
||||
}
|
||||
|
||||
public static Bundle createProgressData(String repoAddress) {
|
||||
Bundle data = new Bundle();
|
||||
data.putString(PROGRESS_DATA_REPO, repoAddress);
|
||||
return data;
|
||||
}
|
||||
|
||||
private int estimateAppCount(File indexFile) {
|
||||
int count = -1;
|
||||
try {
|
||||
@ -182,7 +157,7 @@ abstract public class RepoUpdater {
|
||||
XMLReader reader = parser.getXMLReader();
|
||||
RepoXMLHandler handler = new RepoXMLHandler(repo, progressListener);
|
||||
|
||||
if (isInteractive()) {
|
||||
if (progressListener != null) {
|
||||
// Only bother spending the time to count the expected apps
|
||||
// if we can show that to the user...
|
||||
handler.setTotalAppCount(estimateAppCount(indexFile));
|
||||
@ -195,7 +170,7 @@ abstract public class RepoUpdater {
|
||||
reader.parse(is);
|
||||
apps = handler.getApps();
|
||||
apks = handler.getApks();
|
||||
updateRepo(handler, downloader.getETag());
|
||||
updateRepo(handler, downloader.getCacheTag());
|
||||
}
|
||||
} catch (SAXException e) {
|
||||
throw new UpdateException(
|
||||
|
@ -203,7 +203,7 @@ public class RepoDetailsFragment extends Fragment {
|
||||
UpdateService.updateRepoNow(repo.address, getActivity()).setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(Event event) {
|
||||
if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
|
||||
if (event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) {
|
||||
repo = loadRepoDetails();
|
||||
updateView((ViewGroup)getView());
|
||||
}
|
||||
|
@ -218,8 +218,8 @@ public class RepoListFragment extends ListFragment
|
||||
UpdateService.updateNow(getActivity()).setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(Event event) {
|
||||
if (event.type == UpdateService.STATUS_COMPLETE_AND_SAME ||
|
||||
event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
|
||||
if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME) ||
|
||||
event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) {
|
||||
// No need to prompt to update any more, we just did it!
|
||||
changed = false;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user