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