Merge branch 'master' into 'master'
InstalledAppProviderService I worked a bunch to get the swap code to represent the lifecycle of things better. This includes making `SwapService` stay alive as long as anything related to swap is running. `SwapService` is then becomes the one thing that stores all the state of swap, then the state does not need to be stored in `SwapWorkflowActivity` or any of the swap views. I would like to include this in 0.100, but only if y'all think it won't delay the release. These changes are pretty much entirely contained in the swap stuff. There is some changes to `App`, but those are in the `App(Context context, PackageManager pm, String packageName)`, which is the constructor that is only used for the swap stuff. It also touches `RepoXMLHandler` but that change only adds `public` to the `IndexReceiver` interface. See merge request !299
This commit is contained in:
commit
e80e445aa5
@ -98,6 +98,11 @@ public abstract class ProviderTestCase2MockContext<T extends ContentProvider> ex
|
||||
public Context getApplicationContext() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPackageName() {
|
||||
return "org.fdroid.fdroid";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<AppProvider> {
|
||||
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<AppProvider> {
|
||||
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<AppProvider> {
|
||||
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());
|
||||
|
@ -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<InstalledAppPro
|
||||
|
||||
}
|
||||
|
||||
public void testLastUpdateTime() {
|
||||
String packageName = "com.example.app";
|
||||
|
||||
insertInstalledApp(packageName, 10, "1.0");
|
||||
assertResultCount(1, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb(packageName, 10, "1.0");
|
||||
|
||||
Uri uri = InstalledAppProvider.getAppUri(packageName);
|
||||
|
||||
String[] projection = {
|
||||
InstalledAppProvider.DataColumns.PACKAGE_NAME,
|
||||
InstalledAppProvider.DataColumns.LAST_UPDATE_TIME,
|
||||
};
|
||||
|
||||
Cursor cursor = getMockContentResolver().query(uri, projection, null, null, null);
|
||||
assertNotNull(cursor);
|
||||
assertEquals("App \"" + packageName + "\" not installed", 1, cursor.getCount());
|
||||
cursor.moveToFirst();
|
||||
assertEquals(packageName, cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.PACKAGE_NAME)));
|
||||
long lastUpdateTime = cursor.getLong(cursor.getColumnIndex(InstalledAppProvider.DataColumns.LAST_UPDATE_TIME));
|
||||
assertTrue(lastUpdateTime > 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<InstalledAppPro
|
||||
}
|
||||
|
||||
public void testInsertWithBroadcast() {
|
||||
|
||||
installAndBroadcast("com.example.broadcasted1", 10, "v1.0");
|
||||
installAndBroadcast("com.example.broadcasted2", 105, "v1.05");
|
||||
install("com.example.broadcasted1", 10, "v1.0");
|
||||
install("com.example.broadcasted2", 105, "v1.05");
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.broadcasted1", 10, "v1.0");
|
||||
@ -107,12 +141,12 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
|
||||
|
||||
public void testUpdateWithBroadcast() {
|
||||
|
||||
installAndBroadcast("com.example.toUpgrade", 1, "v0.1");
|
||||
install("com.example.toUpgrade", 1, "v0.1");
|
||||
|
||||
assertResultCount(1, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.toUpgrade", 1, "v0.1");
|
||||
|
||||
upgradeAndBroadcast("com.example.toUpgrade", 2, "v0.2");
|
||||
install("com.example.toUpgrade", 2, "v0.2");
|
||||
|
||||
assertResultCount(1, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.toUpgrade", 2, "v0.2");
|
||||
@ -121,14 +155,14 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
|
||||
|
||||
public void testDeleteWithBroadcast() {
|
||||
|
||||
installAndBroadcast("com.example.toKeep", 1, "v0.1");
|
||||
installAndBroadcast("com.example.toDelete", 1, "v0.1");
|
||||
install("com.example.toKeep", 1, "v0.1");
|
||||
install("com.example.toDelete", 1, "v0.1");
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1");
|
||||
assertIsInstalledVersionInDb("com.example.toDelete", 1, "v0.1");
|
||||
|
||||
removeAndBroadcast("com.example.toDelete");
|
||||
remove("com.example.toDelete");
|
||||
|
||||
assertResultCount(1, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1");
|
||||
@ -137,7 +171,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
|
||||
|
||||
@Override
|
||||
protected String[] getMinimalProjection() {
|
||||
return new String[] {
|
||||
return new String[]{
|
||||
InstalledAppProvider.DataColumns.PACKAGE_NAME,
|
||||
InstalledAppProvider.DataColumns.VERSION_CODE,
|
||||
InstalledAppProvider.DataColumns.VERSION_NAME,
|
||||
@ -157,6 +191,9 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, versionCode);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, versionNumber);
|
||||
values.put(InstalledAppProvider.DataColumns.SIGNATURE, "");
|
||||
values.put(InstalledAppProvider.DataColumns.LAST_UPDATE_TIME, System.currentTimeMillis());
|
||||
values.put(InstalledAppProvider.DataColumns.HASH_TYPE, "sha256");
|
||||
values.put(InstalledAppProvider.DataColumns.HASH, "cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe");
|
||||
return values;
|
||||
}
|
||||
|
||||
@ -165,16 +202,36 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
|
||||
getMockContentResolver().insert(InstalledAppProvider.getContentUri(), values);
|
||||
}
|
||||
|
||||
private void removeAndBroadcast(String appId) {
|
||||
TestUtils.removeAndBroadcast(getSwappableContext(), getPackageManager(), appId);
|
||||
private void remove(String packageName) {
|
||||
remove(getSwappableContext(), getPackageManager(), packageName);
|
||||
}
|
||||
|
||||
private void upgradeAndBroadcast(String appId, int versionCode, String versionName) {
|
||||
TestUtils.upgradeAndBroadcast(getSwappableContext(), getPackageManager(), appId, versionCode, versionName);
|
||||
private void install(String appId, int versionCode, String versionName) {
|
||||
install(getSwappableContext(), getPackageManager(), appId, versionCode, versionName);
|
||||
}
|
||||
|
||||
private void installAndBroadcast(String appId, int versionCode, String versionName) {
|
||||
TestUtils.installAndBroadcast(getSwappableContext(), getPackageManager(), appId, versionCode, versionName);
|
||||
/**
|
||||
* Will tell {@code pm} that we are installing {@code packageName}, and then update the
|
||||
* "installed apps" table in the database.
|
||||
*/
|
||||
public static void install(MockContextSwappableComponents context,
|
||||
MockInstallablePackageManager pm, String packageName,
|
||||
int versionCode, String versionName) {
|
||||
|
||||
context.setPackageManager(pm);
|
||||
pm.install(packageName, versionCode, versionName);
|
||||
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
|
||||
InstalledAppProviderService.insertAppIntoDb(context, packageName, packageInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #install(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String)
|
||||
*/
|
||||
public static void remove(MockContextSwappableComponents context, MockInstallablePackageManager pm, String packageName) {
|
||||
|
||||
context.setPackageManager(pm);
|
||||
pm.remove(packageName);
|
||||
InstalledAppProviderService.deleteAppFromDb(context, packageName);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -423,22 +423,10 @@
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receiver.PackageAddedReceiver" >
|
||||
<receiver android:name=".receiver.PackageManagerReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_ADDED" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receiver.PackageUpgradedReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receiver.PackageRemovedReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_CHANGED" />
|
||||
<action android:name="android.intent.action.PACKAGE_REMOVED" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
@ -465,6 +453,12 @@
|
||||
<service
|
||||
android:name=".installer.InstallManagerService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".localrepo.CacheSwapAppsService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".data.InstalledAppProviderService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -294,8 +294,10 @@ public class App extends ValueObject implements Comparable<App> {
|
||||
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<App> {
|
||||
|
||||
this.name = (String) appInfo.loadLabel(pm);
|
||||
this.icon = getIconName(packageName, packageInfo.versionCode);
|
||||
this.installedVersionName = packageInfo.versionName;
|
||||
this.installedVersionCode = packageInfo.versionCode;
|
||||
this.compatible = true;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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<PackageInfo> toInsert = new ArrayList<>();
|
||||
private final List<String> 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<ContentProviderOperation> 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<String, Integer> cachedInfo = InstalledAppProvider.Helper.all(context);
|
||||
|
||||
List<PackageInfo> 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<String, Integer> entry : cachedInfo.entrySet()) {
|
||||
toDelete.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<ContentProviderOperation> insertIntoCache(List<PackageInfo> appsToInsert) {
|
||||
List<ContentProviderOperation> 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<ContentProviderOperation> deleteFromCache(List<String> packageNames) {
|
||||
List<ContentProviderOperation> 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<Void, Void, Boolean> {
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
Thread.sleep(10000);
|
||||
} catch (InterruptedException ignored) { }
|
||||
return update();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean changed) {
|
||||
if (changed) {
|
||||
notifyProviders();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<String, Integer> all(Context context) {
|
||||
public static Map<String, Long> all(Context context) {
|
||||
|
||||
Map<String, Integer> cachedInfo = new HashMap<>();
|
||||
Map<String, Long> 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.");
|
||||
|
@ -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}
|
||||
* <p/>
|
||||
* 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.
|
||||
* <p/>
|
||||
* 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<String, Long> cachedInfo = InstalledAppProvider.Helper.all(context);
|
||||
|
||||
List<PackageInfo> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String, App> 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<String, App> 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<String, App> 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<String, App> 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<String, App> 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
|
||||
|
||||
// <repo> 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");
|
||||
|
||||
// <application> blocks
|
||||
for (Map.Entry<String, App> 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();
|
||||
|
||||
|
@ -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<String> appsToSwap = new HashSet<>();
|
||||
|
||||
/**
|
||||
* A cache of parsed APKs from the file system.
|
||||
*/
|
||||
private static final ConcurrentHashMap<String, App> 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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user