Merge branch 'sort-out-install-uninstall-events' into 'master'
Sort out install uninstall events Closes #1311, #1031, #1271, #1435, and #1145 See merge request fdroid/fdroidclient!710
This commit is contained in:
		
						commit
						42424ad902
					
				| @ -280,9 +280,6 @@ | ||||
|                 android:name=".data.InstalledAppProviderService" | ||||
|                 android:permission="android.permission.BIND_JOB_SERVICE" | ||||
|                 android:exported="false"/> | ||||
|         <service | ||||
|                 android:name=".AppUpdateStatusService" | ||||
|                 android:exported="false"/> | ||||
| 
 | ||||
| 
 | ||||
|         <!-- Warning: Please add all new services to HidingManager --> | ||||
|  | ||||
| @ -488,15 +488,15 @@ public class AppDetails2 extends AppCompatActivity | ||||
|             case Downloading: | ||||
|                 if (newStatus.progressMax == 0) { | ||||
|                     // The first progress notification we get telling us our status is "Downloading" | ||||
|                     adapter.setProgress(-1, -1, R.string.download_pending); | ||||
|                     adapter.setIndeterminateProgress(R.string.download_pending); | ||||
|                 } else { | ||||
|                     adapter.setProgress(newStatus.progressCurrent, newStatus.progressMax, 0); | ||||
|                     adapter.setProgress(newStatus.progressCurrent, newStatus.progressMax); | ||||
|                 } | ||||
|                 break; | ||||
| 
 | ||||
|             case ReadyToInstall: | ||||
|                 if (justReceived) { | ||||
|                     adapter.clearProgress(); | ||||
|                     adapter.setIndeterminateProgress(R.string.installing); | ||||
|                     localBroadcastManager.registerReceiver(installReceiver, | ||||
|                             Installer.getInstallIntentFilter(Uri.parse(newStatus.getUniqueKey()))); | ||||
|                 } | ||||
| @ -517,6 +517,9 @@ public class AppDetails2 extends AppCompatActivity | ||||
|                 break; | ||||
| 
 | ||||
|             case Installing: | ||||
|                 adapter.setIndeterminateProgress(R.string.installing); | ||||
|                 break; | ||||
| 
 | ||||
