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
 | 
					/build.xml
 | 
				
			||||||
*~
 | 
					*~
 | 
				
			||||||
/.idea/
 | 
					/.idea/
 | 
				
			||||||
/*.iml
 | 
					*.iml
 | 
				
			||||||
out
 | 
					out
 | 
				
			||||||
/.settings/
 | 
					/.settings/
 | 
				
			||||||
 | 
				
			|||||||
@ -19,31 +19,24 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
package org.fdroid.fdroid;
 | 
					package org.fdroid.fdroid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.content.*;
 | 
					import android.app.Activity;
 | 
				
			||||||
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.AlertDialog;
 | 
					import android.app.AlertDialog;
 | 
				
			||||||
import android.app.ListActivity;
 | 
					import android.app.ListActivity;
 | 
				
			||||||
import android.app.ProgressDialog;
 | 
					import android.app.ProgressDialog;
 | 
				
			||||||
import android.bluetooth.BluetoothAdapter;
 | 
					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.net.Uri;
 | 
				
			||||||
import android.os.Bundle;
 | 
					import android.os.Bundle;
 | 
				
			||||||
import android.os.Handler;
 | 
					import android.os.Handler;
 | 
				
			||||||
import android.os.Message;
 | 
					 | 
				
			||||||
import android.preference.PreferenceManager;
 | 
					import android.preference.PreferenceManager;
 | 
				
			||||||
import android.support.v4.app.NavUtils;
 | 
					import android.support.v4.app.NavUtils;
 | 
				
			||||||
import android.support.v4.view.MenuItemCompat;
 | 
					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.Editable;
 | 
				
			||||||
import android.text.Html;
 | 
					import android.text.Html;
 | 
				
			||||||
import android.text.Html.TagHandler;
 | 
					import android.text.Html.TagHandler;
 | 
				
			||||||
@ -58,24 +51,34 @@ import android.view.SubMenu;
 | 
				
			|||||||
import android.view.View;
 | 
					import android.view.View;
 | 
				
			||||||
import android.view.ViewGroup;
 | 
					import android.view.ViewGroup;
 | 
				
			||||||
import android.view.Window;
 | 
					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.DisplayImageOptions;
 | 
				
			||||||
import com.nostra13.universalimageloader.core.ImageLoader;
 | 
					import com.nostra13.universalimageloader.core.ImageLoader;
 | 
				
			||||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
 | 
					import com.nostra13.universalimageloader.core.assist.ImageScaleType;
 | 
				
			||||||
 | 
					 | 
				
			||||||
import org.fdroid.fdroid.Utils.CommaSeparatedList;
 | 
					import org.fdroid.fdroid.Utils.CommaSeparatedList;
 | 
				
			||||||
import org.fdroid.fdroid.compat.ActionBarCompat;
 | 
					import org.fdroid.fdroid.compat.ActionBarCompat;
 | 
				
			||||||
import org.fdroid.fdroid.compat.MenuManager;
 | 
					import org.fdroid.fdroid.compat.MenuManager;
 | 
				
			||||||
import org.fdroid.fdroid.compat.PackageManagerCompat;
 | 
					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.io.File;
 | 
				
			||||||
import java.security.NoSuchAlgorithmException;
 | 
					import java.security.NoSuchAlgorithmException;
 | 
				
			||||||
import java.util.Iterator;
 | 
					import java.util.Iterator;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class AppDetails extends ListActivity {
 | 
					public class AppDetails extends ListActivity implements ProgressListener {
 | 
				
			||||||
    private static final String TAG = "AppDetails";
 | 
					    private static final String TAG = "org.fdroid.fdroid.AppDetails";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static final int REQUEST_ENABLE_BLUETOOTH = 2;
 | 
					    public static final int REQUEST_ENABLE_BLUETOOTH = 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -84,6 +87,7 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private FDroidApp fdroidApp;
 | 
					    private FDroidApp fdroidApp;
 | 
				
			||||||
    private ApkListAdapter adapter;
 | 
					    private ApkListAdapter adapter;
 | 
				
			||||||
 | 
					    private ProgressDialog progressDialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static class ViewHolder {
 | 
					    private static class ViewHolder {
 | 
				
			||||||
        TextView version;
 | 
					        TextView version;
 | 
				
			||||||
@ -98,7 +102,7 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    // observer to update view when package has been installed/deleted
 | 
					    // observer to update view when package has been installed/deleted
 | 
				
			||||||
    AppObserver myAppObserver;
 | 
					    AppObserver myAppObserver;
 | 
				
			||||||
    class AppObserver extends ContentObserver {      
 | 
					    class AppObserver extends ContentObserver {
 | 
				
			||||||
       public AppObserver(Handler handler) {
 | 
					       public AppObserver(Handler handler) {
 | 
				
			||||||
          super(handler);           
 | 
					          super(handler);           
 | 
				
			||||||
       }
 | 
					       }
 | 
				
			||||||
@ -110,7 +114,7 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
       @Override
 | 
					       @Override
 | 
				
			||||||
       public void onChange(boolean selfChange, Uri uri) {
 | 
					       public void onChange(boolean selfChange, Uri uri) {
 | 
				
			||||||
           if (!reset()) {
 | 
					           if (!reset(app.id)) {
 | 
				
			||||||
               AppDetails.this.finish();
 | 
					               AppDetails.this.finish();
 | 
				
			||||||
               return;
 | 
					               return;
 | 
				
			||||||
           }
 | 
					           }
 | 
				
			||||||
@ -267,10 +271,8 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
    private static final int SEND_VIA_BLUETOOTH = Menu.FIRST + 15;
 | 
					    private static final int SEND_VIA_BLUETOOTH = Menu.FIRST + 15;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private App app;
 | 
					    private App app;
 | 
				
			||||||
    private String appid;
 | 
					 | 
				
			||||||
    private PackageManager mPm;
 | 
					    private PackageManager mPm;
 | 
				
			||||||
    private DownloadHandler downloadHandler;
 | 
					    private ApkDownloader downloadHandler;
 | 
				
			||||||
    private boolean stateRetained;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private boolean startingIgnoreAll;
 | 
					    private boolean startingIgnoreAll;
 | 
				
			||||||
    private int startingIgnoreThis;
 | 
					    private int startingIgnoreThis;
 | 
				
			||||||
@ -282,6 +284,68 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
    private DisplayImageOptions displayImageOptions;
 | 
					    private DisplayImageOptions displayImageOptions;
 | 
				
			||||||
    private Installer installer;
 | 
					    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
 | 
					    @Override
 | 
				
			||||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
 | 
					        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
 | 
				
			||||||
@ -307,43 +371,27 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
        // for reason why.
 | 
					        // for reason why.
 | 
				
			||||||
        ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
 | 
					        ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Intent i = getIntent();
 | 
					        if (getIntent().hasExtra(EXTRA_FROM)) {
 | 
				
			||||||
        Uri data = i.getData();
 | 
					            setTitle(getIntent().getStringExtra(EXTRA_FROM));
 | 
				
			||||||
        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));
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        mPm = getPackageManager();
 | 
					        mPm = getPackageManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        installer = Installer.getActivityInstaller(this, mPm,
 | 
					        installer = Installer.getActivityInstaller(this, mPm,
 | 
				
			||||||
                myInstallerCallback);
 | 
					                myInstallerCallback);
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Get the preferences we're going to use in this Activity...
 | 
					        // Get the preferences we're going to use in this Activity...
 | 
				
			||||||
        AppDetails old = (AppDetails) getLastNonConfigurationInstance();
 | 
					        ConfigurationChangeHelper previousData = (ConfigurationChangeHelper)getLastNonConfigurationInstance();
 | 
				
			||||||
        if (old != null) {
 | 
					        if (previousData != null) {
 | 
				
			||||||
            copyState(old);
 | 
					            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 {
 | 
					        } else {
 | 
				
			||||||
            if (!reset()) {
 | 
					            if (!reset(getAppIdFromIntent())) {
 | 
				
			||||||
                finish();
 | 
					                finish();
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -377,7 +425,6 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    protected void onResume() {
 | 
					    protected void onResume() {
 | 
				
			||||||
        Log.d(TAG, "onresume");
 | 
					 | 
				
			||||||
        super.onResume();
 | 
					        super.onResume();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // register observer to know when install status changes
 | 
					        // register observer to know when install status changes
 | 
				
			||||||
@ -386,17 +433,45 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
              AppProvider.getContentUri(app.id),
 | 
					              AppProvider.getContentUri(app.id),
 | 
				
			||||||
              true,
 | 
					              true,
 | 
				
			||||||
              myAppObserver);
 | 
					              myAppObserver);
 | 
				
			||||||
        
 | 
					        if (downloadHandler != null) {
 | 
				
			||||||
        if (!reset()) {
 | 
					            if (downloadHandler.isComplete()) {
 | 
				
			||||||
            finish();
 | 
					                downloadCompleteInstallApk();
 | 
				
			||||||
            return;
 | 
					            } else {
 | 
				
			||||||
 | 
					                downloadHandler.setProgressListener(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Show the progress dialog, if for no other reason than to prevent them attempting
 | 
				
			||||||
 | 
					                // to download again (i.e. we force them to touch 'cancel' before they can access
 | 
				
			||||||
 | 
					                // the rest of the activity).
 | 
				
			||||||
 | 
					                Log.d(TAG, "Showing dialog to user after resuming app details view, because a download was previously in progress");
 | 
				
			||||||
 | 
					                updateProgressDialog();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        updateViews();
 | 
					        updateViews();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        MenuManager.create(this).invalidateOptionsMenu();
 | 
					        MenuManager.create(this).invalidateOptionsMenu();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Remove progress listener, suppress progress dialog, set downloadHandler to null.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void cleanUpFinishedDownload() {
 | 
				
			||||||
        if (downloadHandler != null) {
 | 
					        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) {
 | 
					        if (myAppObserver != null) {
 | 
				
			||||||
            getContentResolver().unregisterContentObserver(myAppObserver);
 | 
					            getContentResolver().unregisterContentObserver(myAppObserver);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (downloadHandler != null) {
 | 
					 | 
				
			||||||
            downloadHandler.stopUpdates();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
 | 
					        if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
 | 
				
			||||||
                || app.ignoreThisUpdate != startingIgnoreThis)) {
 | 
					                || 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);
 | 
					            setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (downloadHandler != null) {
 | 
				
			||||||
 | 
					            downloadHandler.removeProgressListener();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        removeProgressDialog();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        super.onPause();
 | 
					        super.onPause();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -430,65 +510,73 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public Object onRetainNonConfigurationInstance() {
 | 
					    public Object onRetainNonConfigurationInstance() {
 | 
				
			||||||
        stateRetained = true;
 | 
					        inProcessOfChangingConfiguration = true;
 | 
				
			||||||
        return this;
 | 
					        return new ConfigurationChangeHelper(downloadHandler, app);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    protected void onDestroy() {
 | 
					    protected void onDestroy() {
 | 
				
			||||||
        if (downloadHandler != null) {
 | 
					        if (downloadHandler != null) {
 | 
				
			||||||
            if (!stateRetained)
 | 
					            if (!inProcessOfChangingConfiguration) {
 | 
				
			||||||
                downloadHandler.cancel();
 | 
					                downloadHandler.cancel();
 | 
				
			||||||
            downloadHandler.destroy();
 | 
					                cleanUpFinishedDownload();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        inProcessOfChangingConfiguration = false;
 | 
				
			||||||
        super.onDestroy();
 | 
					        super.onDestroy();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Copy all relevant state from an old instance. This is used in
 | 
					    private void removeProgressDialog() {
 | 
				
			||||||
    // place of reset(), so it must initialize all fields normally set
 | 
					        if (progressDialog != null) {
 | 
				
			||||||
    // there.
 | 
					            progressDialog.dismiss();
 | 
				
			||||||
    private void copyState(AppDetails old) {
 | 
					            progressDialog = null;
 | 
				
			||||||
        if (old.downloadHandler != null)
 | 
					        }
 | 
				
			||||||
            downloadHandler = new DownloadHandler(old.downloadHandler);
 | 
					 | 
				
			||||||
        app = old.app;
 | 
					 | 
				
			||||||
        mInstalledSignature = old.mInstalledSignature;
 | 
					 | 
				
			||||||
        mInstalledSigID = old.mInstalledSigID;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Reset the display and list contents. Used when entering the activity, and
 | 
					    // Reset the display and list contents. Used when entering the activity, and
 | 
				
			||||||
    // also when something has been installed/uninstalled.
 | 
					    // also when something has been installed/uninstalled.
 | 
				
			||||||
    // Return true if the app was found, false otherwise.
 | 
					    // Return true if the app was found, false otherwise.
 | 
				
			||||||
    private boolean reset() {
 | 
					    private boolean reset(String appId) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Log.d("FDroid", "Getting application details for " + appid);
 | 
					        Log.d("FDroid", "Getting application details for " + appId);
 | 
				
			||||||
        app = null;
 | 
					        App newApp = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (appid != null && appid.length() > 0) {
 | 
					        if (appId != null && appId.length() > 0) {
 | 
				
			||||||
            app = AppProvider.Helper.findById(getContentResolver(), appid);
 | 
					            newApp = AppProvider.Helper.findById(getContentResolver(), appId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (app == null) {
 | 
					        setApp(newApp);
 | 
				
			||||||
            Toast toast = Toast.makeText(this,
 | 
					
 | 
				
			||||||
                    getString(R.string.no_such_app), Toast.LENGTH_LONG);
 | 
					        return this.app != null;
 | 
				
			||||||
            toast.show();
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 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();
 | 
					            finish();
 | 
				
			||||||
            return false;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        app = newApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        startingIgnoreAll = app.ignoreAllUpdates;
 | 
					        startingIgnoreAll = app.ignoreAllUpdates;
 | 
				
			||||||
        startingIgnoreThis = app.ignoreThisUpdate;
 | 
					        startingIgnoreThis = app.ignoreThisUpdate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Get the signature of the installed package...
 | 
					        // Get the signature of the installed package...
 | 
				
			||||||
        mInstalledSignature = null;
 | 
					        mInstalledSignature = null;
 | 
				
			||||||
        mInstalledSigID = null;
 | 
					        mInstalledSigID = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (app.isInstalled()) {
 | 
					        if (app.isInstalled()) {
 | 
				
			||||||
            PackageManager pm = getBaseContext().getPackageManager();
 | 
					            PackageManager pm = getPackageManager();
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                PackageInfo pi = pm.getPackageInfo(appid,
 | 
					                PackageInfo pi = pm.getPackageInfo(app.id, PackageManager.GET_SIGNATURES);
 | 
				
			||||||
                        PackageManager.GET_SIGNATURES);
 | 
					 | 
				
			||||||
                mInstalledSignature = pi.signatures[0];
 | 
					                mInstalledSignature = pi.signatures[0];
 | 
				
			||||||
                Hasher hash = new Hasher("MD5", mInstalledSignature
 | 
					                Hasher hash = new Hasher("MD5", mInstalledSignature.toCharsString().getBytes());
 | 
				
			||||||
                        .toCharsString().getBytes());
 | 
					 | 
				
			||||||
                mInstalledSigID = hash.getHash();
 | 
					                mInstalledSigID = hash.getHash();
 | 
				
			||||||
            } catch (NameNotFoundException e) {
 | 
					            } catch (NameNotFoundException e) {
 | 
				
			||||||
                Log.d("FDroid", "Failed to get installed signature");
 | 
					                Log.d("FDroid", "Failed to get installed signature");
 | 
				
			||||||
@ -497,7 +585,6 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
                mInstalledSignature = null;
 | 
					                mInstalledSignature = null;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void startViews() {
 | 
					    private void startViews() {
 | 
				
			||||||
@ -511,10 +598,10 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
        headerView.removeAllViews();
 | 
					        headerView.removeAllViews();
 | 
				
			||||||
        if (landparent != null) {
 | 
					        if (landparent != null) {
 | 
				
			||||||
            landparent.addView(infoView);
 | 
					            landparent.addView(infoView);
 | 
				
			||||||
            Log.d("FDroid", "Setting landparent infoview");
 | 
					            Log.d("FDroid", "Setting up landscape view");
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            headerView.addView(infoView);
 | 
					            headerView.addView(infoView);
 | 
				
			||||||
            Log.d("FDroid", "Setting header infoview");
 | 
					            Log.d("FDroid", "Setting up portrait view");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Set the icon...
 | 
					        // Set the icon...
 | 
				
			||||||
@ -610,8 +697,7 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
                        if (permissionName.equals("ACCESS_SUPERUSER")) {
 | 
					                        if (permissionName.equals("ACCESS_SUPERUSER")) {
 | 
				
			||||||
                            sb.append("\t• Full permissions to all device features and storage\n");
 | 
					                            sb.append("\t• Full permissions to all device features and storage\n");
 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
                            Log.d("FDroid", "Permission not yet available: "
 | 
					                            Log.d("FDroid", "Permission not yet available: " + permissionName);
 | 
				
			||||||
                                    +permissionName);
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@ -912,6 +998,7 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Install the version of this app denoted by 'app.curApk'.
 | 
					    // Install the version of this app denoted by 'app.curApk'.
 | 
				
			||||||
    private void install(final Apk apk) {
 | 
					    private void install(final Apk apk) {
 | 
				
			||||||
 | 
					        final Activity activity = this;
 | 
				
			||||||
        String [] projection = { RepoProvider.DataColumns.ADDRESS };
 | 
					        String [] projection = { RepoProvider.DataColumns.ADDRESS };
 | 
				
			||||||
        Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
 | 
					        Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
 | 
				
			||||||
        if (repo == null || repo.address == null) {
 | 
					        if (repo == null || repo.address == null) {
 | 
				
			||||||
@ -926,10 +1013,8 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
                    new DialogInterface.OnClickListener() {
 | 
					                    new DialogInterface.OnClickListener() {
 | 
				
			||||||
                        @Override
 | 
					                        @Override
 | 
				
			||||||
                        public void onClick(DialogInterface dialog,
 | 
					                        public void onClick(DialogInterface dialog,
 | 
				
			||||||
                                int whichButton) {
 | 
					                            int whichButton) {
 | 
				
			||||||
                            downloadHandler = new DownloadHandler(apk,
 | 
					                            startDownload(apk, repoaddress);
 | 
				
			||||||
                                    repoaddress, Utils
 | 
					 | 
				
			||||||
                                    .getApkCacheDir(getBaseContext()));
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
            ask_alrt.setNegativeButton(getString(R.string.no),
 | 
					            ask_alrt.setNegativeButton(getString(R.string.no),
 | 
				
			||||||
@ -958,9 +1043,17 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
            alert.show();
 | 
					            alert.show();
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        downloadHandler = new DownloadHandler(apk, repoaddress,
 | 
					        startDownload(apk, repoaddress);
 | 
				
			||||||
                Utils.getApkCacheDir(getBaseContext()));
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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) {
 | 
					    private void installApk(File file, String packageName) {
 | 
				
			||||||
        setProgressBarIndeterminateVisibility(true);
 | 
					        setProgressBarIndeterminateVisibility(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -989,10 +1082,6 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
                @Override
 | 
					                @Override
 | 
				
			||||||
                public void run() {                    
 | 
					                public void run() {                    
 | 
				
			||||||
                    if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
 | 
					                    if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
 | 
				
			||||||
                        if (downloadHandler != null) {
 | 
					 | 
				
			||||||
                            downloadHandler = null;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        PackageManagerCompat.setInstaller(mPm, app.id);
 | 
					                        PackageManagerCompat.setInstaller(mPm, app.id);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1039,137 +1128,115 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
        shareIntent.setType("text/plain");
 | 
					        shareIntent.setType("text/plain");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        shareIntent.putExtra(Intent.EXTRA_SUBJECT, app.name);
 | 
					        shareIntent.putExtra(Intent.EXTRA_SUBJECT, app.name);
 | 
				
			||||||
        shareIntent.putExtra(Intent.EXTRA_TEXT, app.name+" ("+app.summary+") - https://f-droid.org/app/"+app.id);
 | 
					        shareIntent.putExtra(Intent.EXTRA_TEXT, app.name + " (" + app.summary + ") - https://f-droid.org/app/" + app.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share)));
 | 
					        startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share)));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private ProgressDialog createProgressDialog(String file, int p, int max) {
 | 
					    private ProgressDialog getProgressDialog(String file) {
 | 
				
			||||||
        final ProgressDialog pd = new ProgressDialog(this);
 | 
					        if (progressDialog == null) {
 | 
				
			||||||
        pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
 | 
					            final ProgressDialog pd = new ProgressDialog(this);
 | 
				
			||||||
        pd.setMessage(getString(R.string.download_server) + ":\n " + file);
 | 
					            pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
 | 
				
			||||||
        pd.setMax(max);
 | 
					            pd.setMessage(getString(R.string.download_server) + ":\n " + file);
 | 
				
			||||||
        pd.setProgress(p);
 | 
					            pd.setCancelable(true);
 | 
				
			||||||
        pd.setCancelable(true);
 | 
					            pd.setCanceledOnTouchOutside(false);
 | 
				
			||||||
        pd.setCanceledOnTouchOutside(false);
 | 
					
 | 
				
			||||||
        pd.setOnCancelListener(new DialogInterface.OnCancelListener() {
 | 
					            // The indeterminate-ness will get overridden on the first progress event we receive.
 | 
				
			||||||
            @Override
 | 
					            pd.setIndeterminate(true);
 | 
				
			||||||
            public void onCancel(DialogInterface dialog) {
 | 
					
 | 
				
			||||||
                downloadHandler.cancel();
 | 
					            pd.setOnCancelListener(new DialogInterface.OnCancelListener() {
 | 
				
			||||||
            }
 | 
					                @Override
 | 
				
			||||||
        });
 | 
					                public void onCancel(DialogInterface dialog) {
 | 
				
			||||||
        pd.setButton(DialogInterface.BUTTON_NEUTRAL,
 | 
					                    Log.d(TAG, "User clicked 'cancel' on download, attempting to interrupt download thread.");
 | 
				
			||||||
                getString(R.string.cancel),
 | 
					                    if (downloadHandler !=  null) {
 | 
				
			||||||
                new DialogInterface.OnClickListener() {
 | 
					                        downloadHandler.cancel();
 | 
				
			||||||
                    @Override
 | 
					                        cleanUpFinishedDownload();
 | 
				
			||||||
                    public void onClick(DialogInterface dialog, int which) {
 | 
					                    } else {
 | 
				
			||||||
                        pd.cancel();
 | 
					                        Log.e(TAG, "Tried to cancel, but the downloadHandler doesn't exist.");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                });
 | 
					                    progressDialog = null;
 | 
				
			||||||
        pd.show();
 | 
					                    Toast.makeText(AppDetails.this, getString(R.string.download_cancelled), Toast.LENGTH_LONG).show();
 | 
				
			||||||
        return pd;
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            pd.setButton(DialogInterface.BUTTON_NEUTRAL,
 | 
				
			||||||
 | 
					                    getString(R.string.cancel),
 | 
				
			||||||
 | 
					                    new DialogInterface.OnClickListener() {
 | 
				
			||||||
 | 
					                        @Override
 | 
				
			||||||
 | 
					                        public void onClick(DialogInterface dialog, int which) {
 | 
				
			||||||
 | 
					                            pd.cancel();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            progressDialog = pd;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return progressDialog;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Handler used to update the progress dialog while downloading.
 | 
					    /**
 | 
				
			||||||
    private class DownloadHandler extends Handler {
 | 
					     * Looks at the current <code>downloadHandler</code> and finds it's size and progress.
 | 
				
			||||||
        private Downloader download;
 | 
					     * This is in comparison to {@link org.fdroid.fdroid.AppDetails#updateProgressDialog(int, int)},
 | 
				
			||||||
        private ProgressDialog pd;
 | 
					     * which is used when you have the details from a freshly received
 | 
				
			||||||
        private boolean updating;
 | 
					     * {@link org.fdroid.fdroid.ProgressListener.Event}.
 | 
				
			||||||
        private String id;
 | 
					     */
 | 
				
			||||||
 | 
					    private void updateProgressDialog() {
 | 
				
			||||||
        public DownloadHandler(Apk apk, String repoaddress, File destdir) {
 | 
					        if (downloadHandler != null) {
 | 
				
			||||||
            id = apk.id;
 | 
					            updateProgressDialog(downloadHandler.getProgress(), downloadHandler.getTotalSize());
 | 
				
			||||||
            download = new Downloader(apk, repoaddress, destdir);
 | 
					 | 
				
			||||||
            download.start();
 | 
					 | 
				
			||||||
            startUpdates();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public DownloadHandler(DownloadHandler oldHandler) {
 | 
					    private void updateProgressDialog(int progress, int total) {
 | 
				
			||||||
            if (oldHandler != null) {
 | 
					        if (downloadHandler != null) {
 | 
				
			||||||
                download = oldHandler.download;
 | 
					            ProgressDialog pd = getProgressDialog(downloadHandler.getRemoteAddress());
 | 
				
			||||||
 | 
					            if (total > 0) {
 | 
				
			||||||
 | 
					                pd.setIndeterminate(false);
 | 
				
			||||||
 | 
					                pd.setProgress(progress);
 | 
				
			||||||
 | 
					                pd.setMax(total);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                pd.setIndeterminate(true);
 | 
				
			||||||
 | 
					                pd.setProgress(progress);
 | 
				
			||||||
 | 
					                pd.setMax(0);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            startUpdates();
 | 
					            if (!pd.isShowing()) {
 | 
				
			||||||
        }
 | 
					                Log.d(TAG, "Showing progress dialog for download.");
 | 
				
			||||||
 | 
					                pd.show();
 | 
				
			||||||
        public boolean updateProgress() {
 | 
					 | 
				
			||||||
            boolean finished = false;
 | 
					 | 
				
			||||||
            switch (download.getStatus()) {
 | 
					 | 
				
			||||||
            case RUNNING:
 | 
					 | 
				
			||||||
                if (pd == null) {
 | 
					 | 
				
			||||||
                    pd = createProgressDialog(download.remoteFile(),
 | 
					 | 
				
			||||||
                            download.getProgress(), download.getMax());
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    pd.setProgress(download.getProgress());
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case ERROR:
 | 
					 | 
				
			||||||
                if (pd != null)
 | 
					 | 
				
			||||||
                    pd.dismiss();
 | 
					 | 
				
			||||||
                String text;
 | 
					 | 
				
			||||||
                if (download.getErrorType() == Downloader.Error.CORRUPT)
 | 
					 | 
				
			||||||
                    text = getString(R.string.corrupt_download);
 | 
					 | 
				
			||||||
                else
 | 
					 | 
				
			||||||
                    text = download.getErrorMessage();
 | 
					 | 
				
			||||||
                Toast.makeText(AppDetails.this, text, Toast.LENGTH_LONG).show();
 | 
					 | 
				
			||||||
                finished = true;
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case DONE:
 | 
					 | 
				
			||||||
                if (pd != null)
 | 
					 | 
				
			||||||
                    pd.dismiss();
 | 
					 | 
				
			||||||
                installApk(download.localFile(), id);
 | 
					 | 
				
			||||||
                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() {
 | 
					    @Override
 | 
				
			||||||
            updating = false;
 | 
					    public void onProgress(Event event) {
 | 
				
			||||||
            removeMessages(0);
 | 
					        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;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public void cancel() {
 | 
					        boolean finished = false;
 | 
				
			||||||
            if (download != null)
 | 
					        if (event.type.equals(Downloader.EVENT_PROGRESS)) {
 | 
				
			||||||
                download.interrupt();
 | 
					            updateProgressDialog(event.progress, event.total);
 | 
				
			||||||
        }
 | 
					        } else if (event.type.equals(ApkDownloader.EVENT_ERROR)) {
 | 
				
			||||||
 | 
					            final String text;
 | 
				
			||||||
        public void destroy() {
 | 
					            if (event.getData().getInt(ApkDownloader.EVENT_DATA_ERROR_TYPE) == ApkDownloader.ERROR_HASH_MISMATCH)
 | 
				
			||||||
            // The dialog can't be dismissed when it's not displayed,
 | 
					                text = getString(R.string.corrupt_download);
 | 
				
			||||||
            // 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
 | 
					            else
 | 
				
			||||||
                sendMessageDelayed(obtainMessage(), 50);
 | 
					                text = getString(R.string.details_notinstalled);
 | 
				
			||||||
 | 
					            // this must be on the main UI thread
 | 
				
			||||||
 | 
					            Toast.makeText(this, text, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					            finished = true;
 | 
				
			||||||
 | 
					        } else if (event.type.equals(ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE)) {
 | 
				
			||||||
 | 
					            downloadCompleteInstallApk();
 | 
				
			||||||
 | 
					            finished = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (finished) {
 | 
				
			||||||
 | 
					            removeProgressDialog();
 | 
				
			||||||
 | 
					            downloadHandler = null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1183,7 +1250,7 @@ public class AppDetails extends ListActivity {
 | 
				
			|||||||
        switch (requestCode) {
 | 
					        switch (requestCode) {
 | 
				
			||||||
        case REQUEST_ENABLE_BLUETOOTH:
 | 
					        case REQUEST_ENABLE_BLUETOOTH:
 | 
				
			||||||
            fdroidApp.sendViaBluetooth(this, resultCode, app.id);
 | 
					            fdroidApp.sendViaBluetooth(this, resultCode, app.id);
 | 
				
			||||||
			break;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
					package org.fdroid.fdroid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.os.Bundle;
 | 
					import android.os.Bundle;
 | 
				
			||||||
import android.os.Parcel;
 | 
					import android.os.Parcel;
 | 
				
			||||||
import android.os.Parcelable;
 | 
					import android.os.Parcelable;
 | 
				
			||||||
 | 
					import android.text.TextUtils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public interface ProgressListener {
 | 
					public interface ProgressListener {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -15,7 +17,7 @@ public interface ProgressListener {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        public static final int NO_VALUE = Integer.MIN_VALUE;
 | 
					        public static final int NO_VALUE = Integer.MIN_VALUE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public final int type;
 | 
					        public final String type;
 | 
				
			||||||
        public final Bundle data;
 | 
					        public final Bundle data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // These two are not final, so that you can create a template Event,
 | 
					        // 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 progress;
 | 
				
			||||||
        public int total;
 | 
					        public int total;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Event(int type) {
 | 
					        public Event(String type) {
 | 
				
			||||||
            this(type, NO_VALUE, NO_VALUE, null);
 | 
					            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);
 | 
					            this(type, NO_VALUE, NO_VALUE, data);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Event(int type, int progress) {
 | 
					        public Event(String type, int progress, int total, Bundle data) {
 | 
				
			||||||
            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) {
 | 
					 | 
				
			||||||
            this.type = type;
 | 
					            this.type = type;
 | 
				
			||||||
            this.progress = progress;
 | 
					            this.progress = progress;
 | 
				
			||||||
            this.total = total;
 | 
					            this.total = total;
 | 
				
			||||||
            this.data = data == null ? new Bundle() : data;
 | 
					            this.data = (data == null) ? new Bundle() : data;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        @Override
 | 
					        @Override
 | 
				
			||||||
@ -59,7 +49,7 @@ public interface ProgressListener {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        @Override
 | 
					        @Override
 | 
				
			||||||
        public void writeToParcel(Parcel dest, int flags) {
 | 
					        public void writeToParcel(Parcel dest, int flags) {
 | 
				
			||||||
            dest.writeInt(type);
 | 
					            dest.writeString(type);
 | 
				
			||||||
            dest.writeInt(progress);
 | 
					            dest.writeInt(progress);
 | 
				
			||||||
            dest.writeInt(total);
 | 
					            dest.writeInt(total);
 | 
				
			||||||
            dest.writeBundle(data);
 | 
					            dest.writeBundle(data);
 | 
				
			||||||
@ -68,7 +58,7 @@ public interface ProgressListener {
 | 
				
			|||||||
        public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
 | 
					        public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
 | 
				
			||||||
            @Override
 | 
					            @Override
 | 
				
			||||||
            public Event createFromParcel(Parcel in) {
 | 
					            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
 | 
					            @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) {
 | 
					        } else if (localName.equals("application") && curapp == null) {
 | 
				
			||||||
            curapp = new App();
 | 
					            curapp = new App();
 | 
				
			||||||
            curapp.id = attributes.getValue("", "id");
 | 
					            curapp.id = attributes.getValue("", "id");
 | 
				
			||||||
            Bundle progressData = RepoUpdater.createProgressData(repo.address);
 | 
					 | 
				
			||||||
            progressCounter ++;
 | 
					            progressCounter ++;
 | 
				
			||||||
 | 
					            Bundle data = new Bundle(1);
 | 
				
			||||||
 | 
					            data.putString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS, repo.address);
 | 
				
			||||||
            progressListener.onProgress(
 | 
					            progressListener.onProgress(
 | 
				
			||||||
                new ProgressListener.Event(
 | 
					                new ProgressListener.Event(
 | 
				
			||||||
                    RepoUpdater.PROGRESS_TYPE_PROCESS_XML, progressCounter,
 | 
					                    RepoUpdater.PROGRESS_TYPE_PROCESS_XML,
 | 
				
			||||||
                    totalAppCount, progressData));
 | 
					                    progressCounter, totalAppCount, data));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        } else if (localName.equals("package") && curapp != null && curapk == null) {
 | 
					        } else if (localName.equals("package") && curapp != null && curapk == null) {
 | 
				
			||||||
            curapk = new Apk();
 | 
					            curapk = new Apk();
 | 
				
			||||||
 | 
				
			|||||||
@ -33,6 +33,7 @@ import android.text.TextUtils;
 | 
				
			|||||||
import android.util.Log;
 | 
					import android.util.Log;
 | 
				
			||||||
import android.widget.Toast;
 | 
					import android.widget.Toast;
 | 
				
			||||||
import org.fdroid.fdroid.data.*;
 | 
					import org.fdroid.fdroid.data.*;
 | 
				
			||||||
 | 
					import org.fdroid.fdroid.net.Downloader;
 | 
				
			||||||
import org.fdroid.fdroid.updater.RepoUpdater;
 | 
					import org.fdroid.fdroid.updater.RepoUpdater;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.*;
 | 
					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_ERROR                 = 2;
 | 
				
			||||||
    public static final int STATUS_INFO                  = 3;
 | 
					    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_RECEIVER = "receiver";
 | 
				
			||||||
    public static final String EXTRA_ADDRESS = "address";
 | 
					    public static final String EXTRA_ADDRESS = "address";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -97,28 +106,31 @@ public class UpdateService extends IntentService implements ProgressListener {
 | 
				
			|||||||
            return this;
 | 
					            return this;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private void forwardEvent(String type) {
 | 
				
			||||||
 | 
					            if (listener != null) {
 | 
				
			||||||
 | 
					                listener.onProgress(new Event(type));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        @Override
 | 
					        @Override
 | 
				
			||||||
        protected void onReceiveResult(int resultCode, Bundle resultData) {
 | 
					        protected void onReceiveResult(int resultCode, Bundle resultData) {
 | 
				
			||||||
            String message = resultData.getString(UpdateService.RESULT_MESSAGE);
 | 
					            String message = resultData.getString(UpdateService.RESULT_MESSAGE);
 | 
				
			||||||
            boolean finished = false;
 | 
					            boolean finished = false;
 | 
				
			||||||
            if (resultCode == UpdateService.STATUS_ERROR) {
 | 
					            if (resultCode == UpdateService.STATUS_ERROR) {
 | 
				
			||||||
 | 
					                forwardEvent(EVENT_ERROR);
 | 
				
			||||||
                Toast.makeText(context, message, Toast.LENGTH_LONG).show();
 | 
					                Toast.makeText(context, message, Toast.LENGTH_LONG).show();
 | 
				
			||||||
                finished = true;
 | 
					                finished = true;
 | 
				
			||||||
            } else if (resultCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES
 | 
					            } else if (resultCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
 | 
				
			||||||
                    || resultCode == UpdateService.STATUS_COMPLETE_AND_SAME) {
 | 
					                forwardEvent(EVENT_COMPLETE_WITH_CHANGES);
 | 
				
			||||||
 | 
					                finished = true;
 | 
				
			||||||
 | 
					            } else if (resultCode == UpdateService.STATUS_COMPLETE_AND_SAME) {
 | 
				
			||||||
 | 
					                forwardEvent(EVENT_COMPLETE_AND_SAME);
 | 
				
			||||||
                finished = true;
 | 
					                finished = true;
 | 
				
			||||||
            } else if (resultCode == UpdateService.STATUS_INFO) {
 | 
					            } else if (resultCode == UpdateService.STATUS_INFO) {
 | 
				
			||||||
 | 
					                forwardEvent(EVENT_INFO);
 | 
				
			||||||
                dialog.setMessage(message);
 | 
					                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())
 | 
					            if (finished && dialog.isShowing())
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    dialog.dismiss();
 | 
					                    dialog.dismiss();
 | 
				
			||||||
@ -185,17 +197,10 @@ public class UpdateService extends IntentService implements ProgressListener {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected void sendStatus(int statusCode, String message) {
 | 
					    protected void sendStatus(int statusCode, String message) {
 | 
				
			||||||
        sendStatus(statusCode, message, null);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    protected void sendStatus(int statusCode, String message, Event event) {
 | 
					 | 
				
			||||||
        if (receiver != null) {
 | 
					        if (receiver != null) {
 | 
				
			||||||
            Bundle resultData = new Bundle();
 | 
					            Bundle resultData = new Bundle();
 | 
				
			||||||
            if (message != null && message.length() > 0)
 | 
					            if (message != null && message.length() > 0)
 | 
				
			||||||
                resultData.putString(RESULT_MESSAGE, message);
 | 
					                resultData.putString(RESULT_MESSAGE, message);
 | 
				
			||||||
            if (event == null)
 | 
					 | 
				
			||||||
                event = new Event(statusCode);
 | 
					 | 
				
			||||||
            resultData.putParcelable(RESULT_EVENT, event);
 | 
					 | 
				
			||||||
            receiver.send(statusCode, resultData);
 | 
					            receiver.send(statusCode, resultData);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -675,14 +680,15 @@ public class UpdateService extends IntentService implements ProgressListener {
 | 
				
			|||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public void onProgress(ProgressListener.Event event) {
 | 
					    public void onProgress(ProgressListener.Event event) {
 | 
				
			||||||
        String message = "";
 | 
					        String message = "";
 | 
				
			||||||
        if (event.type == RepoUpdater.PROGRESS_TYPE_DOWNLOAD) {
 | 
					        // TODO: Switch to passing through Bundles of data with the event, rather than a repo address. They are
 | 
				
			||||||
            String repoAddress    = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
 | 
					        // now much more general purpose then just repo downloading.
 | 
				
			||||||
            String downloadedSize = Utils.getFriendlySize( event.progress );
 | 
					        String repoAddress = event.getData().getString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS);
 | 
				
			||||||
            String totalSize      = Utils.getFriendlySize( event.total );
 | 
					        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);
 | 
					            int percent           = (int)((double)event.progress/event.total * 100);
 | 
				
			||||||
            message = getString(R.string.status_download, repoAddress, downloadedSize, totalSize, percent);
 | 
					            message = getString(R.string.status_download, repoAddress, downloadedSize, totalSize, percent);
 | 
				
			||||||
        } else if (event.type == RepoUpdater.PROGRESS_TYPE_PROCESS_XML) {
 | 
					        } else if (event.type.equals(RepoUpdater.PROGRESS_TYPE_PROCESS_XML)) {
 | 
				
			||||||
            String repoAddress    = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
 | 
					 | 
				
			||||||
            message = getString(R.string.status_processing_xml, repoAddress, event.progress, event.total);
 | 
					            message = getString(R.string.status_processing_xml, repoAddress, event.progress, event.total);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        sendStatus(STATUS_INFO, message);
 | 
					        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;
 | 
					package org.fdroid.fdroid.net;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.*;
 | 
					import android.content.Context;
 | 
				
			||||||
import java.net.*;
 | 
					import android.os.Bundle;
 | 
				
			||||||
import android.content.*;
 | 
					import android.util.Log;
 | 
				
			||||||
import org.fdroid.fdroid.*;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class Downloader {
 | 
					import org.fdroid.fdroid.ProgressListener;
 | 
				
			||||||
 | 
					import org.fdroid.fdroid.Utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final String HEADER_IF_NONE_MATCH = "If-None-Match";
 | 
					import java.io.File;
 | 
				
			||||||
    private static final String HEADER_FIELD_ETAG = "ETag";
 | 
					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 OutputStream outputStream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private ProgressListener progressListener = null;
 | 
					    private ProgressListener progressListener = null;
 | 
				
			||||||
    private ProgressListener.Event progressEvent = null;
 | 
					    private Bundle eventData = null;
 | 
				
			||||||
    private String eTag = null;
 | 
					    private File outputFile;
 | 
				
			||||||
    private final File outputFile;
 | 
					    protected String cacheTag = null;
 | 
				
			||||||
    private HttpURLConnection connection;
 | 
					
 | 
				
			||||||
    private int statusCode = -1;
 | 
					    public static final String EVENT_PROGRESS = "downloadProgress";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public abstract InputStream inputStream() throws IOException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // The context is required for opening the file to write to.
 | 
					    // 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 {
 | 
					            throws FileNotFoundException, MalformedURLException {
 | 
				
			||||||
        sourceUrl    = new URL(source);
 | 
					        this(new File(ctx.getFilesDir() + File.separator + destFile));
 | 
				
			||||||
        outputStream = ctx.openFileOutput(destFile, Context.MODE_PRIVATE);
 | 
					 | 
				
			||||||
        outputFile   = new File(ctx.getFilesDir() + File.separator + destFile);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    // The context is required for opening the file to write to.
 | 
				
			||||||
     * Downloads to a temporary file, which *you must delete yourself when
 | 
					    public Downloader(Context ctx) throws IOException {
 | 
				
			||||||
     * you are done*.
 | 
					        this(File.createTempFile("dl-", "", ctx.getCacheDir()));
 | 
				
			||||||
     * @see org.fdroid.fdroid.net.Downloader#getFile()
 | 
					    }
 | 
				
			||||||
     */
 | 
					
 | 
				
			||||||
    public Downloader(String source, Context ctx) throws IOException {
 | 
					    public Downloader(File destFile)
 | 
				
			||||||
 | 
					            throws FileNotFoundException, MalformedURLException {
 | 
				
			||||||
        // http://developer.android.com/guide/topics/data/data-storage.html#InternalCache
 | 
					        // http://developer.android.com/guide/topics/data/data-storage.html#InternalCache
 | 
				
			||||||
        outputFile = File.createTempFile("dl-", "", ctx.getCacheDir());
 | 
					        outputFile = destFile;
 | 
				
			||||||
        outputStream = new FileOutputStream(outputFile);
 | 
					        outputStream = new FileOutputStream(outputFile);
 | 
				
			||||||
        sourceUrl = new URL(source);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Downloader(String source, OutputStream output)
 | 
					    public Downloader(OutputStream output)
 | 
				
			||||||
            throws MalformedURLException {
 | 
					            throws MalformedURLException {
 | 
				
			||||||
        sourceUrl    = new URL(source);
 | 
					 | 
				
			||||||
        outputStream = output;
 | 
					        outputStream = output;
 | 
				
			||||||
        outputFile   = null;
 | 
					        outputFile   = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public void setProgressListener(ProgressListener progressListener,
 | 
					    public void setProgressListener(ProgressListener listener) {
 | 
				
			||||||
                                    ProgressListener.Event progressEvent) {
 | 
					        setProgressListener(listener, null);
 | 
				
			||||||
        this.progressListener = progressListener;
 | 
					    }
 | 
				
			||||||
        this.progressEvent = progressEvent;
 | 
					
 | 
				
			||||||
 | 
					    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;
 | 
					        return outputFile;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public abstract boolean hasChanged();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public abstract int totalDownloadSize();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Only available after downloading a file.
 | 
					     * 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 int getStatusCode() {
 | 
					    public void downloadUninterrupted() throws IOException {
 | 
				
			||||||
        return statusCode;
 | 
					        try {
 | 
				
			||||||
 | 
					            download();
 | 
				
			||||||
 | 
					        } catch (InterruptedException ignored) {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public abstract void download() throws IOException, InterruptedException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public abstract boolean isCached();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected void downloadFromStream() throws IOException, InterruptedException {
 | 
				
			||||||
 | 
					        Log.d(TAG, "Downloading from stream");
 | 
				
			||||||
 | 
					        InputStream input = null;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Even if we have completely downloaded the file, we should probably respect
 | 
				
			||||||
 | 
					        // the wishes of the user who wanted to cancel us.
 | 
				
			||||||
 | 
					        throwExceptionIfInterrupted();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * If you ask for the eTag before calling download(), you will get the
 | 
					     * In a synchronous download (the usual usage of the Downloader interface),
 | 
				
			||||||
     * same one you passed in (if any). If you call it after download(), you
 | 
					     * you will not be able to interrupt this because the thread will block
 | 
				
			||||||
     * will get the new eTag from the server, or null if there was none.
 | 
					     * 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
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public String getETag() {
 | 
					    private void throwExceptionIfInterrupted() throws InterruptedException {
 | 
				
			||||||
        return eTag;
 | 
					        if (Thread.interrupted()) {
 | 
				
			||||||
 | 
					            Log.d(TAG, "Received interrupt, cancelling download");
 | 
				
			||||||
 | 
					            throw new InterruptedException();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    protected void copyInputToOutputStream(InputStream input) throws IOException, InterruptedException {
 | 
				
			||||||
     * 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;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Get a remote file. Returns the HTTP response code.
 | 
					        byte[] buffer = new byte[Utils.BUFFER_SIZE];
 | 
				
			||||||
    // If 'etag' is not null, it's passed to the server as an If-None-Match
 | 
					        int bytesRead = 0;
 | 
				
			||||||
    // header, in which case expect a 304 response if nothing changed.
 | 
					        int totalBytes = totalDownloadSize();
 | 
				
			||||||
    // 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
 | 
					        // Getting the total download size could potentially take time, depending on how
 | 
				
			||||||
    // empty if none was available.
 | 
					        // it is implemented, so we may as well check this before we proceed.
 | 
				
			||||||
    public int download() throws IOException {
 | 
					        throwExceptionIfInterrupted();
 | 
				
			||||||
        connection = (HttpURLConnection)sourceUrl.openConnection();
 | 
					
 | 
				
			||||||
        setupCacheCheck();
 | 
					        sendProgress(bytesRead, totalBytes);
 | 
				
			||||||
        statusCode = connection.getResponseCode();
 | 
					        while (true) {
 | 
				
			||||||
        if (statusCode == 200) {
 | 
					
 | 
				
			||||||
            setupProgressListener();
 | 
					            int count = input.read(buffer);
 | 
				
			||||||
            InputStream input = null;
 | 
					            throwExceptionIfInterrupted();
 | 
				
			||||||
            try {
 | 
					
 | 
				
			||||||
                input = connection.getInputStream();
 | 
					            bytesRead += count;
 | 
				
			||||||
                Utils.copy(input, outputStream,
 | 
					            sendProgress(bytesRead, totalBytes);
 | 
				
			||||||
                        progressListener, progressEvent);
 | 
					            if (count == -1) {
 | 
				
			||||||
            } finally {
 | 
					                Log.d(TAG, "Finished downloading from stream");
 | 
				
			||||||
                Utils.closeQuietly(outputStream);
 | 
					                break;
 | 
				
			||||||
                Utils.closeQuietly(input);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            updateCacheCheck();
 | 
					            outputStream.write(buffer, 0, count);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return statusCode;
 | 
					        outputStream.flush();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected void setupCacheCheck() {
 | 
					    protected void sendProgress(int bytesRead, int totalBytes) {
 | 
				
			||||||
        if (eTag != null) {
 | 
					        sendProgress(new ProgressListener.Event(EVENT_PROGRESS, bytesRead, totalBytes, eventData));
 | 
				
			||||||
            connection.setRequestProperty(HEADER_IF_NONE_MATCH, eTag);
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected void sendProgress(ProgressListener.Event event) {
 | 
				
			||||||
 | 
					        if (progressListener != null) {
 | 
				
			||||||
 | 
					            progressListener.onProgress(event);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected void updateCacheCheck() {
 | 
					 | 
				
			||||||
        eTag = connection.getHeaderField(HEADER_FIELD_ETAG);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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.content.Context;
 | 
				
			||||||
import android.os.Bundle;
 | 
					import android.os.Bundle;
 | 
				
			||||||
import android.util.Log;
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.fdroid.fdroid.ProgressListener;
 | 
					import org.fdroid.fdroid.ProgressListener;
 | 
				
			||||||
import org.fdroid.fdroid.RepoXMLHandler;
 | 
					import org.fdroid.fdroid.RepoXMLHandler;
 | 
				
			||||||
import org.fdroid.fdroid.Utils;
 | 
					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.Repo;
 | 
				
			||||||
import org.fdroid.fdroid.data.RepoProvider;
 | 
					import org.fdroid.fdroid.data.RepoProvider;
 | 
				
			||||||
import org.fdroid.fdroid.net.Downloader;
 | 
					import org.fdroid.fdroid.net.Downloader;
 | 
				
			||||||
 | 
					import org.fdroid.fdroid.net.HttpDownloader;
 | 
				
			||||||
import org.xml.sax.InputSource;
 | 
					import org.xml.sax.InputSource;
 | 
				
			||||||
import org.xml.sax.SAXException;
 | 
					import org.xml.sax.SAXException;
 | 
				
			||||||
import org.xml.sax.XMLReader;
 | 
					import org.xml.sax.XMLReader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.net.ssl.SSLHandshakeException;
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
import javax.xml.parsers.ParserConfigurationException;
 | 
					import java.io.File;
 | 
				
			||||||
import javax.xml.parsers.SAXParser;
 | 
					import java.io.FileNotFoundException;
 | 
				
			||||||
import javax.xml.parsers.SAXParserFactory;
 | 
					import java.io.FileReader;
 | 
				
			||||||
import java.io.*;
 | 
					import java.io.IOException;
 | 
				
			||||||
import java.util.ArrayList;
 | 
					import java.util.ArrayList;
 | 
				
			||||||
import java.util.Date;
 | 
					import java.util.Date;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.xml.parsers.ParserConfigurationException;
 | 
				
			||||||
 | 
					import javax.xml.parsers.SAXParser;
 | 
				
			||||||
 | 
					import javax.xml.parsers.SAXParserFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
abstract public class RepoUpdater {
 | 
					abstract public class RepoUpdater {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static final int PROGRESS_TYPE_DOWNLOAD     = 1;
 | 
					    public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml";
 | 
				
			||||||
    public static final int PROGRESS_TYPE_PROCESS_XML  = 2;
 | 
					
 | 
				
			||||||
    public static final String PROGRESS_DATA_REPO      = "repo";
 | 
					    public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) {
 | 
					    public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) {
 | 
				
			||||||
        if (repo.fingerprint == null && repo.pubkey == null) {
 | 
					        if (repo.fingerprint == null && repo.pubkey == null) {
 | 
				
			||||||
@ -68,10 +74,6 @@ abstract public class RepoUpdater {
 | 
				
			|||||||
        return apks;
 | 
					        return apks;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public boolean isInteractive() {
 | 
					 | 
				
			||||||
        return progressListener != null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * For example, you may want to unzip a jar file to get the index inside,
 | 
					     * 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
 | 
					     * 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 abstract String getIndexAddress();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected Downloader downloadIndex() throws UpdateException {
 | 
					    protected Downloader downloadIndex() throws UpdateException {
 | 
				
			||||||
        Bundle progressData = createProgressData(repo.address);
 | 
					 | 
				
			||||||
        Downloader downloader = null;
 | 
					        Downloader downloader = null;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            downloader = new Downloader(getIndexAddress(), context);
 | 
					            downloader = new HttpDownloader(getIndexAddress(), context);
 | 
				
			||||||
            downloader.setETag(repo.lastetag);
 | 
					            downloader.setCacheTag(repo.lastetag);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (isInteractive()) {
 | 
					            if (progressListener != null) { // interactive session, show progress
 | 
				
			||||||
                ProgressListener.Event event =
 | 
					                Bundle data = new Bundle(1);
 | 
				
			||||||
                    new ProgressListener.Event(
 | 
					                data.putString(PROGRESS_DATA_REPO_ADDRESS, repo.address);
 | 
				
			||||||
                        RepoUpdater.PROGRESS_TYPE_DOWNLOAD, progressData);
 | 
					                downloader.setProgressListener(progressListener, data);
 | 
				
			||||||
                downloader.setProgressListener(progressListener, event);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            int status = downloader.download();
 | 
					            downloader.downloadUninterrupted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (status == 304) {
 | 
					            if (downloader.isCached()) {
 | 
				
			||||||
                // The index is unchanged since we last read it. We just mark
 | 
					                // The index is unchanged since we last read it. We just mark
 | 
				
			||||||
                // everything that came from this repo as being updated.
 | 
					                // 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)");
 | 
					                        + " 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) {
 | 
					        } catch (IOException e) {
 | 
				
			||||||
            if (downloader != null && downloader.getFile() != null) {
 | 
					            if (downloader != null && downloader.getFile() != null) {
 | 
				
			||||||
                downloader.getFile().delete();
 | 
					                downloader.getFile().delete();
 | 
				
			||||||
@ -138,12 +119,6 @@ abstract public class RepoUpdater {
 | 
				
			|||||||
        return downloader;
 | 
					        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) {
 | 
					    private int estimateAppCount(File indexFile) {
 | 
				
			||||||
        int count = -1;
 | 
					        int count = -1;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
@ -182,7 +157,7 @@ abstract public class RepoUpdater {
 | 
				
			|||||||
                XMLReader reader = parser.getXMLReader();
 | 
					                XMLReader reader = parser.getXMLReader();
 | 
				
			||||||
                RepoXMLHandler handler = new RepoXMLHandler(repo, progressListener);
 | 
					                RepoXMLHandler handler = new RepoXMLHandler(repo, progressListener);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (isInteractive()) {
 | 
					                if (progressListener != null) {
 | 
				
			||||||
                    // Only bother spending the time to count the expected apps
 | 
					                    // Only bother spending the time to count the expected apps
 | 
				
			||||||
                    // if we can show that to the user...
 | 
					                    // if we can show that to the user...
 | 
				
			||||||
                    handler.setTotalAppCount(estimateAppCount(indexFile));
 | 
					                    handler.setTotalAppCount(estimateAppCount(indexFile));
 | 
				
			||||||
@ -195,7 +170,7 @@ abstract public class RepoUpdater {
 | 
				
			|||||||
                reader.parse(is);
 | 
					                reader.parse(is);
 | 
				
			||||||
                apps = handler.getApps();
 | 
					                apps = handler.getApps();
 | 
				
			||||||
                apks = handler.getApks();
 | 
					                apks = handler.getApks();
 | 
				
			||||||
                updateRepo(handler, downloader.getETag());
 | 
					                updateRepo(handler, downloader.getCacheTag());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (SAXException e) {
 | 
					        } catch (SAXException e) {
 | 
				
			||||||
            throw new UpdateException(
 | 
					            throw new UpdateException(
 | 
				
			||||||
 | 
				
			|||||||
@ -203,7 +203,7 @@ public class RepoDetailsFragment extends Fragment {
 | 
				
			|||||||
        UpdateService.updateRepoNow(repo.address, getActivity()).setListener(new ProgressListener() {
 | 
					        UpdateService.updateRepoNow(repo.address, getActivity()).setListener(new ProgressListener() {
 | 
				
			||||||
            @Override
 | 
					            @Override
 | 
				
			||||||
            public void onProgress(Event event) {
 | 
					            public void onProgress(Event event) {
 | 
				
			||||||
                if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
 | 
					                if (event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) {
 | 
				
			||||||
                    repo = loadRepoDetails();
 | 
					                    repo = loadRepoDetails();
 | 
				
			||||||
                    updateView((ViewGroup)getView());
 | 
					                    updateView((ViewGroup)getView());
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
				
			|||||||
@ -218,8 +218,8 @@ public class RepoListFragment extends ListFragment
 | 
				
			|||||||
        UpdateService.updateNow(getActivity()).setListener(new ProgressListener() {
 | 
					        UpdateService.updateNow(getActivity()).setListener(new ProgressListener() {
 | 
				
			||||||
            @Override
 | 
					            @Override
 | 
				
			||||||
            public void onProgress(Event event) {
 | 
					            public void onProgress(Event event) {
 | 
				
			||||||
                if (event.type == UpdateService.STATUS_COMPLETE_AND_SAME ||
 | 
					                if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME) ||
 | 
				
			||||||
                        event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
 | 
					                        event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) {
 | 
				
			||||||
                    // No need to prompt to update any more, we just did it!
 | 
					                    // No need to prompt to update any more, we just did it!
 | 
				
			||||||
                    changed = false;
 | 
					                    changed = false;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user