diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9fb2fe575..c1c25f546 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -465,6 +465,9 @@ + diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 6dc0f496b..5e5356cd5 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -309,6 +309,8 @@ public class App extends ValueObject implements Comparable { this.name = (String) appInfo.loadLabel(pm); this.icon = getIconName(packageName, packageInfo.versionCode); + this.installedVersionName = packageInfo.versionName; + this.installedVersionCode = packageInfo.versionCode; this.compatible = true; } diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java new file mode 100644 index 000000000..a3674f9d9 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java @@ -0,0 +1,84 @@ +package org.fdroid.fdroid.localrepo; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import org.apache.commons.io.FileUtils; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.App; + +import java.io.File; +import java.io.IOException; +import java.security.cert.CertificateEncodingException; + +/** + * An {@link IntentService} subclass for generating cached info about the installed APKs + * which are available for swapping. It does not cache system apps, since those are + * rarely swapped. This is meant to start running when {@link SwapService} starts. + *

+ * This could probably be replaced by {@link org.fdroid.fdroid.data.InstalledAppProvider} + * if that contained all of the info to generate complete {@link App} and + * {@link org.fdroid.fdroid.data.Apk} instances. + */ +public class CacheSwapAppsService extends IntentService { + public static final String TAG = "CacheSwapAppsService"; + + private static final String ACTION_PARSE_APP = "org.fdroid.fdroid.localrepo.action.PARSE_APP"; + + public CacheSwapAppsService() { + super("CacheSwapAppsService"); + } + + /** + * Parse the locally installed APK for {@code packageName} and save its XML + * to the APK XML cache. + */ + public static void parseApp(Context context, Intent intent) { + intent.setClass(context, CacheSwapAppsService.class); + intent.setAction(ACTION_PARSE_APP); + context.startService(intent); + } + + /** + * Parse all of the locally installed APKs into a memory cache, starting + * with the currently selected apps. APKs that are already parsed in the + * {@code index.jar} file will be read from that file. + */ + public static void startCaching(Context context) { + File indexJarFile = LocalRepoManager.get(context).getIndexJar(); + PackageManager pm = context.getPackageManager(); + for (ApplicationInfo applicationInfo : pm.getInstalledApplications(0)) { + if (applicationInfo.publicSourceDir.startsWith(FDroidApp.SYSTEM_DIR_NAME)) { + continue; + } + if (!indexJarFile.exists() + || FileUtils.isFileNewer(new File(applicationInfo.sourceDir), indexJarFile)) { + Intent intent = new Intent(); + intent.setData(Uri.parse("package:" + applicationInfo.packageName)); + parseApp(context, intent); + } + } + } + + @Override + protected void onHandleIntent(Intent intent) { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); + if (intent == null || !ACTION_PARSE_APP.equals(intent.getAction())) { + Utils.debugLog(TAG, "received bad Intent: " + intent); + return; + } + + try { + PackageManager pm = getPackageManager(); + String packageName = intent.getData().getSchemeSpecificPart(); + App app = new App(this, pm, packageName); + SwapService.putAppInCache(packageName, app); + } catch (CertificateEncodingException | IOException | PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java index 32978983b..3cf9b79b3 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -257,7 +257,10 @@ public final class LocalRepoManager { public void addApp(Context context, String packageName) { App app; try { - app = new App(context.getApplicationContext(), pm, packageName); + app = SwapService.getAppFromCache(packageName); + if (app == null) { + app = new App(context.getApplicationContext(), pm, packageName); + } if (!app.isValid()) { return; } diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index fcadd5176..8414b05d8 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -31,6 +31,7 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.localrepo.peers.Peer; @@ -52,6 +53,7 @@ import java.util.List; import java.util.Set; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; import rx.Observable; import rx.Subscription; @@ -61,23 +63,6 @@ import rx.schedulers.Schedulers; /** * Central service which manages all of the different moving parts of swap which are required * to enable p2p swapping of apps. - * - * The following UI elements don't do anything: - * + TODO: Be notified of changes to wifi state correctly, particularly from the WiFi AP (https://github.com/mvdan/accesspoint/issues/5) - * + TODO: The "?" button in the top right of the swap start screen doesn't do anything - * (This has been commented out for now, but it is still preferable to have a working help mechanism) - * - * TODO: Show "Waiting for other device to finish setting up swap" when only F-Droid shown in swap - * TODO: Handle "not connected to wifi" more gracefully. For example, Bonjour discovery falls over. - * TODO: When unable to reach the swap repo, but viewing apps to swap, show relevant feedback when attempting to download and install. - * TODO: Remove peers from list of peers when no longer "visible". - * TODO: Feedback for "Setting up (wifi|bluetooth)" in start swap view is not as immediate as I had hoped. - * TODO: Turn off bluetooth after cancelling/timing out if we turned it on. - * TODO: Disable the Scan QR button unless visible via something. Could equally show relevant feedback. - * - * TODO: Starting wifi after cancelling swap and beginning again doesn't work properly - * TODO: Scan QR hangs when updating repoo. Swapper was 2.3.3 and Swappee was 5.0 - * TODO: Showing the progress bar during install doesn't work when the view is inflated again, or when the adapter is scrolled off screen and back again. */ public class SwapService extends Service { @@ -90,6 +75,19 @@ public class SwapService extends Service { @NonNull private final Set appsToSwap = new HashSet<>(); + /** + * A cache of parsed APKs from the file system. + */ + private static final ConcurrentHashMap INSTALLED_APPS = new ConcurrentHashMap<>(); + + static App getAppFromCache(String packageName) { + return INSTALLED_APPS.get(packageName); + } + + static void putAppInCache(String packageName, App app) { + INSTALLED_APPS.put(packageName, app); + } + /** * Where relevant, the state of the swap process will be saved to disk using preferences. * Note that this is not always useful, for example saving the "current wifi network" is @@ -516,6 +514,8 @@ public class SwapService extends Service { Utils.debugLog(TAG, "Creating swap service."); + CacheSwapAppsService.startCaching(this); + SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); appsToSwap.addAll(deserializePackages(preferences.getString(KEY_APPS_TO_SWAP, "")));