Extracted RepoPersister class from RepoUpdater to separate security and DB logic.
This commit is contained in:
parent
1d951e7689
commit
ad6a8e5b4e
@ -1,21 +1,16 @@
|
|||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import android.content.ContentProviderOperation;
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.OperationApplicationException;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.fdroid.fdroid.data.Apk;
|
import org.fdroid.fdroid.data.Apk;
|
||||||
import org.fdroid.fdroid.data.ApkProvider;
|
|
||||||
import org.fdroid.fdroid.data.App;
|
import org.fdroid.fdroid.data.App;
|
||||||
import org.fdroid.fdroid.data.AppProvider;
|
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import org.fdroid.fdroid.data.Repo;
|
||||||
|
import org.fdroid.fdroid.data.RepoPersister;
|
||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
import org.fdroid.fdroid.data.TempApkProvider;
|
import org.fdroid.fdroid.data.TempApkProvider;
|
||||||
import org.fdroid.fdroid.data.TempAppProvider;
|
import org.fdroid.fdroid.data.TempAppProvider;
|
||||||
@ -33,11 +28,8 @@ import java.net.URL;
|
|||||||
import java.security.CodeSigner;
|
import java.security.CodeSigner;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
import java.util.jar.JarFile;
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
@ -63,20 +55,6 @@ public class RepoUpdater {
|
|||||||
public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml";
|
public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml";
|
||||||
public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress";
|
public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress";
|
||||||
|
|
||||||
/**
|
|
||||||
* When an app already exists in the db, and we are updating it on the off chance that some
|
|
||||||
* values changed in the index, some fields should not be updated. Rather, they should be
|
|
||||||
* ignored, because they were explicitly set by the user, and hence can't be automatically
|
|
||||||
* overridden by the index.
|
|
||||||
*
|
|
||||||
* NOTE: In the future, these attributes will be moved to a join table, so that the app table
|
|
||||||
* is essentially completely transient, and can be nuked at any time.
|
|
||||||
*/
|
|
||||||
private static final String[] APP_FIELDS_TO_IGNORE = {
|
|
||||||
AppProvider.DataColumns.IGNORE_ALLUPDATES,
|
|
||||||
AppProvider.DataColumns.IGNORE_THISUPDATE,
|
|
||||||
};
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
protected final Context context;
|
protected final Context context;
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -87,6 +65,9 @@ public class RepoUpdater {
|
|||||||
private String cacheTag;
|
private String cacheTag;
|
||||||
private X509Certificate signingCertFromJar;
|
private X509Certificate signingCertFromJar;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final RepoPersister persister;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an app repo as read out of the database into a {@link Repo} instance.
|
* Updates an app repo as read out of the database into a {@link Repo} instance.
|
||||||
*
|
*
|
||||||
@ -95,6 +76,7 @@ public class RepoUpdater {
|
|||||||
public RepoUpdater(@NonNull Context context, @NonNull Repo repo) {
|
public RepoUpdater(@NonNull Context context, @NonNull Repo repo) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.repo = repo;
|
this.repo = repo;
|
||||||
|
this.persister = new RepoPersister(context, repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setProgressListener(@Nullable ProgressListener progressListener) {
|
public void setProgressListener(@Nullable ProgressListener progressListener) {
|
||||||
@ -177,7 +159,7 @@ public class RepoUpdater {
|
|||||||
@Override
|
@Override
|
||||||
public void receiveApp(App app, List<Apk> packages) {
|
public void receiveApp(App app, List<Apk> packages) {
|
||||||
try {
|
try {
|
||||||
saveToDb(app, packages);
|
persister.saveToDb(app, packages);
|
||||||
} catch (UpdateException e) {
|
} catch (UpdateException e) {
|
||||||
throw new RuntimeException("Error while saving repo details to database.", e);
|
throw new RuntimeException("Error while saving repo details to database.", e);
|
||||||
}
|
}
|
||||||
@ -185,231 +167,6 @@ public class RepoUpdater {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* My crappy benchmark with a Nexus 4, Android 5.0 on a fairly crappy internet connection I get:
|
|
||||||
* * 25 = 37 seconds
|
|
||||||
* * 50 = 33 seconds
|
|
||||||
* * 100 = 30 seconds
|
|
||||||
* * 200 = 32 seconds
|
|
||||||
* Raising this means more memory consumption, so we'd like it to be low, but not
|
|
||||||
* so low that it takes too long.
|
|
||||||
*/
|
|
||||||
private static final int MAX_APP_BUFFER = 50;
|
|
||||||
|
|
||||||
private List<App> appsToSave = new ArrayList<>();
|
|
||||||
private Map<String, List<Apk>> apksToSave = new HashMap<>();
|
|
||||||
|
|
||||||
private void saveToDb(App app, List<Apk> packages) throws UpdateException {
|
|
||||||
appsToSave.add(app);
|
|
||||||
apksToSave.put(app.id, packages);
|
|
||||||
|
|
||||||
if (appsToSave.size() >= MAX_APP_BUFFER) {
|
|
||||||
flushBufferToDb();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void flushBufferToDb() throws UpdateException {
|
|
||||||
if (apksToSave.size() > 0 || appsToSave.size() > 0) {
|
|
||||||
Log.d(TAG, "Flushing details of up to " + MAX_APP_BUFFER + " apps and their packages to the database.");
|
|
||||||
flushAppsToDbInBatch();
|
|
||||||
flushApksToDbInBatch();
|
|
||||||
apksToSave.clear();
|
|
||||||
appsToSave.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void flushApksToDbInBatch() throws UpdateException {
|
|
||||||
List<Apk> apksToSaveList = new ArrayList<>();
|
|
||||||
for (Map.Entry<String, List<Apk>> entries : apksToSave.entrySet()) {
|
|
||||||
apksToSaveList.addAll(entries.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
calcApkCompatibilityFlags(apksToSaveList);
|
|
||||||
|
|
||||||
ArrayList<ContentProviderOperation> apkOperations = new ArrayList<>();
|
|
||||||
ContentProviderOperation clearOrphans = deleteOrphanedApks(appsToSave, apksToSave);
|
|
||||||
if (clearOrphans != null) {
|
|
||||||
apkOperations.add(clearOrphans);
|
|
||||||
}
|
|
||||||
apkOperations.addAll(insertOrUpdateApks(apksToSaveList));
|
|
||||||
|
|
||||||
try {
|
|
||||||
context.getContentResolver().applyBatch(TempApkProvider.getAuthority(), apkOperations);
|
|
||||||
} catch (RemoteException | OperationApplicationException e) {
|
|
||||||
throw new UpdateException(repo, "An internal error occured while updating the database", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void flushAppsToDbInBatch() throws UpdateException {
|
|
||||||
ArrayList<ContentProviderOperation> appOperations = insertOrUpdateApps(appsToSave);
|
|
||||||
|
|
||||||
try {
|
|
||||||
context.getContentResolver().applyBatch(TempAppProvider.getAuthority(), appOperations);
|
|
||||||
} catch (RemoteException | OperationApplicationException e) {
|
|
||||||
throw new UpdateException(repo, "An internal error occured while updating the database", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Depending on whether the {@link App}s have been added to the database previously, this
|
|
||||||
* will queue up an update or an insert {@link ContentProviderOperation} for each app.
|
|
||||||
*/
|
|
||||||
private ArrayList<ContentProviderOperation> insertOrUpdateApps(List<App> apps) {
|
|
||||||
ArrayList<ContentProviderOperation> operations = new ArrayList<>(apps.size());
|
|
||||||
for (App app : apps) {
|
|
||||||
if (isAppInDatabase(app)) {
|
|
||||||
operations.add(updateExistingApp(app));
|
|
||||||
} else {
|
|
||||||
operations.add(insertNewApp(app));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return operations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Depending on whether the .apks have been added to the database previously, this
|
|
||||||
* will queue up an update or an insert {@link ContentProviderOperation} for each package.
|
|
||||||
*/
|
|
||||||
private ArrayList<ContentProviderOperation> insertOrUpdateApks(List<Apk> packages) {
|
|
||||||
List<Apk> existingApks = ApkProvider.Helper.knownApks(context, packages, new String[]{ApkProvider.DataColumns.VERSION_CODE});
|
|
||||||
ArrayList<ContentProviderOperation> operations = new ArrayList<>(packages.size());
|
|
||||||
for (Apk apk : packages) {
|
|
||||||
boolean exists = false;
|
|
||||||
for (Apk existing : existingApks) {
|
|
||||||
if (existing.vercode == apk.vercode) {
|
|
||||||
exists = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
operations.add(updateExistingApk(apk));
|
|
||||||
} else {
|
|
||||||
operations.add(insertNewApk(apk));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return operations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an update {@link ContentProviderOperation} for the {@link App} in question.
|
|
||||||
* <strong>Does not do any checks to see if the app already exists or not.</strong>
|
|
||||||
*/
|
|
||||||
private ContentProviderOperation updateExistingApp(App app) {
|
|
||||||
Uri uri = TempAppProvider.getAppUri(app);
|
|
||||||
ContentValues values = app.toContentValues();
|
|
||||||
for (final String toIgnore : APP_FIELDS_TO_IGNORE) {
|
|
||||||
if (values.containsKey(toIgnore)) {
|
|
||||||
values.remove(toIgnore);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an insert {@link ContentProviderOperation} for the {@link App} in question.
|
|
||||||
* <strong>Does not do any checks to see if the app already exists or not.</strong>
|
|
||||||
*/
|
|
||||||
private ContentProviderOperation insertNewApp(App app) {
|
|
||||||
ContentValues values = app.toContentValues();
|
|
||||||
Uri uri = TempAppProvider.getContentUri();
|
|
||||||
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks in the database to see which apps we already know about. Only
|
|
||||||
* returns ids of apps that are in the database if they are in the "apps"
|
|
||||||
* array.
|
|
||||||
*/
|
|
||||||
private boolean isAppInDatabase(App app) {
|
|
||||||
String[] fields = {AppProvider.DataColumns.APP_ID};
|
|
||||||
App found = AppProvider.Helper.findById(context.getContentResolver(), app.id, fields);
|
|
||||||
return found != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an update {@link ContentProviderOperation} for the {@link Apk} in question.
|
|
||||||
* <strong>Does not do any checks to see if the apk already exists or not.</strong>
|
|
||||||
*/
|
|
||||||
private ContentProviderOperation updateExistingApk(final Apk apk) {
|
|
||||||
Uri uri = TempApkProvider.getApkUri(apk);
|
|
||||||
ContentValues values = apk.toContentValues();
|
|
||||||
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an insert {@link ContentProviderOperation} for the {@link Apk} in question.
|
|
||||||
* <strong>Does not do any checks to see if the apk already exists or not.</strong>
|
|
||||||
*/
|
|
||||||
private ContentProviderOperation insertNewApk(final Apk apk) {
|
|
||||||
ContentValues values = apk.toContentValues();
|
|
||||||
Uri uri = TempApkProvider.getContentUri();
|
|
||||||
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all apks from the repo we are currently updating, that belong to the specified app,
|
|
||||||
* and delete them as they are no longer provided by that repo.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private ContentProviderOperation deleteOrphanedApks(List<App> apps, Map<String, List<Apk>> packages) {
|
|
||||||
|
|
||||||
String[] projection = new String[]{ApkProvider.DataColumns.APK_ID, ApkProvider.DataColumns.VERSION_CODE};
|
|
||||||
List<Apk> existing = ApkProvider.Helper.find(context, repo, apps, projection);
|
|
||||||
|
|
||||||
List<Apk> toDelete = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Apk existingApk : existing) {
|
|
||||||
|
|
||||||
boolean shouldStay = false;
|
|
||||||
|
|
||||||
for (Map.Entry<String, List<Apk>> entry : packages.entrySet()) {
|
|
||||||
for (Apk newApk : entry.getValue()) {
|
|
||||||
if (newApk.vercode == existingApk.vercode) {
|
|
||||||
shouldStay = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldStay) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldStay) {
|
|
||||||
toDelete.add(existingApk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toDelete.size() > 0) {
|
|
||||||
Uri uri = TempApkProvider.getApksUri(repo, toDelete);
|
|
||||||
return ContentProviderOperation.newDelete(uri).build();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This cannot be offloaded to the database (as we did with the query which
|
|
||||||
* updates apps, depending on whether their apks are compatible or not).
|
|
||||||
* The reason is that we need to interact with the CompatibilityChecker
|
|
||||||
* in order to see if, and why an apk is not compatible.
|
|
||||||
*/
|
|
||||||
public void calcApkCompatibilityFlags(List<Apk> apks) {
|
|
||||||
final CompatibilityChecker checker = new CompatibilityChecker(context);
|
|
||||||
for (final Apk apk : apks) {
|
|
||||||
final List<String> reasons = checker.getIncompatibleReasons(apk);
|
|
||||||
if (reasons.size() > 0) {
|
|
||||||
apk.compatible = false;
|
|
||||||
apk.incompatibleReasons = Utils.CommaSeparatedList.make(reasons);
|
|
||||||
} else {
|
|
||||||
apk.compatible = true;
|
|
||||||
apk.incompatibleReasons = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void processDownloadedFile(File downloadedFile) throws UpdateException {
|
public void processDownloadedFile(File downloadedFile) throws UpdateException {
|
||||||
InputStream indexInputStream = null;
|
InputStream indexInputStream = null;
|
||||||
try {
|
try {
|
||||||
@ -441,8 +198,6 @@ public class RepoUpdater {
|
|||||||
reader.setContentHandler(repoXMLHandler);
|
reader.setContentHandler(repoXMLHandler);
|
||||||
reader.parse(new InputSource(indexInputStream));
|
reader.parse(new InputSource(indexInputStream));
|
||||||
|
|
||||||
flushBufferToDb();
|
|
||||||
|
|
||||||
signingCertFromJar = getSigningCertFromJar(indexEntry);
|
signingCertFromJar = getSigningCertFromJar(indexEntry);
|
||||||
|
|
||||||
// JarEntry can only read certificates after the file represented by that JarEntry
|
// JarEntry can only read certificates after the file represented by that JarEntry
|
||||||
@ -450,9 +205,7 @@ public class RepoUpdater {
|
|||||||
assertSigningCertFromXmlCorrect();
|
assertSigningCertFromXmlCorrect();
|
||||||
|
|
||||||
Log.i(TAG, "Repo signature verified, saving app metadata to database.");
|
Log.i(TAG, "Repo signature verified, saving app metadata to database.");
|
||||||
TempAppProvider.Helper.commit(context);
|
persister.commit(repoDetailsToSave);
|
||||||
TempApkProvider.Helper.commit(context);
|
|
||||||
RepoProvider.Helper.update(context, repo, repoDetailsToSave);
|
|
||||||
|
|
||||||
} catch (SAXException | ParserConfigurationException | IOException e) {
|
} catch (SAXException | ParserConfigurationException | IOException e) {
|
||||||
throw new UpdateException(repo, "Error parsing index", e);
|
throw new UpdateException(repo, "Error parsing index", e);
|
||||||
|
287
F-Droid/src/org/fdroid/fdroid/data/RepoPersister.java
Normal file
287
F-Droid/src/org/fdroid/fdroid/data/RepoPersister.java
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
package org.fdroid.fdroid.data;
|
||||||
|
|
||||||
|
import android.content.ContentProviderOperation;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.OperationApplicationException;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.CompatibilityChecker;
|
||||||
|
import org.fdroid.fdroid.RepoUpdater;
|
||||||
|
import org.fdroid.fdroid.Utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class RepoPersister {
|
||||||
|
|
||||||
|
private static final String TAG = "RepoPersister";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an app already exists in the db, and we are updating it on the off chance that some
|
||||||
|
* values changed in the index, some fields should not be updated. Rather, they should be
|
||||||
|
* ignored, because they were explicitly set by the user, and hence can't be automatically
|
||||||
|
* overridden by the index.
|
||||||
|
*
|
||||||
|
* NOTE: In the future, these attributes will be moved to a join table, so that the app table
|
||||||
|
* is essentially completely transient, and can be nuked at any time.
|
||||||
|
*/
|
||||||
|
private static final String[] APP_FIELDS_TO_IGNORE = {
|
||||||
|
AppProvider.DataColumns.IGNORE_ALLUPDATES,
|
||||||
|
AppProvider.DataColumns.IGNORE_THISUPDATE,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crappy benchmark with a Nexus 4, Android 5.0 on a fairly crappy internet connection I get:
|
||||||
|
* * 25 = 37 seconds
|
||||||
|
* * 50 = 33 seconds
|
||||||
|
* * 100 = 30 seconds
|
||||||
|
* * 200 = 32 seconds
|
||||||
|
* Raising this means more memory consumption, so we'd like it to be low, but not
|
||||||
|
* so low that it takes too long.
|
||||||
|
*/
|
||||||
|
private static final int MAX_APP_BUFFER = 50;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Repo repo;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final List<App> appsToSave = new ArrayList<>();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Map<String, List<Apk>> apksToSave = new HashMap<>();
|
||||||
|
|
||||||
|
public RepoPersister(@NonNull Context context, @NonNull Repo repo) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveToDb(App app, List<Apk> packages) throws RepoUpdater.UpdateException {
|
||||||
|
appsToSave.add(app);
|
||||||
|
apksToSave.put(app.id, packages);
|
||||||
|
|
||||||
|
if (appsToSave.size() >= MAX_APP_BUFFER) {
|
||||||
|
flushBufferToDb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void commit(ContentValues repoDetailsToSave) throws RepoUpdater.UpdateException {
|
||||||
|
flushBufferToDb();
|
||||||
|
TempAppProvider.Helper.commit(context);
|
||||||
|
TempApkProvider.Helper.commit(context);
|
||||||
|
RepoProvider.Helper.update(context, repo, repoDetailsToSave);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushBufferToDb() throws RepoUpdater.UpdateException {
|
||||||
|
if (apksToSave.size() > 0 || appsToSave.size() > 0) {
|
||||||
|
Log.d(TAG, "Flushing details of up to " + MAX_APP_BUFFER + " apps and their packages to the database.");
|
||||||
|
flushAppsToDbInBatch();
|
||||||
|
flushApksToDbInBatch();
|
||||||
|
apksToSave.clear();
|
||||||
|
appsToSave.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushApksToDbInBatch() throws RepoUpdater.UpdateException {
|
||||||
|
List<Apk> apksToSaveList = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<Apk>> entries : apksToSave.entrySet()) {
|
||||||
|
apksToSaveList.addAll(entries.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
calcApkCompatibilityFlags(apksToSaveList);
|
||||||
|
|
||||||
|
ArrayList<ContentProviderOperation> apkOperations = new ArrayList<>();
|
||||||
|
ContentProviderOperation clearOrphans = deleteOrphanedApks(appsToSave, apksToSave);
|
||||||
|
if (clearOrphans != null) {
|
||||||
|
apkOperations.add(clearOrphans);
|
||||||
|
}
|
||||||
|
apkOperations.addAll(insertOrUpdateApks(apksToSaveList));
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.getContentResolver().applyBatch(TempApkProvider.getAuthority(), apkOperations);
|
||||||
|
} catch (RemoteException | OperationApplicationException e) {
|
||||||
|
throw new RepoUpdater.UpdateException(repo, "An internal error occured while updating the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushAppsToDbInBatch() throws RepoUpdater.UpdateException {
|
||||||
|
ArrayList<ContentProviderOperation> appOperations = insertOrUpdateApps(appsToSave);
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.getContentResolver().applyBatch(TempAppProvider.getAuthority(), appOperations);
|
||||||
|
} catch (RemoteException | OperationApplicationException e) {
|
||||||
|
throw new RepoUpdater.UpdateException(repo, "An internal error occured while updating the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on whether the {@link App}s have been added to the database previously, this
|
||||||
|
* will queue up an update or an insert {@link ContentProviderOperation} for each app.
|
||||||
|
*/
|
||||||
|
private ArrayList<ContentProviderOperation> insertOrUpdateApps(List<App> apps) {
|
||||||
|
ArrayList<ContentProviderOperation> operations = new ArrayList<>(apps.size());
|
||||||
|
for (App app : apps) {
|
||||||
|
if (isAppInDatabase(app)) {
|
||||||
|
operations.add(updateExistingApp(app));
|
||||||
|
} else {
|
||||||
|
operations.add(insertNewApp(app));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on whether the .apks have been added to the database previously, this
|
||||||
|
* will queue up an update or an insert {@link ContentProviderOperation} for each package.
|
||||||
|
*/
|
||||||
|
private ArrayList<ContentProviderOperation> insertOrUpdateApks(List<Apk> packages) {
|
||||||
|
List<Apk> existingApks = ApkProvider.Helper.knownApks(context, packages, new String[]{ApkProvider.DataColumns.VERSION_CODE});
|
||||||
|
ArrayList<ContentProviderOperation> operations = new ArrayList<>(packages.size());
|
||||||
|
for (Apk apk : packages) {
|
||||||
|
boolean exists = false;
|
||||||
|
for (Apk existing : existingApks) {
|
||||||
|
if (existing.vercode == apk.vercode) {
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
operations.add(updateExistingApk(apk));
|
||||||
|
} else {
|
||||||
|
operations.add(insertNewApk(apk));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an update {@link ContentProviderOperation} for the {@link App} in question.
|
||||||
|
* <strong>Does not do any checks to see if the app already exists or not.</strong>
|
||||||
|
*/
|
||||||
|
private ContentProviderOperation updateExistingApp(App app) {
|
||||||
|
Uri uri = TempAppProvider.getAppUri(app);
|
||||||
|
ContentValues values = app.toContentValues();
|
||||||
|
for (final String toIgnore : APP_FIELDS_TO_IGNORE) {
|
||||||
|
if (values.containsKey(toIgnore)) {
|
||||||
|
values.remove(toIgnore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an insert {@link ContentProviderOperation} for the {@link App} in question.
|
||||||
|
* <strong>Does not do any checks to see if the app already exists or not.</strong>
|
||||||
|
*/
|
||||||
|
private ContentProviderOperation insertNewApp(App app) {
|
||||||
|
ContentValues values = app.toContentValues();
|
||||||
|
Uri uri = TempAppProvider.getContentUri();
|
||||||
|
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks in the database to see which apps we already know about. Only
|
||||||
|
* returns ids of apps that are in the database if they are in the "apps"
|
||||||
|
* array.
|
||||||
|
*/
|
||||||
|
private boolean isAppInDatabase(App app) {
|
||||||
|
String[] fields = {AppProvider.DataColumns.APP_ID};
|
||||||
|
App found = AppProvider.Helper.findById(context.getContentResolver(), app.id, fields);
|
||||||
|
return found != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an update {@link ContentProviderOperation} for the {@link Apk} in question.
|
||||||
|
* <strong>Does not do any checks to see if the apk already exists or not.</strong>
|
||||||
|
*/
|
||||||
|
private ContentProviderOperation updateExistingApk(final Apk apk) {
|
||||||
|
Uri uri = TempApkProvider.getApkUri(apk);
|
||||||
|
ContentValues values = apk.toContentValues();
|
||||||
|
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an insert {@link ContentProviderOperation} for the {@link Apk} in question.
|
||||||
|
* <strong>Does not do any checks to see if the apk already exists or not.</strong>
|
||||||
|
*/
|
||||||
|
private ContentProviderOperation insertNewApk(final Apk apk) {
|
||||||
|
ContentValues values = apk.toContentValues();
|
||||||
|
Uri uri = TempApkProvider.getContentUri();
|
||||||
|
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all apks from the repo we are currently updating, that belong to the specified app,
|
||||||
|
* and delete them as they are no longer provided by that repo.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private ContentProviderOperation deleteOrphanedApks(List<App> apps, Map<String, List<Apk>> packages) {
|
||||||
|
|
||||||
|
String[] projection = new String[]{ApkProvider.DataColumns.APK_ID, ApkProvider.DataColumns.VERSION_CODE};
|
||||||
|
List<Apk> existing = ApkProvider.Helper.find(context, repo, apps, projection);
|
||||||
|
|
||||||
|
List<Apk> toDelete = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Apk existingApk : existing) {
|
||||||
|
|
||||||
|
boolean shouldStay = false;
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<Apk>> entry : packages.entrySet()) {
|
||||||
|
for (Apk newApk : entry.getValue()) {
|
||||||
|
if (newApk.vercode == existingApk.vercode) {
|
||||||
|
shouldStay = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldStay) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldStay) {
|
||||||
|
toDelete.add(existingApk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toDelete.size() > 0) {
|
||||||
|
Uri uri = TempApkProvider.getApksUri(repo, toDelete);
|
||||||
|
return ContentProviderOperation.newDelete(uri).build();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This cannot be offloaded to the database (as we did with the query which
|
||||||
|
* updates apps, depending on whether their apks are compatible or not).
|
||||||
|
* The reason is that we need to interact with the CompatibilityChecker
|
||||||
|
* in order to see if, and why an apk is not compatible.
|
||||||
|
*/
|
||||||
|
private void calcApkCompatibilityFlags(List<Apk> apks) {
|
||||||
|
final CompatibilityChecker checker = new CompatibilityChecker(context);
|
||||||
|
for (final Apk apk : apks) {
|
||||||
|
final List<String> reasons = checker.getIncompatibleReasons(apk);
|
||||||
|
if (reasons.size() > 0) {
|
||||||
|
apk.compatible = false;
|
||||||
|
apk.incompatibleReasons = Utils.CommaSeparatedList.make(reasons);
|
||||||
|
} else {
|
||||||
|
apk.compatible = true;
|
||||||
|
apk.incompatibleReasons = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user