diff --git a/app/src/androidTest/java/android/test/ProviderTestCase2MockContext.java b/app/src/androidTest/java/android/test/ProviderTestCase2MockContext.java index 659fba2f3..405902649 100644 --- a/app/src/androidTest/java/android/test/ProviderTestCase2MockContext.java +++ b/app/src/androidTest/java/android/test/ProviderTestCase2MockContext.java @@ -98,6 +98,11 @@ public abstract class ProviderTestCase2MockContext ex public Context getApplicationContext() { return this; } + + @Override + public String getPackageName() { + return "org.fdroid.fdroid"; + } } /** diff --git a/app/src/androidTest/java/mock/MockApplicationInfo.java b/app/src/androidTest/java/mock/MockApplicationInfo.java index 4d88d3f9b..1ab4592a4 100644 --- a/app/src/androidTest/java/mock/MockApplicationInfo.java +++ b/app/src/androidTest/java/mock/MockApplicationInfo.java @@ -5,6 +5,9 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import java.io.File; +import java.io.IOException; + @SuppressLint("ParcelCreator") public class MockApplicationInfo extends ApplicationInfo { @@ -12,6 +15,11 @@ public class MockApplicationInfo extends ApplicationInfo { public MockApplicationInfo(PackageInfo info) { this.info = info; + try { + this.publicSourceDir = File.createTempFile(info.packageName, "apk").getAbsolutePath(); + } catch (IOException e) { + this.publicSourceDir = "/data/app/" + info.packageName + "-4.apk"; + } } @Override diff --git a/app/src/androidTest/java/mock/MockInstallablePackageManager.java b/app/src/androidTest/java/mock/MockInstallablePackageManager.java index b7056711f..1fcef1e86 100644 --- a/app/src/androidTest/java/mock/MockInstallablePackageManager.java +++ b/app/src/androidTest/java/mock/MockInstallablePackageManager.java @@ -37,6 +37,8 @@ public class MockInstallablePackageManager extends MockPackageManager { p.packageName = id; p.versionCode = version; p.versionName = versionName; + p.applicationInfo = new MockApplicationInfo(p); + p.lastUpdateTime = System.currentTimeMillis(); info.add(p); } } diff --git a/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java b/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java index 21a49518c..8b01eb630 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java @@ -4,7 +4,6 @@ import android.app.Instrumentation; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; -import android.content.Intent; import android.net.Uri; import android.os.Environment; import android.support.annotation.Nullable; @@ -15,9 +14,6 @@ import junit.framework.AssertionFailedError; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.FDroidProviderTest; -import org.fdroid.fdroid.receiver.PackageAddedReceiver; -import org.fdroid.fdroid.receiver.PackageRemovedReceiver; -import org.fdroid.fdroid.receiver.PackageUpgradedReceiver; import java.io.File; import java.io.FileOutputStream; @@ -28,9 +24,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import mock.MockContextSwappableComponents; -import mock.MockInstallablePackageManager; - public class TestUtils { private static final String TAG = "TestUtils"; @@ -146,54 +139,6 @@ public class TestUtils { return providerTest.getMockContentResolver().insert(uri, values); } - /** - * Will tell {@code pm} that we are installing {@code appId}, and then alert the - * {@link org.fdroid.fdroid.receiver.PackageAddedReceiver}. This will in turn update the - * "installed apps" table in the database. - */ - public static void installAndBroadcast(MockContextSwappableComponents context, - MockInstallablePackageManager pm, String appId, - int versionCode, String versionName) { - - context.setPackageManager(pm); - pm.install(appId, versionCode, versionName); - Intent installIntent = new Intent(Intent.ACTION_PACKAGE_ADDED); - installIntent.setData(Uri.parse("package:" + appId)); - new PackageAddedReceiver().onReceive(context, installIntent); - - } - - /** - * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String) - */ - public static void upgradeAndBroadcast(MockContextSwappableComponents context, - MockInstallablePackageManager pm, String appId, - int versionCode, String versionName) { - /* - removeAndBroadcast(context, pm, appId); - installAndBroadcast(context, pm, appId, versionCode, versionName); - */ - context.setPackageManager(pm); - pm.install(appId, versionCode, versionName); - Intent installIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED); - installIntent.setData(Uri.parse("package:" + appId)); - new PackageUpgradedReceiver().onReceive(context, installIntent); - - } - - /** - * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String) - */ - public static void removeAndBroadcast(MockContextSwappableComponents context, MockInstallablePackageManager pm, String appId) { - - context.setPackageManager(pm); - pm.remove(appId); - Intent installIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED); - installIntent.setData(Uri.parse("package:" + appId)); - new PackageRemovedReceiver().onReceive(context, installIntent); - - } - @Nullable public static File copyAssetToDir(Context context, String assetName, File directory) { File tempFile; diff --git a/app/src/androidTest/java/org/fdroid/fdroid/data/AppProviderTest.java b/app/src/androidTest/java/org/fdroid/fdroid/data/AppProviderTest.java index b19c1d819..454759c0e 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/data/AppProviderTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/data/AppProviderTest.java @@ -2,6 +2,7 @@ package org.fdroid.fdroid.data; import android.content.ContentResolver; import android.content.ContentValues; +import android.content.pm.PackageInfo; import android.content.res.Resources; import android.database.Cursor; @@ -56,24 +57,27 @@ public class AppProviderTest extends FDroidProviderTest { insertApp("com.example.app1000", "App 1000"); for (int i = 0; i < 50; i++) { - pm.install("com.example.app" + i, 1, "v" + 1); + String packageName = "com.example.app" + i; + pm.install(packageName, 1, "v" + 1); + PackageInfo packageInfo = pm.getPackageInfo(packageName, 0); + InstalledAppProviderService.insertAppIntoDb(getSwappableContext(), packageName, packageInfo); } - InstalledAppCacheUpdater.updateInForeground(getMockContext()); - assertResultCount(1, AppProvider.getInstalledUri()); for (int i = 50; i < 500; i++) { - pm.install("com.example.app" + i, 1, "v" + 1); + String packageName = "com.example.app" + i; + pm.install(packageName, 1, "v" + 1); + PackageInfo packageInfo = pm.getPackageInfo(packageName, 0); + InstalledAppProviderService.insertAppIntoDb(getSwappableContext(), packageName, packageInfo); } - InstalledAppCacheUpdater.updateInForeground(getMockContext()); - assertResultCount(2, AppProvider.getInstalledUri()); for (int i = 500; i < 1100; i++) { - pm.install("com.example.app" + i, 1, "v" + 1); + String packageName = "com.example.app" + i; + pm.install(packageName, 1, "v" + 1); + PackageInfo packageInfo = pm.getPackageInfo(packageName, 0); + InstalledAppProviderService.insertAppIntoDb(getSwappableContext(), packageName, packageInfo); } - InstalledAppCacheUpdater.updateInForeground(getMockContext()); - assertResultCount(3, AppProvider.getInstalledUri()); } @@ -127,7 +131,7 @@ public class AppProviderTest extends FDroidProviderTest { values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVercode); insertApp(id, "App: " + id, values); - TestUtils.installAndBroadcast(getSwappableContext(), packageManager, id, installedVercode, "v" + installedVercode); + InstalledAppProviderTest.install(getSwappableContext(), packageManager, id, installedVercode, "v" + installedVercode); } public void testCanUpdate() { @@ -247,7 +251,7 @@ public class AppProviderTest extends FDroidProviderTest { assertResultCount(0, AppProvider.getInstalledUri()); for (int i = 10; i < 20; i++) { - TestUtils.installAndBroadcast(getSwappableContext(), pm, "com.example.test." + i, i, "v1"); + InstalledAppProviderTest.install(getSwappableContext(), pm, "com.example.test." + i, i, "v1"); } assertResultCount(10, AppProvider.getInstalledUri()); diff --git a/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java b/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java index f36fcfed3..7c96e66e2 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java @@ -1,9 +1,11 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; +import android.content.pm.PackageInfo; +import android.database.Cursor; +import android.net.Uri; -import org.fdroid.fdroid.TestUtils; - +import mock.MockContextSwappableComponents; import mock.MockInstallablePackageManager; @SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics @@ -81,6 +83,39 @@ public class InstalledAppProviderTest extends FDroidProviderTest 0); + assertTrue(lastUpdateTime < System.currentTimeMillis()); + cursor.close(); + + insertInstalledApp(packageName, 11, "1.1"); + cursor = getMockContentResolver().query(uri, projection, null, null, null); + assertNotNull(cursor); + assertEquals("App \"" + packageName + "\" not installed", 1, cursor.getCount()); + cursor.moveToFirst(); + assertTrue(lastUpdateTime < cursor.getLong(cursor.getColumnIndex(InstalledAppProvider.DataColumns.LAST_UPDATE_TIME))); + cursor.close(); + } + public void testDelete() { insertInstalledApp("com.example.app1", 10, "1.0"); @@ -96,9 +131,8 @@ public class InstalledAppProviderTest extends FDroidProviderTest - + - - - - - - - - - - - - - + @@ -465,6 +453,12 @@ + + diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 95e0ea708..41eda63e0 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -55,7 +55,7 @@ import org.fdroid.fdroid.Preferences.ChangeListener; import org.fdroid.fdroid.Preferences.Theme; import org.fdroid.fdroid.compat.PRNGFixes; import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.InstalledAppCacheUpdater; +import org.fdroid.fdroid.data.InstalledAppProviderService; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.net.IconDownloader; import org.fdroid.fdroid.net.WifiStateChangeService; @@ -224,7 +224,7 @@ public class FDroidApp extends Application { curTheme = Preferences.get().getTheme(); Preferences.get().configureProxy(); - InstalledAppCacheUpdater.updateInBackground(getApplicationContext()); + InstalledAppProviderService.compareToPackageManager(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, diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 73b81d805..619e63bbc 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -267,6 +267,13 @@ public final class Utils { return b.build(); } + /** + * Create a standard {@link PackageManager} {@link Uri} for pointing to an app. + */ + public static Uri getPackageUri(String packageName) { + return Uri.parse("package:" + packageName); + } + /** * This location is only for caching, do not install directly from this location * because if the file is on the External Storage, any other app could swap out 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..b20203b89 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -294,8 +294,10 @@ public class App extends ValueObject implements Comparable { final CharSequence appDescription = appInfo.loadDescription(pm); if (TextUtils.isEmpty(appDescription)) { this.summary = "(installed by " + installerPackageLabel + ")"; - } else { + } else if (appDescription.length() > 40) { this.summary = (String) appDescription.subSequence(0, 40); + } else { + this.summary = (String) appDescription; } this.added = new Date(packageInfo.firstInstallTime); this.lastUpdated = new Date(packageInfo.lastUpdateTime); @@ -309,6 +311,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/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 72fc8e42a..5890126d5 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -103,11 +103,14 @@ class DBHelper extends SQLiteOpenHelper { + InstalledAppProvider.DataColumns.VERSION_CODE + " INT NOT NULL, " + InstalledAppProvider.DataColumns.VERSION_NAME + " TEXT NOT NULL, " + InstalledAppProvider.DataColumns.APPLICATION_LABEL + " TEXT NOT NULL, " - + InstalledAppProvider.DataColumns.SIGNATURE + " TEXT NOT NULL " + + InstalledAppProvider.DataColumns.SIGNATURE + " TEXT NOT NULL, " + + InstalledAppProvider.DataColumns.LAST_UPDATE_TIME + " INTEGER NOT NULL DEFAULT 0, " + + InstalledAppProvider.DataColumns.HASH_TYPE + " TEXT NOT NULL, " + + InstalledAppProvider.DataColumns.HASH + " TEXT NOT NULL" + " );"; private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + TABLE_INSTALLED_APP + ";"; - private static final int DB_VERSION = 55; + private static final int DB_VERSION = 56; private final Context context; @@ -199,7 +202,7 @@ class DBHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { createAppApk(db); - createInstalledApp(db); + db.execSQL(CREATE_TABLE_INSTALLED_APP); db.execSQL(CREATE_TABLE_REPO); insertRepo( @@ -287,16 +290,15 @@ class DBHelper extends SQLiteOpenHelper { addLastUpdatedToRepo(db, oldVersion); renameRepoId(db, oldVersion); populateRepoNames(db, oldVersion); - if (oldVersion < 43) createInstalledApp(db); addIsSwapToRepo(db, oldVersion); addChangelogToApp(db, oldVersion); addIconUrlLargeToApp(db, oldVersion); updateIconUrlLarge(db, oldVersion); - recreateInstalledCache(db, oldVersion); addCredentialsToRepo(db, oldVersion); addAuthorToApp(db, oldVersion); useMaxValueInMaxSdkVersion(db, oldVersion); requireTimestampInRepos(db, oldVersion); + recreateInstalledAppTable(db, oldVersion); } /** @@ -555,19 +557,19 @@ class DBHelper extends SQLiteOpenHelper { db.execSQL("create index apk_id on " + TABLE_APK + " (id);"); } - private void createInstalledApp(SQLiteDatabase db) { - Utils.debugLog(TAG, "Creating 'installed app' database table."); - db.execSQL(CREATE_TABLE_INSTALLED_APP); - } - - // If any column was added or removed, just drop the table, create it - // again and let the cache be filled from scratch again. - private void recreateInstalledCache(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 51) { + /** + * If any column was added or removed, just drop the table, create it again + * and let the cache be filled from scratch by {@link InstalledAppProviderService} + * For DB versions older than 43, this will create the {@link InstalledAppProvider} + * table for the first time. + */ + private void recreateInstalledAppTable(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 57) { return; } + Utils.debugLog(TAG, "(re)creating 'installed app' database table."); db.execSQL(DROP_TABLE_INSTALLED_APP); - createInstalledApp(db); + db.execSQL(CREATE_TABLE_INSTALLED_APP); } private static boolean columnExists(SQLiteDatabase db, diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java deleted file mode 100644 index 1e86bc8c9..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.fdroid.fdroid.data; - -import android.content.ContentProviderOperation; -import android.content.Context; -import android.content.OperationApplicationException; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.RemoteException; -import android.util.Log; - -import org.fdroid.fdroid.Utils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Compares what is in the fdroid_installedApp SQLite database table with the package - * info that we can gleam from the {@link android.content.pm.PackageManager}. If there - * is any updates/removals/insertions which need to take place, we will perform them. - * TODO: The content providers are not thread safe, so it is possible we will be writing - * to the database at the same time we respond to a broadcasted intent. - */ -public final class InstalledAppCacheUpdater { - - private static final String TAG = "InstalledAppCache"; - - private final Context context; - - private final List toInsert = new ArrayList<>(); - private final List toDelete = new ArrayList<>(); - - private InstalledAppCacheUpdater(Context context) { - this.context = context; - } - - /** - * Ensure our database of installed apps is in sync with what the PackageManager tells us is installed. - * Once completed, the relevant ContentProviders will be notified of any changes to installed statuses. - * This method will block until completed, which could be in the order of a few seconds (depending on - * how many apps are installed). - */ - public static void updateInForeground(Context context) { - InstalledAppCacheUpdater updater = new InstalledAppCacheUpdater(context); - if (updater.update()) { - updater.notifyProviders(); - } - } - - /** - * Ensure our database of installed apps is in sync with what the PackageManager tells us is installed. - * The installed app cache hasn't gotten out of sync somehow, e.g. if we crashed/ran out of battery - * half way through responding to a package installed {@link android.content.Intent}. Once completed, - * the relevant {@link android.content.ContentProvider}s will be notified of any changes to installed - * statuses. This method returns immediately, and will continue to work in an AsyncTask. It doesn't - * really matter where we put this in the bootstrap process, because it runs on a different thread, - * which will be delayed by some seconds to avoid an error where the database is locked due to the - * database updater. - */ - public static void updateInBackground(Context context) { - InstalledAppCacheUpdater updater = new InstalledAppCacheUpdater(context); - updater.startBackgroundWorker(); - } - - private boolean update() { - - long startTime = System.currentTimeMillis(); - - compareCacheToPackageManager(); - updateCache(); - - long duration = System.currentTimeMillis() - startTime; - Utils.debugLog(TAG, "Took " + duration + "ms to compare the installed app cache with PackageManager."); - - return hasChanged(); - } - - private void notifyProviders() { - Utils.debugLog(TAG, "Installed app cache has changed, notifying content providers (so they can update the relevant views)."); - context.getContentResolver().notifyChange(AppProvider.getContentUri(), null); - context.getContentResolver().notifyChange(ApkProvider.getContentUri(), null); - } - - private void startBackgroundWorker() { - new PostponedWorker().execute(); - } - - /** - * If any of the cached app details have been removed, updated or inserted, - * then the cache has changed. - */ - private boolean hasChanged() { - return toInsert.size() > 0 || toDelete.size() > 0; - } - - private void updateCache() { - - ArrayList ops = new ArrayList<>(); - ops.addAll(deleteFromCache(toDelete)); - ops.addAll(insertIntoCache(toInsert)); - - if (ops.size() > 0) { - try { - context.getContentResolver().applyBatch(InstalledAppProvider.getAuthority(), ops); - Utils.debugLog(TAG, "Finished executing " + ops.size() + " CRUD operations on installed app cache."); - } catch (RemoteException | OperationApplicationException e) { - Log.e(TAG, "Error updating installed app cache: " + e); - } - } - - } - - private void compareCacheToPackageManager() { - - Map cachedInfo = InstalledAppProvider.Helper.all(context); - - List installedPackages = context.getPackageManager() - .getInstalledPackages(PackageManager.GET_SIGNATURES); - for (PackageInfo appInfo : installedPackages) { - toInsert.add(appInfo); - if (cachedInfo.containsKey(appInfo.packageName)) { - cachedInfo.remove(appInfo.packageName); - } - } - - if (cachedInfo.size() > 0) { - for (Map.Entry entry : cachedInfo.entrySet()) { - toDelete.add(entry.getKey()); - } - } - } - - private List insertIntoCache(List appsToInsert) { - List ops = new ArrayList<>(appsToInsert.size()); - if (appsToInsert.size() > 0) { - Utils.debugLog(TAG, "Preparing to cache installed info for " + appsToInsert.size() + " new apps."); - Uri uri = InstalledAppProvider.getContentUri(); - for (PackageInfo info : appsToInsert) { - ContentProviderOperation op = ContentProviderOperation.newInsert(uri) - .withValue(InstalledAppProvider.DataColumns.PACKAGE_NAME, info.packageName) - .withValue(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode) - .withValue(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName) - .withValue(InstalledAppProvider.DataColumns.APPLICATION_LABEL, - InstalledAppProvider.getApplicationLabel(context, info.packageName)) - .withValue(InstalledAppProvider.DataColumns.SIGNATURE, - InstalledAppProvider.getPackageSig(info)) - .build(); - ops.add(op); - } - } - return ops; - } - - private List deleteFromCache(List packageNames) { - List ops = new ArrayList<>(packageNames.size()); - if (packageNames.size() > 0) { - Utils.debugLog(TAG, "Preparing to remove " + packageNames.size() + " apps from the installed app cache."); - for (final String packageName : packageNames) { - Uri uri = InstalledAppProvider.getAppUri(packageName); - ops.add(ContentProviderOperation.newDelete(uri).build()); - } - } - return ops; - } - - /** - * Waits 5 seconds before beginning to update cache of installed apps. - * This is due to a bug where the database was locked as F-Droid was starting, - * which caused a crash. - */ - private class PostponedWorker extends AsyncTask { - - @Override - protected Boolean doInBackground(Void... params) { - try { - Thread.sleep(10000); - } catch (InterruptedException ignored) { } - return update(); - } - - @Override - protected void onPostExecute(Boolean changed) { - if (changed) { - notifyProviders(); - } - } - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java index 414e528f5..166d838f0 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -27,12 +27,12 @@ public class InstalledAppProvider extends FDroidProvider { public static class Helper { /** - * @return The keys are the app ids (package names), and their corresponding values are - * the version code which is installed. + * @return The keys are the package names, and their corresponding values are + * the {@link PackageInfo#lastUpdateTime last update time} in milliseconds. */ - public static Map all(Context context) { + public static Map all(Context context) { - Map cachedInfo = new HashMap<>(); + Map cachedInfo = new HashMap<>(); final Uri uri = InstalledAppProvider.getContentUri(); final String[] projection = InstalledAppProvider.DataColumns.ALL; @@ -43,7 +43,7 @@ public class InstalledAppProvider extends FDroidProvider { while (!cursor.isAfterLast()) { cachedInfo.put( cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.PACKAGE_NAME)), - cursor.getInt(cursor.getColumnIndex(InstalledAppProvider.DataColumns.VERSION_CODE)) + cursor.getLong(cursor.getColumnIndex(DataColumns.LAST_UPDATE_TIME)) ); cursor.moveToNext(); } @@ -64,10 +64,13 @@ public class InstalledAppProvider extends FDroidProvider { String VERSION_NAME = "versionName"; String APPLICATION_LABEL = "applicationLabel"; String SIGNATURE = "sig"; + String LAST_UPDATE_TIME = "lastUpdateTime"; + String HASH_TYPE = "hashType"; + String HASH = "hash"; String[] ALL = { _ID, PACKAGE_NAME, VERSION_CODE, VERSION_NAME, APPLICATION_LABEL, - SIGNATURE, + SIGNATURE, LAST_UPDATE_TIME, HASH_TYPE, HASH, }; } @@ -89,6 +92,9 @@ public class InstalledAppProvider extends FDroidProvider { return Uri.parse("content://" + getAuthority()); } + /** + * @return the {@link Uri} that points to a specific installed app + */ public static Uri getAppUri(String packageName) { return Uri.withAppendedPath(getContentUri(), packageName); } @@ -217,6 +223,11 @@ public class InstalledAppProvider extends FDroidProvider { return getAppUri(values.getAsString(DataColumns.PACKAGE_NAME)); } + /** + * Update is not supported for {@code InstalledAppProvider}. Instead, use + * {@link #insert(Uri, ContentValues)}, and it will overwrite the relevant + * row, if one exists. This just throws {@link UnsupportedOperationException} + */ @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { throw new UnsupportedOperationException("\"Update' not supported for installed appp provider. Instead, you should insert, and it will overwrite the relevant rows if one exists."); diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java new file mode 100644 index 000000000..3dc103bac --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -0,0 +1,203 @@ +package org.fdroid.fdroid.data; + +import android.app.IntentService; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Process; + +import org.fdroid.fdroid.Utils; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Handles all updates to {@link InstalledAppProvider}, whether checking the contents + * versus what Android says is installed, or processing {@link Intent}s that come + * from {@link android.content.BroadcastReceiver}s for {@link Intent#ACTION_PACKAGE_ADDED} + * and {@link Intent#ACTION_PACKAGE_REMOVED} + *

+ * Since {@link android.content.ContentProvider#insert(Uri, ContentValues)} does not check + * for duplicate records, it is entirely the job of this service to ensure that it is not + * inserting duplicate versions of the same installed APK. On that note, + * {@link #insertAppIntoDb(Context, String, PackageInfo)} and + * {@link #deleteAppFromDb(Context, String)} are both static methods to enable easy testing + * of this stuff. + */ +public class InstalledAppProviderService extends IntentService { + private static final String TAG = "InstalledAppProviderSer"; + + private static final String ACTION_INSERT = "org.fdroid.fdroid.data.action.INSERT"; + private static final String ACTION_DELETE = "org.fdroid.fdroid.data.action.DELETE"; + + private static final String EXTRA_PACKAGE_INFO = "org.fdroid.fdroid.data.extra.PACKAGE_INFO"; + + private ScheduledExecutorService worker; + private boolean notifyChangeNeedsSending; + + public InstalledAppProviderService() { + super("InstalledAppProviderService"); + } + + /** + * Inserts an app into {@link InstalledAppProvider} based on a {@code package:} {@link Uri}. + * This has no checks for whether it is inserting an exact duplicate, whatever is provided + * will be inserted. + */ + public static void insert(Context context, PackageInfo packageInfo) { + insert(context, Utils.getPackageUri(packageInfo.packageName), packageInfo); + } + + /** + * Inserts an app into {@link InstalledAppProvider} based on a {@code package:} {@link Uri}. + * This has no checks for whether it is inserting an exact duplicate, whatever is provided + * will be inserted. + */ + public static void insert(Context context, Uri uri) { + insert(context, uri, null); + } + + private static void insert(Context context, Uri uri, PackageInfo packageInfo) { + Intent intent = new Intent(context, InstalledAppProviderService.class); + intent.setAction(ACTION_INSERT); + intent.setData(uri); + intent.putExtra(EXTRA_PACKAGE_INFO, packageInfo); + context.startService(intent); + } + + /** + * Deletes an app from {@link InstalledAppProvider} based on a {@code package:} {@link Uri} + */ + public static void delete(Context context, String packageName) { + delete(context, Utils.getPackageUri(packageName)); + } + + /** + * Deletes an app from {@link InstalledAppProvider} based on a {@code package:} {@link Uri} + */ + public static void delete(Context context, Uri uri) { + Intent intent = new Intent(context, InstalledAppProviderService.class); + intent.setAction(ACTION_DELETE); + intent.setData(uri); + context.startService(intent); + } + + /** + * Make sure that {@link InstalledAppProvider}, our database of installed apps, + * is in sync with what the {@link PackageManager} tells us is installed. Once + * completed, the relevant {@link android.content.ContentProvider}s will be + * notified of any changes to installed statuses. + *

+ * The installed app cache could get out of sync, e.g. if F-Droid crashed/ or + * ran out of battery half way through responding to {@link Intent#ACTION_PACKAGE_ADDED}. + * This method returns immediately, and will continue to work in an + * {@link IntentService}. It doesn't really matter where we put this in the + * bootstrap process, because it runs in its own thread, at the lowest priority: + * {@link Process#THREAD_PRIORITY_LOWEST}. + */ + public static void compareToPackageManager(Context context) { + Map cachedInfo = InstalledAppProvider.Helper.all(context); + + List packageInfoList = context.getPackageManager() + .getInstalledPackages(PackageManager.GET_SIGNATURES); + for (PackageInfo packageInfo : packageInfoList) { + if (cachedInfo.containsKey(packageInfo.packageName)) { + if (packageInfo.lastUpdateTime > cachedInfo.get(packageInfo.packageName)) { + insert(context, packageInfo); + } + cachedInfo.remove(packageInfo.packageName); + } else { + insert(context, packageInfo); + } + } + + for (String packageName : cachedInfo.keySet()) { + delete(context, packageName); + } + } + + @Override + protected void onHandleIntent(Intent intent) { + Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); + if (intent != null) { + String packageName = intent.getData().getSchemeSpecificPart(); + final String action = intent.getAction(); + if (ACTION_INSERT.equals(action)) { + insertAppIntoDb(this, packageName, (PackageInfo) intent.getParcelableExtra(EXTRA_PACKAGE_INFO)); + } else if (ACTION_DELETE.equals(action)) { + deleteAppFromDb(this, packageName); + } + notifyChange(); + } + } + + static void insertAppIntoDb(Context context, String packageName, PackageInfo packageInfo) { + if (packageInfo == null) { + try { + packageInfo = context.getPackageManager().getPackageInfo(packageName, + PackageManager.GET_SIGNATURES); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return; + } + } + + Uri uri = InstalledAppProvider.getContentUri(); + ContentValues contentValues = new ContentValues(); + contentValues.put(InstalledAppProvider.DataColumns.PACKAGE_NAME, packageInfo.packageName); + contentValues.put(InstalledAppProvider.DataColumns.VERSION_CODE, packageInfo.versionCode); + contentValues.put(InstalledAppProvider.DataColumns.VERSION_NAME, packageInfo.versionName); + contentValues.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL, + InstalledAppProvider.getApplicationLabel(context, packageInfo.packageName)); + contentValues.put(InstalledAppProvider.DataColumns.SIGNATURE, + InstalledAppProvider.getPackageSig(packageInfo)); + contentValues.put(InstalledAppProvider.DataColumns.LAST_UPDATE_TIME, packageInfo.lastUpdateTime); + + String hashType = "sha256"; + String hash = Utils.getBinaryHash(new File(packageInfo.applicationInfo.publicSourceDir), hashType); + contentValues.put(InstalledAppProvider.DataColumns.HASH_TYPE, hashType); + contentValues.put(InstalledAppProvider.DataColumns.HASH, hash); + + context.getContentResolver().insert(uri, contentValues); + } + + static void deleteAppFromDb(Context context, String packageName) { + Uri uri = InstalledAppProvider.getAppUri(packageName); + context.getContentResolver().delete(uri, null, null); + } + + /** + * This notifies the users of this {@link android.content.ContentProvider} + * that the contents has changed. Since {@link Intent}s can come in slow + * or fast, and this can trigger a lot of UI updates, the actual + * notifications are rate limited to one per second. + */ + private void notifyChange() { + notifyChangeNeedsSending = true; + Runnable task = new Runnable() { + @Override + public void run() { + if (notifyChangeNeedsSending) { + Utils.debugLog(TAG, "Notifying content providers (so they can update the relevant views)."); + getContentResolver().notifyChange(AppProvider.getContentUri(), null); + getContentResolver().notifyChange(ApkProvider.getContentUri(), null); + notifyChangeNeedsSending = false; + } else { + worker.shutdown(); + worker = null; + } + } + }; + if (worker == null || worker.isShutdown()) { + worker = Executors.newSingleThreadScheduledExecutor(); + worker.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS); + } + } +} \ No newline at end of file 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..63a1fa1c0 --- /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(Utils.getPackageUri(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 9b49f0ba1..3cf9b79b3 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -1,7 +1,6 @@ package org.fdroid.fdroid.localrepo; import android.content.Context; -import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; @@ -11,17 +10,14 @@ import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; -import android.widget.Toast; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Preferences; -import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; @@ -30,20 +26,16 @@ import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; -import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.io.Writer; import java.security.cert.CertificateEncodingException; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -65,23 +57,19 @@ import java.util.jar.JarOutputStream; public final class LocalRepoManager { private static final String TAG = "LocalRepoManager"; - // For ref, official F-droid repo presently uses a maxage of 14 days - private static final String DEFAULT_REPO_MAX_AGE_DAYS = "14"; - private final Context context; private final PackageManager pm; private final AssetManager assetManager; private final String fdroidPackageName; private static final String[] WEB_ROOT_ASSET_FILES = { - "swap-icon.png", - "swap-tick-done.png", - "swap-tick-not-done.png", + "swap-icon.png", + "swap-tick-done.png", + "swap-tick-not-done.png", }; private final Map apps = new HashMap<>(); - private final SanitizedFile xmlIndex; private final SanitizedFile xmlIndexJar; private final SanitizedFile xmlIndexJarUnsigned; private final SanitizedFile webRoot; @@ -115,7 +103,6 @@ public final class LocalRepoManager { repoDir = new SanitizedFile(fdroidDir, "repo"); repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO"); iconsDir = new SanitizedFile(repoDir, "icons"); - xmlIndex = new SanitizedFile(repoDir, "index.xml"); xmlIndexJar = new SanitizedFile(repoDir, "index.jar"); xmlIndexJarUnsigned = new SanitizedFile(repoDir, "index.unsigned.jar"); @@ -270,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; } @@ -327,70 +317,52 @@ public final class LocalRepoManager { /** * Helper class to aid in constructing index.xml file. - * It uses the PullParser API, because the DOM api is only able to be serialized from - * API 8 upwards, but we support 7 at time of implementation. */ - public static class IndexXmlBuilder { + public static final class IndexXmlBuilder { + + private static IndexXmlBuilder indexXmlBuilder; + + public static IndexXmlBuilder get() throws XmlPullParserException { + if (indexXmlBuilder == null) { + indexXmlBuilder = new IndexXmlBuilder(); + } + return indexXmlBuilder; + } @NonNull private final XmlSerializer serializer; - @NonNull - private final Map apps; - - @NonNull - private final Context context; - @NonNull private final DateFormat dateToStr = new SimpleDateFormat("yyyy-MM-dd", Locale.US); - public IndexXmlBuilder(@NonNull Context context, @NonNull Map apps) throws XmlPullParserException, IOException { - this.context = context; - this.apps = apps; + private IndexXmlBuilder() throws XmlPullParserException { serializer = XmlPullParserFactory.newInstance().newSerializer(); } - public void build(File file) throws IOException, LocalRepoKeyStore.InitException { - Writer output = new FileWriter(file); - serializer.setOutput(output); + public void build(Context context, Map apps, OutputStream output) throws IOException, LocalRepoKeyStore.InitException { + serializer.setOutput(output, "UTF-8"); serializer.startDocument(null, null); - tagFdroid(); - serializer.endDocument(); - output.close(); - } - - private void tagFdroid() throws IOException, LocalRepoKeyStore.InitException { serializer.startTag("", "fdroid"); - tagRepo(); - for (Map.Entry entry : apps.entrySet()) { - tagApplication(entry.getValue()); - } - serializer.endTag("", "fdroid"); - } - - private void tagRepo() throws IOException, LocalRepoKeyStore.InitException { - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - // max age is an EditTextPreference, which is always a String - // TODO: This pref is probably never being set. Also, why - // are we mixing floats and ints? - int repoMaxAge = Float.valueOf(prefs.getString("max_repo_age_days", DEFAULT_REPO_MAX_AGE_DAYS)).intValue(); // NOPMD + // block serializer.startTag("", "repo"); - serializer.attribute("", "icon", "blah.png"); - serializer.attribute("", "maxage", String.valueOf(repoMaxAge)); serializer.attribute("", "name", Preferences.get().getLocalRepoName() + " on " + FDroidApp.ipAddressString); serializer.attribute("", "pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate())); long timestamp = System.currentTimeMillis() / 1000L; serializer.attribute("", "timestamp", String.valueOf(timestamp)); serializer.attribute("", "version", "10"); - tag("description", "A local FDroid repo generated from apps installed on " + Preferences.get().getLocalRepoName()); - serializer.endTag("", "repo"); + // blocks + for (Map.Entry entry : apps.entrySet()) { + tagApplication(entry.getValue()); + } + + serializer.endTag("", "fdroid"); + serializer.endDocument(); + output.close(); } /** @@ -404,6 +376,7 @@ public final class LocalRepoManager { /** * Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)} * That accepts a number instead of string. + * * @see IndexXmlBuilder#tag(String, String) */ private void tag(String name, long number) throws IOException { @@ -413,6 +386,7 @@ public final class LocalRepoManager { /** * Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)} * that accepts a date instead of a string. + * * @see IndexXmlBuilder#tag(String, String) */ private void tag(String name, Date date) throws IOException { @@ -508,32 +482,12 @@ public final class LocalRepoManager { } } - public void writeIndexJar() throws IOException { - - try { - new IndexXmlBuilder(context, apps).build(xmlIndex); - } catch (Exception e) { - Log.e(TAG, "Could not write index jar", e); - Toast.makeText(context, R.string.failed_to_create_index, Toast.LENGTH_LONG).show(); - return; - } - + public void writeIndexJar() throws IOException, XmlPullParserException, LocalRepoKeyStore.InitException { BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned)); JarOutputStream jo = new JarOutputStream(bo); - - BufferedInputStream bi = new BufferedInputStream(new FileInputStream(xmlIndex)); - JarEntry je = new JarEntry("index.xml"); jo.putNextEntry(je); - - byte[] buf = new byte[1024]; - int bytesRead; - - while ((bytesRead = bi.read(buf)) != -1) { - jo.write(buf, 0, bytesRead); - } - - bi.close(); + IndexXmlBuilder.get().build(context, apps, jo); jo.close(); bo.close(); 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..6f1eb4b47 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,24 @@ 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<>(); + + public static void stop(Context context) { + Intent intent = new Intent(context, SwapService.class); + context.stopService(intent); + } + + 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 @@ -386,14 +389,17 @@ public class SwapService extends Service { // Remember which swap technologies a user used in the past // ============================================================= - private void persistPreferredSwapTypes() { - Utils.debugLog(TAG, "Remembering that Bluetooth swap " + (bluetoothSwap.isConnected() ? "IS" : "is NOT") + - " connected and WiFi swap " + (wifiSwap.isConnected() ? "IS" : "is NOT") + " connected."); - persistence().edit() - .putBoolean(KEY_BLUETOOTH_ENABLED, bluetoothSwap.isConnected()) - .putBoolean(KEY_WIFI_ENABLED, wifiSwap.isConnected()) - .commit(); - } + private final BroadcastReceiver receiveSwapStatusChanged = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Utils.debugLog(TAG, "Remembering that Bluetooth swap " + (bluetoothSwap.isConnected() ? "IS" : "is NOT") + + " connected and WiFi swap " + (wifiSwap.isConnected() ? "IS" : "is NOT") + " connected."); + persistence().edit() + .putBoolean(KEY_BLUETOOTH_ENABLED, bluetoothSwap.isConnected()) + .putBoolean(KEY_WIFI_ENABLED, wifiSwap.isConnected()) + .commit(); + } + }; /* private boolean wasBluetoothEnabled() { @@ -405,32 +411,6 @@ public class SwapService extends Service { return persistence().getBoolean(KEY_WIFI_ENABLED, false); } - // ========================================== - // Local repo stop/start/restart handling - // ========================================== - - /** - * Moves the service to the forground and [re]starts the timeout timer. - */ - private void attachService() { - Utils.debugLog(TAG, "Moving SwapService to foreground so that it hangs around even when F-Droid is closed (may already be foregrounded)."); - startForeground(NOTIFICATION, createNotification()); - - // Regardless of whether it was previously enabled, start the timer again. This ensures that - // if, e.g. a person views the swap activity again, it will attempt to enable swapping if - // appropriate, and thus restart this timer. - initTimer(); - } - - private void detachService() { - if (timer != null) { - timer.cancel(); - } - - Utils.debugLog(TAG, "Moving SwapService to background so that it can be GC'ed if required."); - stopForeground(true); - } - /** * Handles checking if the {@link SwapService} is running, and only restarts it if it was running. */ @@ -515,6 +495,9 @@ public class SwapService extends Service { super.onCreate(); Utils.debugLog(TAG, "Creating swap service."); + startForeground(NOTIFICATION, createNotification()); + + CacheSwapAppsService.startCaching(this); SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); @@ -545,55 +528,33 @@ public class SwapService extends Service { } } - /** - * Responsible for moving the service into the foreground or the background, depending on - * whether or not there are any swap services (i.e. bluetooth or wifi) running or not. - */ - private final BroadcastReceiver receiveSwapStatusChanged = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.hasExtra(EXTRA_STARTED)) { - if (getWifiSwap().isConnected() || getBluetoothSwap().isConnected()) { - attachService(); - } - } else if (intent.hasExtra(EXTRA_STOPPED)) { - if (!getWifiSwap().isConnected() && !getBluetoothSwap().isConnected()) { - detachService(); - } - } - persistPreferredSwapTypes(); - } - }; - @Override public int onStartCommand(Intent intent, int flags, int startId) { - return START_STICKY; } @Override public IBinder onBind(Intent intent) { + // reset the timer on each new connect, the user has come back + initTimer(); return binder; } - public void disableAllSwapping() { - Log.i(TAG, "Asked to stop swapping, will stop bluetooth, wifi, and move service to BG for GC."); - //getBluetoothSwap().stopInBackground(); - getWifiSwap().stopInBackground(); - - // Ensure the user is sent back go the first screen when returning if we have just forceably - // cancelled all swapping. - setStep(STEP_INTRO); - detachService(); - } - @Override public void onDestroy() { Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners."); - disableAllSwapping(); Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange); LocalBroadcastManager.getInstance(this).unregisterReceiver(receiveSwapStatusChanged); + + //TODO getBluetoothSwap().stopInBackground(); + getWifiSwap().stopInBackground(); + + if (timer != null) { + timer.cancel(); + } + stopForeground(true); + super.onDestroy(); } @@ -621,7 +582,7 @@ public class SwapService extends Service { @Override public void run() { Utils.debugLog(TAG, "Disabling swap because " + TIMEOUT + "ms passed."); - disableAllSwapping(); + stop(SwapService.this); } }, TIMEOUT); } diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageAddedReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageAddedReceiver.java deleted file mode 100644 index 849a2c1ed..000000000 --- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageAddedReceiver.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2014 Peter Serwylo, peter@serwylo.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.receiver; - -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.net.Uri; - -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.InstalledAppProvider; - -public class PackageAddedReceiver extends PackageReceiver { - - private static final String TAG = "PackageAddedReceiver"; - - @Override - protected boolean toDiscard(Intent intent) { - if (intent.hasExtra(Intent.EXTRA_REPLACING)) { - Utils.debugLog(TAG, "Discarding since this PACKAGE_ADDED is just a PACKAGE_REPLACED"); - return true; - } - return false; - } - - @Override - protected void handle(Context context, String packageName) { - PackageInfo info = getPackageInfo(context, packageName); - if (info == null) { - Utils.debugLog(TAG, "Could not get package info on '" + packageName + "' - skipping."); - return; - } - - Utils.debugLog(TAG, "Inserting installed app info for '" + packageName + "' (v" + info.versionCode + ")"); - - Uri uri = InstalledAppProvider.getContentUri(); - ContentValues values = new ContentValues(4); - values.put(InstalledAppProvider.DataColumns.PACKAGE_NAME, packageName); - values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode); - values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName); - values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL, - InstalledAppProvider.getApplicationLabel(context, packageName)); - values.put(InstalledAppProvider.DataColumns.SIGNATURE, - InstalledAppProvider.getPackageSig(info)); - context.getContentResolver().insert(uri, values); - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java new file mode 100644 index 000000000..d9c44b19c --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java @@ -0,0 +1,35 @@ +package org.fdroid.fdroid.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.InstalledAppProviderService; + +/** + * Receive {@link Intent#ACTION_PACKAGE_ADDED} and {@link Intent#ACTION_PACKAGE_REMOVED} + * events from {@link android.content.pm.PackageManager} to keep + * {@link org.fdroid.fdroid.data.InstalledAppProvider} updated. This ignores + * {@link Intent#EXTRA_REPLACING} and instead handles updates by just deleting then + * inserting the app being updated in direct response to the {@code Intent}s from + * the system. This is also necessary because there are no other checks to prevent + * multiple copies of the same app being inserted into {@Link InstalledAppProvider}. + */ +public class PackageManagerReceiver extends BroadcastReceiver { + private static final String TAG = "PackageManagerReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null) { + String action = intent.getAction(); + if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + InstalledAppProviderService.insert(context, intent.getData()); + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + InstalledAppProviderService.delete(context, intent.getData()); + } else { + Utils.debugLog(TAG, "unsupported action: " + action + " " + intent); + } + } + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageReceiver.java deleted file mode 100644 index b8d368845..000000000 --- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageReceiver.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2014 Ciaran Gultnieks, ciaran@ciarang.com, - * Peter Serwylo, peter@serwylo.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.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; - -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.ApkProvider; -import org.fdroid.fdroid.data.AppProvider; - -abstract class PackageReceiver extends BroadcastReceiver { - - private static final String TAG = "PackageReceiver"; - - protected abstract boolean toDiscard(Intent intent); - - protected abstract void handle(Context context, String packageName); - - protected PackageInfo getPackageInfo(Context context, String packageName) { - PackageInfo info = null; - try { - info = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES); - } catch (PackageManager.NameNotFoundException e) { - // ignore - } - return info; - } - - @Override - public void onReceive(Context context, Intent intent) { - Utils.debugLog(TAG, "PackageReceiver received [action = '" + intent.getAction() + "', data = '" + intent.getData() + "']"); - if (toDiscard(intent)) { - return; - } - String packageName = intent.getData().getSchemeSpecificPart(); - handle(context, packageName); - context.getContentResolver().notifyChange(AppProvider.getContentUri(packageName), null); - context.getContentResolver().notifyChange(ApkProvider.getAppUri(packageName), null); - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageRemovedReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageRemovedReceiver.java deleted file mode 100644 index ab0c3827c..000000000 --- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageRemovedReceiver.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2014 Peter Serwylo, peter@serwylo.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.receiver; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.InstalledAppProvider; - -public class PackageRemovedReceiver extends PackageReceiver { - - private static final String TAG = "PackageRemovedReceiver"; - - @Override - protected boolean toDiscard(Intent intent) { - if (intent.hasExtra(Intent.EXTRA_REPLACING)) { - Utils.debugLog(TAG, "Discarding since this PACKAGE_REMOVED is just a PACKAGE_REPLACED"); - return true; - } - return false; - } - - @Override - protected void handle(Context context, String packageName) { - - Utils.debugLog(TAG, "Removing installed app info for '" + packageName + "'"); - - Uri uri = InstalledAppProvider.getAppUri(packageName); - context.getContentResolver().delete(uri, null, null); - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageUpgradedReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageUpgradedReceiver.java deleted file mode 100644 index dcf914f84..000000000 --- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageUpgradedReceiver.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2014 Peter Serwylo, peter@serwylo.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.receiver; - -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.net.Uri; - -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.InstalledAppProvider; - -/** - * For some reason, devices seem to be keen on sending a REMOVED and then an INSTALLED - * intent, rather than an CHANGED intent. Therefore, this is probably not used on many - * devices. Regardless, it is tested in the unit tests and should work on devices that - * opt instead to send the PACKAGE_CHANGED intent. - */ -public class PackageUpgradedReceiver extends PackageReceiver { - - private static final String TAG = "PackageUpgradedReceiver"; - - @Override - protected boolean toDiscard(Intent intent) { - return false; - } - - @Override - protected void handle(Context context, String packageName) { - PackageInfo info = getPackageInfo(context, packageName); - if (info == null) { - Utils.debugLog(TAG, "Could not get package info on '" + packageName + "' - skipping."); - return; - } - - Utils.debugLog(TAG, "Updating installed app info for '" + packageName + "' to v" + info.versionCode + " (" + info.versionName + ")"); - - Uri uri = InstalledAppProvider.getContentUri(); - ContentValues values = new ContentValues(4); - values.put(InstalledAppProvider.DataColumns.PACKAGE_NAME, packageName); - values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode); - values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName); - values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL, - InstalledAppProvider.getApplicationLabel(context, packageName)); - values.put(InstalledAppProvider.DataColumns.SIGNATURE, - InstalledAppProvider.getPackageSig(info)); - context.getContentResolver().insert(uri, values); - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 72a8eca54..fc760bda7 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -151,9 +151,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { @Override public void onBackPressed() { if (currentView.getStep() == SwapService.STEP_INTRO) { - if (service != null) { - service.disableAllSwapping(); - } + SwapService.stop(this); finish(); } else { int nextStep = currentView.getPreviousStep(); @@ -359,7 +357,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private void onToolbarCancel() { - getService().disableAllSwapping(); + SwapService.stop(this); finish(); }