|             case Installed: | ||||
|             case UpdateAvailable: | ||||
|             case InstallError: | ||||
| @ -553,7 +556,7 @@ public class AppDetails2 extends AppCompatActivity | ||||
|         public void onReceive(Context context, Intent intent) { | ||||
|             switch (intent.getAction()) { | ||||
|                 case Installer.ACTION_INSTALL_STARTED: | ||||
|                     adapter.setProgress(-1, -1, R.string.installing); | ||||
|                     adapter.setIndeterminateProgress(R.string.installing); | ||||
|                     break; | ||||
|                 case Installer.ACTION_INSTALL_COMPLETE: | ||||
|                     adapter.clearProgress(); | ||||
| @ -625,7 +628,7 @@ public class AppDetails2 extends AppCompatActivity | ||||
|         public void onReceive(Context context, Intent intent) { | ||||
|             switch (intent.getAction()) { | ||||
|                 case Installer.ACTION_UNINSTALL_STARTED: | ||||
|                     adapter.setProgress(-1, -1, R.string.uninstalling); | ||||
|                     adapter.setIndeterminateProgress(R.string.uninstalling); | ||||
|                     break; | ||||
|                 case Installer.ACTION_UNINSTALL_COMPLETE: | ||||
|                     adapter.clearProgress(); | ||||
| @ -762,18 +765,29 @@ public class AppDetails2 extends AppCompatActivity | ||||
|         installApk(apkToInstall); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Uninstall the app from the current screen.  Since there are many ways | ||||
|      * to uninstall an app, including from Google Play, {@code adb uninstall}, | ||||
|      * or Settings -> Apps, this method cannot ever be sure that the app isn't | ||||
|      * already being uninstalled.  So it needs to check that we can actually | ||||
|      * get info on the installed app, otherwise, just call it interrupted and | ||||
|      * quit. | ||||
|      * | ||||
|      * @see <a href="https://gitlab.com/fdroid/fdroidclient/issues/1435">issue #1435</a> | ||||
|      */ | ||||
|     @Override | ||||
|     public void uninstallApk() { | ||||
|         Apk apk = app.installedApk; | ||||
|         if (apk == null) { | ||||
|             // TODO ideally, app would be refreshed immediately after install, then this | ||||
|             // workaround would be unnecessary - unless it is a media file | ||||
|             apk = app.getMediaApkifInstalled(getApplicationContext()); | ||||
|             if (apk == null) { | ||||
|                 // When the app isn't a media file - the above workaround refers to this. | ||||
|                 apk = app.getInstalledApk(this); | ||||
|                 if (apk == null) { | ||||
|                     throw new IllegalStateException("Couldn't find installed apk for " + app.packageName); | ||||
|                     Log.d(TAG, "Couldn't find installed apk for " + app.packageName); | ||||
|                     Toast.makeText(this, R.string.uninstall_error_unknown, Toast.LENGTH_SHORT).show(); | ||||
|                     uninstallReceiver.onReceive(this, new Intent(Installer.ACTION_UNINSTALL_INTERRUPTED)); | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|             app.installedApk = apk; | ||||
|  | ||||
| @ -18,6 +18,7 @@ import org.fdroid.fdroid.data.App; | ||||
| import org.fdroid.fdroid.data.AppProvider; | ||||
| import org.fdroid.fdroid.data.Repo; | ||||
| import org.fdroid.fdroid.installer.ErrorDialogActivity; | ||||
| import org.fdroid.fdroid.installer.InstallManagerService; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| @ -41,7 +42,7 @@ import java.util.Map; | ||||
|  */ | ||||
| public final class AppUpdateStatusManager { | ||||
| 
 | ||||
|     private static final String TAG = "AppUpdateStatusManager"; | ||||
|     public static final String TAG = "AppUpdateStatusManager"; | ||||
| 
 | ||||
|     /** | ||||
|      * Broadcast when: | ||||
| @ -90,7 +91,6 @@ public final class AppUpdateStatusManager { | ||||
|     private static final String LOGTAG = "AppUpdateStatusManager"; | ||||
| 
 | ||||
|     public enum Status { | ||||
|         PendingDownload, | ||||
|         DownloadInterrupted, | ||||
|         UpdateAvailable, | ||||
|         Downloading, | ||||
| @ -125,6 +125,11 @@ public final class AppUpdateStatusManager { | ||||
|             this.intent = intent; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * @return the unique ID used to represent this specific package's install process | ||||
|          * also known as {@code urlString}. | ||||
|          * @see org.fdroid.fdroid.installer.InstallManagerService | ||||
|          */ | ||||
|         public String getUniqueKey() { | ||||
|             return apk.getUrl(); | ||||
|         } | ||||
| @ -196,15 +201,9 @@ public final class AppUpdateStatusManager { | ||||
|     private final HashMap<String, AppUpdateStatus> appMapping = new HashMap<>(); | ||||
|     private boolean isBatchUpdating; | ||||
| 
 | ||||
|     /** | ||||
|      * @see #isPendingInstall(String) | ||||
|      */ | ||||
|     private final SharedPreferences apksPendingInstall; | ||||
| 
 | ||||
|     private AppUpdateStatusManager(Context context) { | ||||
|         this.context = context; | ||||
|         localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext()); | ||||
|         apksPendingInstall = context.getSharedPreferences("apks-pending-install", Context.MODE_PRIVATE); | ||||
|     } | ||||
| 
 | ||||
|     public void removeAllByRepo(Repo repo) { | ||||
| @ -261,6 +260,10 @@ public final class AppUpdateStatusManager { | ||||
|         entry.intent = intent; | ||||
|         setEntryContentIntentIfEmpty(entry); | ||||
|         notifyChange(entry, isStatusUpdate); | ||||
| 
 | ||||
|         if (status == Status.Installed) { | ||||
|             InstallManagerService.removePendingInstall(context, entry.getUniqueKey()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void addApkInternal(@NonNull Apk apk, @NonNull Status status, PendingIntent intent) { | ||||
| @ -269,6 +272,10 @@ public final class AppUpdateStatusManager { | ||||
|         setEntryContentIntentIfEmpty(entry); | ||||
|         appMapping.put(entry.getUniqueKey(), entry); | ||||
|         notifyAdd(entry); | ||||
| 
 | ||||
|         if (status == Status.Installed) { | ||||
|             InstallManagerService.removePendingInstall(context, entry.getUniqueKey()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void notifyChange(String reason) { | ||||
| @ -370,8 +377,15 @@ public final class AppUpdateStatusManager { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove an APK from being tracked, since it is now considered {@link Status#Installed} | ||||
|      * | ||||
|      * @param key the unique ID for the install process, also called {@code urlString} | ||||
|      * @see org.fdroid.fdroid.installer.InstallManagerService | ||||
|      */ | ||||
|     public void removeApk(String key) { | ||||
|         synchronized (appMapping) { | ||||
|             InstallManagerService.removePendingInstall(context, key); | ||||
|             AppUpdateStatus entry = appMapping.remove(key); | ||||
|             if (entry != null) { | ||||
|                 Utils.debugLog(LOGTAG, "Remove APK " + entry.apk.apkName); | ||||
| @ -426,6 +440,8 @@ public final class AppUpdateStatusManager { | ||||
|             entry.errorText = errorText; | ||||
|             entry.intent = getAppErrorIntent(entry); | ||||
|             notifyChange(entry, false); | ||||
| 
 | ||||
|             InstallManagerService.removePendingInstall(context, entry.getUniqueKey()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -541,55 +557,4 @@ public final class AppUpdateStatusManager { | ||||
|                 errorDialogIntent, | ||||
|                 PendingIntent.FLAG_UPDATE_CURRENT); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Note that this could technically be made private and automatically invoked when | ||||
|      * {@link #addApk(Apk, Status, PendingIntent)} is called, but that would greatly reduce | ||||
|      * the maintainability of this class. Right now it is used by two clients: the notification | ||||
|      * manager, and the Updates tab. They have different requirements, with the Updates information | ||||
|      * being more permanent than the notification info. As such, the different clients should be | ||||
|      * aware of their requirements when invoking general-sounding methods like "addApk()", rather | ||||
|      * than this class trying to second-guess why they added an apk. | ||||
|      * | ||||
|      * @see #isPendingInstall(String) | ||||
|      */ | ||||
|     public void markAsPendingInstall(String urlString) { | ||||
|         AppUpdateStatus entry = get(urlString); | ||||
|         if (entry != null) { | ||||
|             Utils.debugLog(TAG, "Marking " + entry.apk.packageName + " as pending install."); | ||||
|             apksPendingInstall.edit().putBoolean(entry.apk.hash, true).apply(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @see #markAsNoLongerPendingInstall(AppUpdateStatus) | ||||
|      * @see #isPendingInstall(String) | ||||
|      */ | ||||
|     public void markAsNoLongerPendingInstall(String urlString) { | ||||
|         AppUpdateStatus entry = get(urlString); | ||||
|         if (entry != null) { | ||||
|             markAsNoLongerPendingInstall(entry); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @see #markAsNoLongerPendingInstall(AppUpdateStatus) | ||||
|      * @see #isPendingInstall(String) | ||||
|      */ | ||||
|     public void markAsNoLongerPendingInstall(@NonNull AppUpdateStatus entry) { | ||||
|         Utils.debugLog(TAG, "Marking " + entry.apk.packageName + " as NO LONGER pending install."); | ||||
|         apksPendingInstall.edit().remove(entry.apk.hash).apply(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Keep track of the list of apks for which an install was initiated (i.e. a download + install). | ||||
|      * This is used when F-Droid starts, so that it can look through the cached apks and decide whether | ||||
|      * the presence of a .apk file means we should tell the user to press "Install" to complete the | ||||
|      * process, or whether it is purely there because it was installed some time ago and is no longer | ||||
|      * needed. | ||||
|      */ | ||||
|     public boolean isPendingInstall(String apkHash) { | ||||
|         return apksPendingInstall.contains(apkHash); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| } | ||||
| @ -1,183 +0,0 @@ | ||||
| package org.fdroid.fdroid; | ||||
| 
 | ||||
| import android.app.IntentService; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.pm.PackageInfo; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| import org.fdroid.fdroid.data.Apk; | ||||
| import org.fdroid.fdroid.data.ApkProvider; | ||||
| import org.fdroid.fdroid.data.InstalledAppProviderService; | ||||
| import org.fdroid.fdroid.installer.ApkCache; | ||||
| import org.fdroid.fdroid.installer.InstallManagerService; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * Scans the list of downloaded .apk files in the cache.  This info is used to | ||||
|  * determine if an APK is ready to install.  When a valid .apk file is found, | ||||
|  * this checks whether that APK is already installed, and whether the user's | ||||
|  * install request is still active.  If all those are true, then this tells the | ||||
|  * {@link AppUpdateStatusManager} that the APK is | ||||
|  * {@link AppUpdateStatusManager.Status#ReadyToInstall}. This is an | ||||
|  * {@link IntentService} so as to run on a background thread, as it hits the | ||||
|  * disk a bit to figure out the hash of each downloaded file. | ||||
|  */ | ||||
| public class AppUpdateStatusService extends IntentService { | ||||
| 
 | ||||
|     private static final String TAG = "AppUpdateStatusService"; | ||||
| 
 | ||||
|     /** | ||||
|      * Queue up a background scan of all downloaded apk files to see if we should notify the user | ||||
|      * that they are ready to install. | ||||
|      */ | ||||
|     public static void scanDownloadedApks(Context context) { | ||||
|         context.startService(new Intent(context, AppUpdateStatusService.class)); | ||||
|     } | ||||
| 
 | ||||
|     public AppUpdateStatusService() { | ||||
|         super("AppUpdateStatusService"); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onHandleIntent(@Nullable Intent intent) { | ||||
|         Utils.debugLog(TAG, "Scanning apk cache to see if we need to prompt the user to install any apks."); | ||||
|         File cacheDir = ApkCache.getApkCacheDir(this); | ||||
|         if (cacheDir == null) { | ||||
|             return; | ||||
|         } | ||||
|         String[] cacheDirList = cacheDir.list(); | ||||
|         if (cacheDirList == null) { | ||||
|             return; | ||||
|         } | ||||
|         List<Apk> apksReadyToInstall = new ArrayList<>(); | ||||
|         for (String repoDirName : cacheDirList) { | ||||
|             File repoDir = new File(cacheDir, repoDirName); | ||||
|             String[] apks = repoDir.list(); | ||||
|             if (apks == null) { | ||||
|                 continue; | ||||
|             } | ||||
|             for (String apkFileName : apks) { | ||||
|                 Apk apk = processDownloadedApk(new File(repoDir, apkFileName)); | ||||
|                 if (apk != null) { | ||||
|                     PackageInfo packageInfo = Utils.getPackageInfo(this, apk.packageName); | ||||
|                     if (packageInfo == null || packageInfo.versionCode != apk.versionCode) { | ||||
|                         Utils.debugLog(TAG, "Marking downloaded apk " + apk.apkName + " as ReadyToInstall"); | ||||
|                         apksReadyToInstall.add(apk); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (apksReadyToInstall.size() > 0) { | ||||
|             AppUpdateStatusManager.getInstance(this).addApks(apksReadyToInstall, | ||||
|                     AppUpdateStatusManager.Status.ReadyToInstall); | ||||
|             InstallManagerService.managePreviouslyDownloadedApks(this); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Verifies that {@param apkPath} is a valid apk which the user intends to install. | ||||
|      * If it is corrupted to the point where {@link PackageManager} can't read it, doesn't match the hash of any apk | ||||
|      * we know about in our database, is not pending install, or is already installed, then it will return null. | ||||
|      */ | ||||
|     @Nullable | ||||
|     private Apk processDownloadedApk(File apkPath) { | ||||
|         Utils.debugLog(TAG, "Checking " + apkPath); | ||||
| 
 | ||||
|         // Overly defensive checking for existence. One would think that the file exists at this point, | ||||
|         // because we got it from the result of File#list() earlier. However, this has proven to not be | ||||
|         // sufficient, and by the time we get here we are often hitting a non-existent file. | ||||
|         // This may be due to the fact that the loop checking each file in the cache takes a long time to execute. | ||||
|         // If the number of apps in the cache is large, it can take 10s of seconds to complete. In such | ||||
|         // cases, it is possible that Android has cleared up some files in the cache to make space in | ||||
|         // the meantime. | ||||
|         // | ||||
|         // This is all just a hypothesis about what may have caused | ||||
|         // https://gitlab.com/fdroid/fdroidclient/issues/1172 | ||||
|         if (!apkPath.exists()) { | ||||
|             Log.i(TAG, "Was going to check " + apkPath + ", but it has since been removed from the cache."); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         PackageInfo downloadedInfo = getPackageManager().getPackageArchiveInfo(apkPath.getAbsolutePath(), | ||||
|                 PackageManager.GET_GIDS); | ||||
|         if (downloadedInfo == null) { | ||||
|             Log.i(TAG, "Skipping " + apkPath + " because PackageManager was unable to read it."); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         Utils.debugLog(TAG, "Found package for " + downloadedInfo.packageName + ':' + downloadedInfo.versionCode | ||||
|                 + ", checking its hash to see if it downloaded correctly."); | ||||
|         Apk downloadedApk = findApkMatchingHash(apkPath); | ||||
|         if (downloadedApk == null) { | ||||
|             Log.i(TAG, "Either the apk wasn't downloaded fully, or the repo it came from has been disabled. " | ||||
|                     + "Either way, not notifying the user about it."); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         if (!AppUpdateStatusManager.getInstance(this).isPendingInstall(downloadedApk.hash)) { | ||||
|             Log.i(TAG, downloadedApk.packageName + ':' + downloadedApk.versionCode | ||||
|                     + " is NOT pending install, probably just left over from a previous install."); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         PackageInfo info = Utils.getPackageInfo(this, downloadedApk.packageName); | ||||
|         if (info != null) { | ||||
|             File pathToInstalled = InstalledAppProviderService.getPathToInstalledApk(info); | ||||
|             if (pathToInstalled != null && pathToInstalled.canRead() && | ||||
|                     pathToInstalled.length() == downloadedApk.size && // Check size before hash for performance. | ||||
|                     TextUtils.equals(Utils.getBinaryHash(pathToInstalled, "sha256"), downloadedApk.hash)) { | ||||
|                 Log.i(TAG, downloadedApk.packageName | ||||
|                         + " is pending install, but we already have the correct version installed."); | ||||
|                 AppUpdateStatusManager.getInstance(this).markAsNoLongerPendingInstall(downloadedApk.getUrl()); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         Utils.debugLog(TAG, downloadedApk.packageName + ':' + downloadedApk.versionCode | ||||
|                 + " is pending install, so we need to notify the user about installing it."); | ||||
|         return downloadedApk; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * There could be multiple apks with the same hash, provided by different repositories. | ||||
|      * This method looks for all matching records in the database. It then asks each of these | ||||
|      * {@link Apk} instances where they expect to be downloaded. If they expect to be downloaded | ||||
|      * to {@param apkPath} then that instance is returned. | ||||
|      * <p> | ||||
|      * If no files have a matching hash, or only those which don't belong to the correct repo, then | ||||
|      * this will return null.  This method needs to do its own check whether the file exists, | ||||
|      * since files can be deleted from the cache at any time without warning. | ||||
|      */ | ||||
|     @Nullable | ||||
|     private Apk findApkMatchingHash(File apkPath) { | ||||
|         if (!apkPath.canRead()) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         // NOTE: This presumes SHA256 is the only supported hash. It seems like that is an assumption | ||||
|         // in more than one place in the F-Droid client. If this becomes a problem in the future, we | ||||
|         // can query the Apk table for `SELECT DISTINCT hashType FROM fdroid_apk` and then we can just | ||||
|         // try each of the hash types that have been specified in the metadata. Seems a bit overkill | ||||
|         // at the time of writing though. | ||||
|         String hash = Utils.getBinaryHash(apkPath, "sha256"); | ||||
| 
 | ||||
|         List<Apk> apksMatchingHash = ApkProvider.Helper.findApksByHash(this, hash); | ||||
|         Utils.debugLog(TAG, "Found " + apksMatchingHash.size() + " apk(s) matching the hash " + hash); | ||||
| 
 | ||||
|         for (Apk apk : apksMatchingHash) { | ||||
|             if (apkPath.equals(ApkCache.getApkDownloadPath(this, Uri.parse(apk.getUrl())))) { | ||||
|                 return apk; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| @ -372,7 +372,6 @@ public class FDroidApp extends Application { | ||||
|         } | ||||
| 
 | ||||
|         InstalledAppProviderService.compareToPackageManager(this); | ||||
|         AppUpdateStatusService.scanDownloadedApks(this); | ||||
| 
 | ||||
|         // If the user changes the preference to do with filtering rooted apps, | ||||
|         // it is easier to just notify a change in the app provider, | ||||
|  | ||||
| @ -45,6 +45,12 @@ class NotificationHelper { | ||||
|     private static final int MAX_UPDATES_TO_SHOW = 5; | ||||
|     private static final int MAX_INSTALLED_TO_SHOW = 10; | ||||
| 
 | ||||
|     /** | ||||
|      * Unique ID used to represent this specific package's install process, | ||||
|      * including {@link Notification}s, also known as {@code urlString}. | ||||
|      * | ||||
|      * @see org.fdroid.fdroid.installer.InstallManagerService | ||||
|      */ | ||||
|     static final String EXTRA_NOTIFICATION_KEY = "key"; | ||||
|     private static final String GROUP_UPDATES = "updates"; | ||||
|     private static final String GROUP_INSTALLED = "installed"; | ||||
|  | ||||
| @ -12,7 +12,6 @@ import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.JobIntentService; | ||||
| import android.util.Log; | ||||
| import org.acra.ACRA; | ||||
| import org.fdroid.fdroid.AppUpdateStatusManager; | ||||
| import org.fdroid.fdroid.Utils; | ||||
| import org.fdroid.fdroid.data.Schema.InstalledAppTable; | ||||
| import rx.functions.Action1; | ||||
| @ -235,22 +234,6 @@ public class InstalledAppProviderService extends JobIntentService { | ||||
|                     try { | ||||
|                         String hashType = "sha256"; | ||||
|                         String hash = Utils.getBinaryHash(apk, hashType); | ||||
| 
 | ||||
|                         // Ensure that we no longer notify the user that this apk successfully | ||||
|                         // downloaded and is now ready to be installed. Used to be handled only | ||||
|                         // by InstallManagerService after receiving ACTION_INSTALL_COMPLETE, but | ||||
|                         // that doesn't work for F-Droid itself, which never receives that action. | ||||
|                         for (Apk apkInRepo : ApkProvider.Helper.findApksByHash(this, hash)) { | ||||
| 
 | ||||
|                             Utils.debugLog(TAG, "Noticed that " + apkInRepo.apkName + | ||||
|                                     " version " + apkInRepo.versionName + " was installed," + | ||||
|                                     " so marking as no longer pending install"); | ||||
| 
 | ||||
|                             AppUpdateStatusManager.getInstance(this) | ||||
|                                     .markAsNoLongerPendingInstall(apkInRepo.getUrl()); | ||||
| 
 | ||||
|                         } | ||||
| 
 | ||||
|                         insertAppIntoDb(this, packageInfo, hashType, hash); | ||||
|                     } catch (IllegalArgumentException e) { | ||||
|                         Utils.debugLog(TAG, e.getMessage()); | ||||
|  | ||||
| @ -63,8 +63,6 @@ public class DefaultInstaller extends Installer { | ||||
| 
 | ||||
|     @Override | ||||
|     protected void uninstallPackage() { | ||||
|         sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED); | ||||
| 
 | ||||
|         Intent uninstallIntent = new Intent(context, DefaultInstallerActivity.class); | ||||
|         uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE); | ||||
|         uninstallIntent.putExtra(Installer.EXTRA_APK, apk); | ||||
|  | ||||
| @ -78,8 +78,6 @@ public class ExtensionInstaller extends Installer { | ||||
| 
 | ||||
|     @Override | ||||
|     protected void uninstallPackage() { | ||||
|         sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED); | ||||
| 
 | ||||
|         Intent uninstallIntent = new Intent(context, InstallExtensionDialogActivity.class); | ||||
|         uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); | ||||
| 
 | ||||
|  | ||||
| @ -68,8 +68,6 @@ public class FileInstaller extends Installer { | ||||
| 
 | ||||
|     @Override | ||||
|     protected void uninstallPackage() { | ||||
|         sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED); | ||||
| 
 | ||||
|         Intent uninstallIntent = new Intent(context, FileInstallerActivity.class); | ||||
|         uninstallIntent.setAction(FileInstallerActivity.ACTION_UNINSTALL_FILE); | ||||
|         uninstallIntent.putExtra(Installer.EXTRA_APK, apk); | ||||
|  | ||||
| @ -7,9 +7,11 @@ import android.content.BroadcastReceiver; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.IntentFilter; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.pm.PackageInfo; | ||||
| import android.net.Uri; | ||||
| import android.os.IBinder; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.v4.content.LocalBroadcastManager; | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| @ -40,6 +42,10 @@ import java.io.IOException; | ||||
|  * For example, if this {@code InstallManagerService} gets killed, Android will cache | ||||
|  * and then redeliver the {@link Intent} for us, which includes all of the data needed | ||||
|  * for {@code InstallManagerService} to do its job for the whole lifecycle of an install. | ||||
|  * This {@code Service} never stops itself after completing the action, e.g. | ||||
|  * {@code {@link #stopSelf(int)}}, so {@code Intent}s are sometimes redelivered even | ||||
|  * though they are no longer valid.  {@link #onStartCommand(Intent, int, int)} checks | ||||
|  * first that the incoming {@code Intent} is not an invalid, redelivered {@code Intent}. | ||||
|  * <p> | ||||
|  * The canonical URL for the APK file to download is also used as the unique ID to | ||||
|  * represent the download itself throughout F-Droid.  This follows the model | ||||
| @ -61,6 +67,11 @@ import java.io.IOException; | ||||
|  * This also handles downloading OBB "APK Extension" files for any APK that has one | ||||
|  * assigned to it.  OBB files are queued up for download before the APK so that they | ||||
|  * are hopefully in place before the APK starts.  That is not guaranteed though. | ||||
|  * <p> | ||||
|  * There may be multiple, available APK files with the same hash. Although it | ||||
|  * is not a security issue to install one or the other, they may have different | ||||
|  * metadata to display in the client.  Thus, it may result in weirdness if one | ||||
|  * has a different name/description/summary, etc). | ||||
|  * | ||||
|  * @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a> | ||||
|  */ | ||||
| @ -71,19 +82,11 @@ public class InstallManagerService extends Service { | ||||
|     private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.action.INSTALL"; | ||||
|     private static final String ACTION_CANCEL = "org.fdroid.fdroid.installer.action.CANCEL"; | ||||
| 
 | ||||
|     /** | ||||
|      * The install manager service needs to monitor downloaded apks so that it can wait for a user to | ||||
|      * install them and respond accordingly. Usually the thing which starts listening for such events | ||||
|      * does so directly after a download is complete. This works great, except when the user then | ||||
|      * subsequently closes F-Droid and opens it at a later date. Under these circumstances, a background | ||||
|      * service will scan all downloaded apks and notify the user about them. When it does so, the | ||||
|      * install manager service needs to add listeners for if the apks get installed. | ||||
|      */ | ||||
|     private static final String ACTION_MANAGE_DOWNLOADED_APKS = "org.fdroid.fdroid.installer.action.ACTION_MANAGE_DOWNLOADED_APKS"; | ||||
| 
 | ||||
|     private static final String EXTRA_APP = "org.fdroid.fdroid.installer.extra.APP"; | ||||
|     private static final String EXTRA_APK = "org.fdroid.fdroid.installer.extra.APK"; | ||||
| 
 | ||||
|     private static SharedPreferences pendingInstalls; | ||||
| 
 | ||||
|     private LocalBroadcastManager localBroadcastManager; | ||||
|     private AppUpdateStatusManager appUpdateStatusManager; | ||||
|     private BroadcastReceiver broadcastReceiver; | ||||
| @ -119,8 +122,16 @@ public class InstallManagerService extends Service { | ||||
|         intentFilter.addDataScheme("package"); | ||||
|         registerReceiver(broadcastReceiver, intentFilter); | ||||
|         running = true; | ||||
|         pendingInstalls = getPendingInstalls(this); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * If this {@link Service} is stopped, then all of the various | ||||
|      * {@link BroadcastReceiver}s need to unregister themselves if they get | ||||
|      * called.  There can be multiple {@code BroadcastReceiver}s registered, | ||||
|      * so it can't be done with a simple call here. So {@link #running} is the | ||||
|      * signal to all the existing {@code BroadcastReceiver}s to unregister. | ||||
|      */ | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         running = false; | ||||
| @ -145,19 +156,14 @@ public class InstallManagerService extends Service { | ||||
|     public int onStartCommand(Intent intent, int flags, int startId) { | ||||
|         Utils.debugLog(TAG, "onStartCommand " + intent); | ||||
| 
 | ||||
|         String action = intent.getAction(); | ||||
| 
 | ||||
|         if (ACTION_MANAGE_DOWNLOADED_APKS.equals(action)) { | ||||
|             registerInstallerReceiversForDownlaodedApks(); | ||||
|             return START_NOT_STICKY; | ||||
|         } | ||||
| 
 | ||||
|         String urlString = intent.getDataString(); | ||||
|         if (TextUtils.isEmpty(urlString)) { | ||||
|             Utils.debugLog(TAG, "empty urlString, nothing to do"); | ||||
|             return START_NOT_STICKY; | ||||
|         } | ||||
| 
 | ||||
|         String action = intent.getAction(); | ||||
| 
 | ||||
|         if (ACTION_CANCEL.equals(action)) { | ||||
|             DownloaderService.cancel(this, urlString); | ||||
|             Apk apk = appUpdateStatusManager.getApk(urlString); | ||||
| @ -165,10 +171,14 @@ public class InstallManagerService extends Service { | ||||
|                 DownloaderService.cancel(this, apk.getPatchObbUrl()); | ||||
|                 DownloaderService.cancel(this, apk.getMainObbUrl()); | ||||
|             } | ||||
|             appUpdateStatusManager.markAsNoLongerPendingInstall(urlString); | ||||
|             appUpdateStatusManager.removeApk(urlString); | ||||
|             return START_NOT_STICKY; | ||||
|         } else if (!ACTION_INSTALL.equals(action)) { | ||||
|         } else if (ACTION_INSTALL.equals(action)) { | ||||
|             if (!isPendingInstall(urlString)) { | ||||
|                 Log.i(TAG, "Ignoring INSTALL that is not Pending Install: " + intent); | ||||
|                 return START_NOT_STICKY; | ||||
|             } | ||||
|         } else { | ||||
|             Log.i(TAG, "Ignoring unknown intent action: " + intent); | ||||
|             return START_NOT_STICKY; | ||||
|         } | ||||
| @ -204,9 +214,8 @@ public class InstallManagerService extends Service { | ||||
|         DownloaderService.setTimeout(FDroidApp.getTimeout()); | ||||
| 
 | ||||
|         appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Downloading, null); | ||||
|         appUpdateStatusManager.markAsPendingInstall(urlString); | ||||
| 
 | ||||
|         registerApkDownloaderReceivers(urlString); | ||||
|         registerPackageDownloaderReceivers(urlString); | ||||
|         getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256); | ||||
|         getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256); | ||||
| 
 | ||||
| @ -304,7 +313,11 @@ public class InstallManagerService extends Service { | ||||
|                 DownloaderService.getIntentFilter(obbUrlString)); | ||||
|     } | ||||
| 
 | ||||
|     private void registerApkDownloaderReceivers(String urlString) { | ||||
|     /** | ||||
|      * Register a {@link BroadcastReceiver} for tracking download progress for a | ||||
|      * give {@code urlString}.  There can be multiple of these registered at a time. | ||||
|      */ | ||||
|     private void registerPackageDownloaderReceivers(String urlString) { | ||||
| 
 | ||||
|         BroadcastReceiver downloadReceiver = new BroadcastReceiver() { | ||||
|             @Override | ||||
| @ -340,7 +353,7 @@ public class InstallManagerService extends Service { | ||||
|                         appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.ReadyToInstall, null); | ||||
| 
 | ||||
|                         localBroadcastManager.unregisterReceiver(this); | ||||
|                         registerInstallerReceivers(downloadUri); | ||||
|                         registerInstallReceiver(downloadUri); | ||||
| 
 | ||||
|                         Apk apk = appUpdateStatusManager.getApk(urlString); | ||||
|                         if (apk != null) { | ||||
| @ -348,7 +361,6 @@ public class InstallManagerService extends Service { | ||||
|                         } | ||||
|                         break; | ||||
|                     case Downloader.ACTION_INTERRUPTED: | ||||
|                         appUpdateStatusManager.markAsNoLongerPendingInstall(urlString); | ||||
|                         appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)); | ||||
|                         localBroadcastManager.unregisterReceiver(this); | ||||
|                         break; | ||||
| @ -357,7 +369,6 @@ public class InstallManagerService extends Service { | ||||
|                             DownloaderService.queue(context, FDroidApp.getMirror(mirrorUrlString, repoId), repoId, urlString); | ||||
|                             DownloaderService.setTimeout(FDroidApp.getTimeout()); | ||||
|                         } catch (IOException e) { | ||||
|                             appUpdateStatusManager.markAsNoLongerPendingInstall(urlString); | ||||
|                             appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)); | ||||
|                             localBroadcastManager.unregisterReceiver(this); | ||||
|                         } | ||||
| @ -373,20 +384,10 @@ public class InstallManagerService extends Service { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * For each app in the {@link AppUpdateStatusManager.Status#ReadyToInstall} state, setup listeners | ||||
|      * so that if the user installs it then we can respond accordingly. This makes sure that whether | ||||
|      * the user just finished downloading it, or whether they downloaded it a day ago but have not yet | ||||
|      * installed it, we get the same experience upon completing an install. | ||||
|      * Register a {@link BroadcastReceiver} for tracking install progress for a | ||||
|      * give {@link Uri}.  There can be multiple of these registered at a time. | ||||
|      */ | ||||
|     private void registerInstallerReceiversForDownlaodedApks() { | ||||
|         for (AppUpdateStatusManager.AppUpdateStatus appStatus : AppUpdateStatusManager.getInstance(this).getAll()) { | ||||
|             if (appStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) { | ||||
|                 registerInstallerReceivers(Uri.parse(appStatus.getUniqueKey())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void registerInstallerReceivers(Uri downloadUri) { | ||||
|     private void registerInstallReceiver(Uri downloadUri) { | ||||
| 
 | ||||
|         BroadcastReceiver installReceiver = new BroadcastReceiver() { | ||||
|             @Override | ||||
| @ -402,9 +403,8 @@ public class InstallManagerService extends Service { | ||||
|                         appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installing, null); | ||||
|                         break; | ||||
|                     case Installer.ACTION_INSTALL_COMPLETE: | ||||
|                         appUpdateStatusManager.markAsNoLongerPendingInstall(downloadUrl); | ||||
|                         appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installed, null); | ||||
|                         Apk apkComplete =  appUpdateStatusManager.getApk(downloadUrl); | ||||
|                         Apk apkComplete = appUpdateStatusManager.getApk(downloadUrl); | ||||
| 
 | ||||
|                         if (apkComplete != null && apkComplete.isApk()) { | ||||
|                             try { | ||||
| @ -419,7 +419,6 @@ public class InstallManagerService extends Service { | ||||
|                         apk = intent.getParcelableExtra(Installer.EXTRA_APK); | ||||
|                         String errorMessage = | ||||
|                                 intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); | ||||
|                         appUpdateStatusManager.markAsNoLongerPendingInstall(downloadUrl); | ||||
|                         if (!TextUtils.isEmpty(errorMessage)) { | ||||
|                             appUpdateStatusManager.setApkError(apk, errorMessage); | ||||
|                         } else { | ||||
| @ -443,13 +442,20 @@ public class InstallManagerService extends Service { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Install an APK, checking the cache and downloading if necessary before starting the process. | ||||
|      * All notifications are sent as an {@link Intent} via local broadcasts to be received by | ||||
|      * Install an APK, checking the cache and downloading if necessary before | ||||
|      * starting the process.  All notifications are sent as an {@link Intent} | ||||
|      * via local broadcasts to be received by {@link BroadcastReceiver}s per | ||||
|      * {@code urlString}.  This also marks a given APK as in the process of | ||||
|      * being installed, with the {@code urlString} of the download used as the | ||||
|      * unique ID, | ||||
|      * <p> | ||||
|      * and the file hash used to verify that things are the same. | ||||
|      * | ||||
|      * @param context this app's {@link Context} | ||||
|      */ | ||||
|     public static void queue(Context context, App app, Apk apk) { | ||||
|     public static void queue(Context context, App app, @NonNull Apk apk) { | ||||
|         String urlString = apk.getUrl(); | ||||
|         putPendingInstall(context, urlString, apk.packageName); | ||||
|         Uri downloadUri = Uri.parse(urlString); | ||||
|         Installer.sendBroadcastInstall(context, downloadUri, Installer.ACTION_INSTALL_STARTED, apk, | ||||
|                 null, null); | ||||
| @ -463,15 +469,46 @@ public class InstallManagerService extends Service { | ||||
|     } | ||||
| 
 | ||||
|     public static void cancel(Context context, String urlString) { | ||||
|         removePendingInstall(context, urlString); | ||||
|         Intent intent = new Intent(context, InstallManagerService.class); | ||||
|         intent.setAction(ACTION_CANCEL); | ||||
|         intent.setData(Uri.parse(urlString)); | ||||
|         context.startService(intent); | ||||
|     } | ||||
| 
 | ||||
|     public static void managePreviouslyDownloadedApks(Context context) { | ||||
|         Intent intent = new Intent(context, InstallManagerService.class); | ||||
|         intent.setAction(ACTION_MANAGE_DOWNLOADED_APKS); | ||||
|         context.startService(intent); | ||||
|     /** | ||||
|      * Is the APK that matches the provided {@code hash} still waiting to be | ||||
|      * installed?  This restarts the install process for this APK if it was | ||||
|      * interrupted somehow, like if F-Droid was killed before the download | ||||
|      * completed, or the device lost power in the middle of the install | ||||
|      * process. | ||||
|      */ | ||||
|     public boolean isPendingInstall(String urlString) { | ||||
|         return pendingInstalls.contains(urlString); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Mark a given APK as in the process of being installed, with | ||||
|      * the {@code urlString} of the download used as the unique ID, | ||||
|      * and the file hash used to verify that things are the same. | ||||
|      * | ||||
|      * @see #isPendingInstall(String) | ||||
|      */ | ||||
|     public static void putPendingInstall(Context context, String urlString, String packageName) { | ||||
|         if (pendingInstalls == null) { | ||||
|             pendingInstalls = getPendingInstalls(context); | ||||
|         } | ||||
|         pendingInstalls.edit().putString(urlString, packageName).apply(); | ||||
|     } | ||||
| 
 | ||||
|     public static void removePendingInstall(Context context, String urlString) { | ||||
|         if (pendingInstalls == null) { | ||||
|             pendingInstalls = getPendingInstalls(context); | ||||
|         } | ||||
|         pendingInstalls.edit().remove(urlString).apply(); | ||||
|     } | ||||
| 
 | ||||
|     private static SharedPreferences getPendingInstalls(Context context) { | ||||
|         return context.getSharedPreferences("pending-installs", Context.MODE_PRIVATE); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -200,6 +200,15 @@ public abstract class Installer { | ||||
|     } | ||||
| 
 | ||||
|     private void sendBroadcastUninstall(String action, PendingIntent pendingIntent, String errorMessage) { | ||||
|         sendBroadcastUninstall(context, apk, action, pendingIntent, errorMessage); | ||||
|     } | ||||
| 
 | ||||
|     static void sendBroadcastUninstall(Context context, Apk apk, String action) { | ||||
|         sendBroadcastUninstall(context, apk, action, null, null); | ||||
|     } | ||||
| 
 | ||||
|     private static void sendBroadcastUninstall(Context context, Apk apk, String action, | ||||
|                                                PendingIntent pendingIntent, String errorMessage) { | ||||
|         Uri uri = Uri.fromParts("package", apk.packageName, null); | ||||
| 
 | ||||
|         Intent intent = new Intent(action); | ||||
|  | ||||
| @ -142,6 +142,9 @@ public class InstallerService extends JobIntentService { | ||||
|      */ | ||||
|     public static void uninstall(Context context, @NonNull Apk apk) { | ||||
|         Objects.requireNonNull(apk); | ||||
| 
 | ||||
|         Installer.sendBroadcastUninstall(context, apk, Installer.ACTION_UNINSTALL_STARTED); | ||||
| 
 | ||||
|         Intent intent = new Intent(context, InstallerService.class); | ||||
|         intent.setAction(ACTION_UNINSTALL); | ||||
|         intent.putExtra(Installer.EXTRA_APK, apk); | ||||
|  | ||||
| @ -355,8 +355,6 @@ public class PrivilegedInstaller extends Installer { | ||||
| 
 | ||||
|     @Override | ||||
|     protected void uninstallPackage() { | ||||
|         sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED); | ||||
| 
 | ||||
|         ServiceConnection mServiceConnection = new ServiceConnection() { | ||||
|             public void onServiceConnected(ComponentName name, IBinder service) { | ||||
|                 IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service); | ||||
|  | ||||
| @ -34,9 +34,7 @@ import android.widget.LinearLayout; | ||||
| import android.widget.ProgressBar; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
| 
 | ||||
| import com.nostra13.universalimageloader.core.ImageLoader; | ||||
| 
 | ||||
| import org.fdroid.fdroid.Preferences; | ||||
| import org.fdroid.fdroid.R; | ||||
| import org.fdroid.fdroid.Utils; | ||||
| @ -204,12 +202,20 @@ public class AppDetailsRecyclerViewAdapter | ||||
|     } | ||||
| 
 | ||||
|     public void clearProgress() { | ||||
|         setProgress(0, 0, 0); | ||||
|         if (headerView != null) { | ||||
|             headerView.clearProgress(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void setProgress(long bytesDownloaded, long totalBytes, int resIdString) { | ||||
|     public void setIndeterminateProgress(int resIdString) { | ||||
|         if (headerView != null) { | ||||
|             headerView.setProgress(bytesDownloaded, totalBytes, resIdString); | ||||
|             headerView.setIndeterminateProgress(resIdString); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void setProgress(long bytesDownloaded, long totalBytes) { | ||||
|         if (headerView != null) { | ||||
|             headerView.setProgress(bytesDownloaded, totalBytes); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -360,38 +366,39 @@ public class AppDetailsRecyclerViewAdapter | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         public void setProgress(long bytesDownloaded, long totalBytes, int resIdString) { | ||||
|             if (bytesDownloaded == 0 && totalBytes == 0) { | ||||
|                 // Remove progress bar | ||||
|                 progressLayout.setVisibility(View.GONE); | ||||
|                 buttonLayout.setVisibility(View.VISIBLE); | ||||
|             } else { | ||||
|                 progressBar.setMax(Utils.bytesToKb(totalBytes)); | ||||
|                 progressBar.setProgress(Utils.bytesToKb(bytesDownloaded)); | ||||
|                 progressBar.setIndeterminate(totalBytes == -1); | ||||
|                 progressLabel.setContentDescription(""); | ||||
|                 if (resIdString != 0) { | ||||
|                     progressLabel.setText(resIdString); | ||||
|                     progressLabel.setContentDescription(context.getString(R.string.downloading)); | ||||
|                     progressPercent.setText(""); | ||||
|                 } else if (totalBytes > 0 && bytesDownloaded >= 0) { | ||||
|                     int percent = Utils.getPercent(bytesDownloaded, totalBytes); | ||||
|                     progressLabel.setText(Utils.getFriendlySize(bytesDownloaded) | ||||
|                             + " / " + Utils.getFriendlySize(totalBytes)); | ||||
|                     progressLabel.setContentDescription(context.getString(R.string.app__tts__downloading_progress, | ||||
|                             percent)); | ||||
|                     progressPercent.setText(String.format(Locale.ENGLISH, "%d%%", percent)); | ||||
|                 } else if (bytesDownloaded >= 0) { | ||||
|                     progressLabel.setText(Utils.getFriendlySize(bytesDownloaded)); | ||||
|                     progressLabel.setContentDescription(context.getString(R.string.downloading)); | ||||
|                     progressPercent.setText(""); | ||||
|                 } | ||||
|         public void clearProgress() { | ||||
|             progressLayout.setVisibility(View.GONE); | ||||
|             buttonLayout.setVisibility(View.VISIBLE); | ||||
|         } | ||||
| 
 | ||||
|                 // Make sure it's visible | ||||
|                 if (progressLayout.getVisibility() != View.VISIBLE) { | ||||
|                     progressLayout.setVisibility(View.VISIBLE); | ||||
|                     buttonLayout.setVisibility(View.GONE); | ||||
|                 } | ||||
|         public void setIndeterminateProgress(int resIdString) { | ||||
|             progressLayout.setVisibility(View.VISIBLE); | ||||
|             buttonLayout.setVisibility(View.GONE); | ||||
|             progressBar.setIndeterminate(true); | ||||
|             progressLabel.setText(resIdString); | ||||
|             progressLabel.setContentDescription(context.getString(R.string.downloading)); | ||||
|             progressPercent.setText(""); | ||||
|         } | ||||
| 
 | ||||
|         public void setProgress(long bytesDownloaded, long totalBytes) { | ||||
|             progressLayout.setVisibility(View.VISIBLE); | ||||
|             buttonLayout.setVisibility(View.GONE); | ||||
| 
 | ||||
|             progressBar.setMax(Utils.bytesToKb(totalBytes)); | ||||
|             progressBar.setProgress(Utils.bytesToKb(bytesDownloaded)); | ||||
|             progressBar.setIndeterminate(totalBytes <= 0); | ||||
|             progressLabel.setContentDescription(""); | ||||
|             if (totalBytes > 0 && bytesDownloaded >= 0) { | ||||
|                 int percent = Utils.getPercent(bytesDownloaded, totalBytes); | ||||
|                 progressLabel.setText(Utils.getFriendlySize(bytesDownloaded) | ||||
|                         + " / " + Utils.getFriendlySize(totalBytes)); | ||||
|                 progressLabel.setContentDescription(context.getString(R.string.app__tts__downloading_progress, | ||||
|                         percent)); | ||||
|                 progressPercent.setText(String.format(Locale.ENGLISH, "%d%%", percent)); | ||||
|             } else if (bytesDownloaded >= 0) { | ||||
|                 progressLabel.setText(Utils.getFriendlySize(bytesDownloaded)); | ||||
|                 progressLabel.setContentDescription(context.getString(R.string.downloading)); | ||||
|                 progressPercent.setText(""); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -395,7 +395,6 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB | ||||
|      * There are a bunch of reasons why we would get notified about app statuses. | ||||
|      * The ones we are interested in are those which would result in the "items requiring user interaction" | ||||
|      * to increase or decrease: | ||||
|      * * Bulk updates of ready-to-install-apps (relating to {@link org.fdroid.fdroid.AppUpdateStatusService}. | ||||
|      * * Change in status to: | ||||
|      * * {@link AppUpdateStatusManager.Status#ReadyToInstall} (Causes the count to go UP by one) | ||||
|      * * {@link AppUpdateStatusManager.Status#Installed} (Causes the count to go DOWN by one) | ||||
|  | ||||
| @ -61,12 +61,6 @@ public class AppStatusListItemController extends AppListItemController { | ||||
|             AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(activity); | ||||
|             manager.removeApk(status.getUniqueKey()); | ||||
|             switch (status.status) { | ||||
|                 case ReadyToInstall: | ||||
|                     manager.markAsNoLongerPendingInstall(status); | ||||
|                     // Do this silently, because it should be pretty obvious based on the context | ||||
|                     // of a "Ready to install" app being dismissed. | ||||
|                     break; | ||||
| 
 | ||||
|                 case Downloading: | ||||
|                     cancelDownload(); | ||||
|                     message = activity.getString(R.string.app_list__dismiss_downloading_app); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Hans-Christoph Steiner
						Hans-Christoph Steiner