Merge branch 'cr-of-299' into 'master'

Changes made during CR of 299

Here is a collection of small changes I implemented while CRing 299. The most interesting is probably the nice opportunity to use a little bit of RX. This is a good example of where it is a useful API, to "debounce" requests by 1 second (i.e. collect all requests but only respond to the last one after X time units). The `PublishSubject` sits there for the duration of the service, and passively receives events when required. However, it only emits events to the subscriber after one second because before subscribig we ask it to debounce events.

See merge request !319
This commit is contained in:
Peter Serwylo 2016-06-02 11:41:58 +00:00
commit 4e9c8e9e5e
5 changed files with 51 additions and 143 deletions

View File

@ -11,23 +11,10 @@ import mock.MockInstallablePackageManager;
@SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics
public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppProvider> {
private MockInstallablePackageManager packageManager;
public InstalledAppProviderTest() {
super(InstalledAppProvider.class, InstalledAppProvider.getAuthority());
}
@Override
public void setUp() throws Exception {
super.setUp();
packageManager = new MockInstallablePackageManager();
getSwappableContext().setPackageManager(packageManager);
}
protected MockInstallablePackageManager getPackageManager() {
return packageManager;
}
public void testUris() {
assertInvalidUri(InstalledAppProvider.getAuthority());
assertInvalidUri(RepoProvider.getContentUri());
@ -130,45 +117,6 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
}
public void testInsertWithBroadcast() {
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");
assertIsInstalledVersionInDb("com.example.broadcasted2", 105, "v1.05");
}
public void testUpdateWithBroadcast() {
install("com.example.toUpgrade", 1, "v0.1");
assertResultCount(1, InstalledAppProvider.getContentUri());
assertIsInstalledVersionInDb("com.example.toUpgrade", 1, "v0.1");
install("com.example.toUpgrade", 2, "v0.2");
assertResultCount(1, InstalledAppProvider.getContentUri());
assertIsInstalledVersionInDb("com.example.toUpgrade", 2, "v0.2");
}
public void testDeleteWithBroadcast() {
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");
remove("com.example.toDelete");
assertResultCount(1, InstalledAppProvider.getContentUri());
assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1");
}
@Override
protected String[] getMinimalProjection() {
return new String[]{
@ -202,14 +150,6 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
getMockContentResolver().insert(InstalledAppProvider.getContentUri(), values);
}
private void remove(String packageName) {
remove(getSwappableContext(), getPackageManager(), packageName);
}
private void install(String appId, int versionCode, String versionName) {
install(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.
@ -224,14 +164,4 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
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);
}
}

View File

@ -6,17 +6,14 @@ import android.content.UriMatcher;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
@ -118,21 +115,6 @@ public class InstalledAppProvider extends FDroidProvider {
return packageName; // all else fails, return packageName
}
public static String getPackageSig(PackageInfo info) {
if (info == null || info.signatures == null || info.signatures.length < 1) {
return "";
}
Signature sig = info.signatures[0];
String sigHash = "";
try {
Hasher hash = new Hasher("MD5", sig.toCharsString().getBytes());
sigHash = hash.getHash();
} catch (NoSuchAlgorithmException e) {
// ignore
}
return sigHash;
}
@Override
protected String getTableName() {
return DBHelper.TABLE_INSTALLED_APP;
@ -201,11 +183,7 @@ public class InstalledAppProvider extends FDroidProvider {
QuerySelection query = new QuerySelection(where, whereArgs);
query = query.add(queryApp(uri.getLastPathSegment()));
int count = db().delete(getTableName(), query.getSelection(), query.getArgs());
if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
return db().delete(getTableName(), query.getSelection(), query.getArgs());
}
@Override
@ -217,9 +195,6 @@ public class InstalledAppProvider extends FDroidProvider {
verifyVersionNameNotNull(values);
db().replaceOrThrow(getTableName(), null, values);
if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null);
}
return getAppUri(values.getAsString(DataColumns.PACKAGE_NAME));
}

View File

@ -6,18 +6,23 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.net.Uri;
import android.os.Process;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.Utils;
import java.io.File;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
/**
* Handles all updates to {@link InstalledAppProvider}, whether checking the contents
* versus what Android says is installed, or processing {@link Intent}s that come
@ -39,13 +44,34 @@ public class InstalledAppProviderService extends IntentService {
private static final String EXTRA_PACKAGE_INFO = "org.fdroid.fdroid.data.extra.PACKAGE_INFO";
private ScheduledExecutorService worker;
private boolean notifyChangeNeedsSending;
/**
* This is for notifing 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 PublishSubject<Void> notifyEvents;
public InstalledAppProviderService() {
super("InstalledAppProviderService");
}
@Override
public void onCreate() {
super.onCreate();
notifyEvents = PublishSubject.create();
notifyEvents.debounce(1, TimeUnit.SECONDS)
.subscribeOn(Schedulers.newThread())
.subscribe(new Action1<Void>() {
@Override
public void call(Void voidArg) {
Utils.debugLog(TAG, "Notifying content providers (so they can update the relevant views).");
getContentResolver().notifyChange(AppProvider.getContentUri(), null);
getContentResolver().notifyChange(ApkProvider.getContentUri(), null);
}
});
}
/**
* 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
@ -134,7 +160,7 @@ public class InstalledAppProviderService extends IntentService {
} else if (ACTION_DELETE.equals(action)) {
deleteAppFromDb(this, packageName);
}
notifyChange();
notifyEvents.onNext(null);
}
}
@ -156,8 +182,7 @@ public class InstalledAppProviderService extends IntentService {
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.SIGNATURE, getPackageSig(packageInfo));
contentValues.put(InstalledAppProvider.DataColumns.LAST_UPDATE_TIME, packageInfo.lastUpdateTime);
String hashType = "sha256";
@ -173,31 +198,19 @@ public class InstalledAppProviderService extends IntentService {
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);
private static String getPackageSig(PackageInfo info) {
if (info == null || info.signatures == null || info.signatures.length < 1) {
return "";
}
Signature sig = info.signatures[0];
String sigHash = "";
try {
Hasher hash = new Hasher("MD5", sig.toCharsString().getBytes());
sigHash = hash.getHash();
} catch (NoSuchAlgorithmException e) {
// ignore
}
return sigHash;
}
}

View File

@ -37,7 +37,9 @@ public class CacheSwapAppsService extends IntentService {
* 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) {
private static void parseApp(Context context, String packageName) {
Intent intent = new Intent();
intent.setData(Utils.getPackageUri(packageName));
intent.setClass(context, CacheSwapAppsService.class);
intent.setAction(ACTION_PARSE_APP);
context.startService(intent);
@ -57,9 +59,7 @@ public class CacheSwapAppsService extends IntentService {
}
if (!indexJarFile.exists()
|| FileUtils.isFileNewer(new File(applicationInfo.sourceDir), indexJarFile)) {
Intent intent = new Intent();
intent.setData(Utils.getPackageUri(applicationInfo.packageName));
parseApp(context, intent);
parseApp(context, applicationInfo.packageName);
}
}
}

View File

@ -319,16 +319,6 @@ public final class LocalRepoManager {
* Helper class to aid in constructing index.xml file.
*/
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;
@ -487,7 +477,7 @@ public final class LocalRepoManager {
JarOutputStream jo = new JarOutputStream(bo);
JarEntry je = new JarEntry("index.xml");
jo.putNextEntry(je);
IndexXmlBuilder.get().build(context, apps, jo);
new IndexXmlBuilder().build(context, apps, jo);
jo.close();
bo.close();