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