Merge branch 'JobIntentService-revamp' into 'master'

JobIntentService revamp

Closes #1426

See merge request fdroid/fdroidclient!678
This commit is contained in:
Hans-Christoph Steiner 2018-04-25 20:48:39 +00:00
commit 76150db0c2
25 changed files with 268 additions and 202 deletions

View File

@ -46,6 +46,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Indicate that F-Droid may request root access (introduced by Koush's Superuser app)
This permission is deprecated, but necessary for some old superuser
@ -251,7 +252,10 @@
</intent-filter>
</receiver>
<service android:name=".UpdateService"/>
<service
android:name=".UpdateService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".UpdateJobService"
android:exported="false"
@ -261,18 +265,26 @@
android:exported="false"/>
<service
android:name=".installer.InstallerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".CleanCacheService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".CleanCacheJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".DeleteCacheService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".net.WifiStateChangeService"
android:exported="false"/>
<service
android:name=".net.ConnectivityMonitorService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service android:name=".localrepo.SwapService"/>
<service
@ -286,6 +298,7 @@
android:exported="false"/>
<service
android:name=".data.InstalledAppProviderService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".AppUpdateStatusService"

View File

@ -0,0 +1,22 @@
package org.fdroid.fdroid;
import android.annotation.TargetApi;
import android.app.job.JobParameters;
import android.app.job.JobService;
/**
* Shim to run {@link CleanCacheService} with {@link android.app.job.JobScheduler}
*/
@TargetApi(21)
public class CleanCacheJobService extends JobService {
@Override
public boolean onStartJob(JobParameters jobParameters) {
CleanCacheService.start(this);
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
return true;
}
}

View File

@ -1,13 +1,17 @@
package org.fdroid.fdroid;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Process;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.installer.ApkCache;
@ -27,7 +31,10 @@ import java.util.concurrent.TimeUnit;
* and newer. On older Android, last modified time from {@link File#lastModified()}
* is used.
*/
public class CleanCacheService extends IntentService {
public class CleanCacheService extends JobIntentService {
public static final String TAG = "CleanCacheService";
private static final int JOB_ID = 0x982374;
/**
* Schedule or cancel this service to update the app index, according to the
@ -41,28 +48,36 @@ public class CleanCacheService extends IntentService {
interval = keepTime;
}
Intent intent = new Intent(context, CleanCacheService.class);
PendingIntent pending = PendingIntent.getService(context, 0, intent, 0);
if (Build.VERSION.SDK_INT < 21) {
Intent intent = new Intent(context, CleanCacheService.class);
PendingIntent pending = PendingIntent.getService(context, 0, intent, 0);
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarm.cancel(pending);
alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 5000, interval, pending);
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarm.cancel(pending);
alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 5000, interval, pending);
} else {
Utils.debugLog(TAG, "Using android-21 JobScheduler for updates");
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName componentName = new ComponentName(context, CleanCacheJobService.class);
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, componentName)
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.setPeriodic(interval);
if (Build.VERSION.SDK_INT >= 26) {
builder.setRequiresBatteryNotLow(true);
}
jobScheduler.schedule(builder.build());
}
}
public static void start(Context context) {
context.startService(new Intent(context, CleanCacheService.class));
}
public CleanCacheService() {
super("CleanCacheService");
enqueueWork(context, CleanCacheService.class, JOB_ID, new Intent(context, CleanCacheService.class));
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
return;
}
protected void onHandleWork(@NonNull Intent intent) {
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
deleteExpiredApksFromCache();
deleteStrayIndexFiles();

View File

@ -1,9 +1,10 @@
package org.fdroid.fdroid;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.os.Process;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import org.apache.commons.io.FileUtils;
@ -11,25 +12,18 @@ import org.apache.commons.io.FileUtils;
import java.io.File;
/**
* An {@link IntentService} subclass for deleting the full cache for this app.
* An {@link JobIntentService} subclass for deleting the full cache for this app.
*/
public class DeleteCacheService extends IntentService {
public class DeleteCacheService extends JobIntentService {
public static final String TAG = "DeleteCacheService";
public DeleteCacheService() {
super("DeleteCacheService");
}
public static void deleteAll(Context context) {
Intent intent = new Intent(context, DeleteCacheService.class);
context.startService(intent);
enqueueWork(context, DeleteCacheService.class, 0x523432, intent);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
return;
}
protected void onHandleWork(@NonNull Intent intent) {
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
Log.w(TAG, "Deleting all cached contents!");
try {

View File

@ -3,15 +3,10 @@ package org.fdroid.fdroid;
import android.annotation.TargetApi;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Intent;
/**
* Interface between the new {@link android.app.job.JobScheduler} API and
* our old {@link UpdateService}, which is based on {@link android.app.IntentService}.
* This does not do things the way it should, e.g. stopping the job on
* {@link #onStopJob(JobParameters)} and properly reporting
* {@link #jobFinished(JobParameters, boolean)}, but this at least provides
* the nice early triggering when there is good power/wifi available.
* {@link UpdateService}, which is based on {@link android.support.v4.app.JobIntentService}.
*
* @see <a href="https://developer.android.com/about/versions/android-5.0.html#Power">Project Volta: Scheduling jobs</a>
*/
@ -19,25 +14,13 @@ import android.content.Intent;
public class UpdateJobService extends JobService {
@Override
public boolean onStartJob(final JobParameters params) {
new Thread() {
@Override
public void run() {
// faking the actually run time
try {
startService(new Intent(UpdateJobService.this, UpdateService.class));
Thread.sleep(2000);
} catch (InterruptedException e) {
// ignored
} finally {
jobFinished(params, false);
}
}
}.start();
return true;
UpdateService.updateNow(this);
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
// TODO this should gracefully stop UpdateService
return true;
}
}

View File

@ -30,12 +30,15 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.preference.PreferenceManager;
@ -59,7 +62,7 @@ import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class UpdateService extends IntentService {
public class UpdateService extends JobIntentService {
private static final String TAG = "UpdateService";
@ -68,7 +71,6 @@ public class UpdateService extends IntentService {
public static final String EXTRA_MESSAGE = "msg";
public static final String EXTRA_REPO_ERRORS = "repoErrors";
public static final String EXTRA_STATUS_CODE = "status";
public static final String EXTRA_ADDRESS = "address";
public static final String EXTRA_MANUAL_UPDATE = "manualUpdate";
public static final String EXTRA_FORCED_UPDATE = "forcedUpdate";
public static final String EXTRA_PROGRESS = "progress";
@ -81,6 +83,7 @@ public class UpdateService extends IntentService {
public static final int STATUS_INFO = 5;
private static final String STATE_LAST_UPDATED = "lastUpdateCheck";
private static final int JOB_ID = 0xfedcba;
private static final int NOTIFY_ID_UPDATING = 0;
@ -92,10 +95,6 @@ public class UpdateService extends IntentService {
private static boolean updating;
public UpdateService() {
super("UpdateService");
}
public static void updateNow(Context context) {
updateRepoNow(context, null);
}
@ -104,9 +103,9 @@ public class UpdateService extends IntentService {
Intent intent = new Intent(context, UpdateService.class);
intent.putExtra(EXTRA_MANUAL_UPDATE, true);
if (!TextUtils.isEmpty(address)) {
intent.putExtra(EXTRA_ADDRESS, address);
intent.setData(Uri.parse(address));
}
context.startService(intent);
enqueueWork(context, intent);
}
/**
@ -117,7 +116,16 @@ public class UpdateService extends IntentService {
public static void forceUpdateRepo(Context context) {
Intent intent = new Intent(context, UpdateService.class);
intent.putExtra(EXTRA_FORCED_UPDATE, true);
context.startService(intent);
enqueueWork(context, intent);
}
/**
* Add work to the queue for processing now
*
* @see JobIntentService#enqueueWork(Context, Class, int, Intent)
*/
private static void enqueueWork(Context context, @NonNull Intent intent) {
enqueueWork(context, UpdateService.class, JOB_ID, intent);
}
/**
@ -150,7 +158,7 @@ public class UpdateService extends IntentService {
Utils.debugLog(TAG, "Using android-21 JobScheduler for updates");
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName componentName = new ComponentName(context, UpdateJobService.class);
JobInfo.Builder builder = new JobInfo.Builder(0xfedcba, componentName)
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, componentName)
.setRequiresDeviceIdle(true)
.setPeriodic(interval);
if (Build.VERSION.SDK_INT >= 26) {
@ -368,18 +376,13 @@ public class UpdateService extends IntentService {
}
@Override
protected void onHandleIntent(Intent intent) {
protected void onHandleWork(@NonNull Intent intent) {
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
final long startTime = System.currentTimeMillis();
boolean manualUpdate = false;
boolean forcedUpdate = false;
String address = null;
if (intent != null) {
address = intent.getStringExtra(EXTRA_ADDRESS); // TODO switch to Intent.setData()
manualUpdate = intent.getBooleanExtra(EXTRA_MANUAL_UPDATE, false);
forcedUpdate = intent.getBooleanExtra(EXTRA_FORCED_UPDATE, false);
}
boolean manualUpdate = intent.getBooleanExtra(EXTRA_MANUAL_UPDATE, false);
boolean forcedUpdate = intent.getBooleanExtra(EXTRA_FORCED_UPDATE, false);
String address = intent.getDataString();
try {
final Preferences fdroidPrefs = Preferences.get();

View File

@ -492,6 +492,7 @@ public final class Utils {
* probably warranted. See https://www.gitlab.com/fdroid/fdroidclient/issues/855
* for more detail.
*/
@Nullable
public static String getBinaryHash(File apk, String algo) {
FileInputStream fis = null;
try {
@ -514,12 +515,12 @@ public final class Utils {
} else if (message.contains(" ENOENT ")) {
Utils.debugLog(TAG, apk + " vanished: " + message);
}
throw new IllegalArgumentException(e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
} finally {
closeQuietly(fis);
}
return null;
}
/**

View File

@ -70,6 +70,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public String versionName;
public int versionCode;
public int size; // Size in bytes - 0 means we don't know!
@NonNull
public String hash; // checksum of the APK, in lowercase hex
public String hashType;
public int minSdkVersion = SDK_VERSION_MIN_VALUE; // 0 if unknown
@ -358,7 +359,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
@Override
@TargetApi(19)
public int compareTo(Apk apk) {
public int compareTo(@NonNull Apk apk) {
if (Build.VERSION.SDK_INT < 19) {
return Integer.valueOf(versionCode).compareTo(apk.versionCode);
}

View File

@ -9,7 +9,6 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.AntiFeatureTable;
import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable;
@ -20,13 +19,13 @@ import org.fdroid.fdroid.data.Schema.PackageTable;
import org.fdroid.fdroid.data.Schema.RepoTable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@SuppressWarnings("LineLength")
public class ApkProvider extends FDroidProvider {
private static final String TAG = "ApkProvider";
@ -41,7 +40,8 @@ public class ApkProvider extends FDroidProvider {
public static final class Helper {
private Helper() { }
private Helper() {
}
public static void update(Context context, Apk apk) {
ContentResolver resolver = context.getContentResolver();
@ -83,19 +83,21 @@ public class ApkProvider extends FDroidProvider {
/**
* Find an app which is closest to the version code suggested by the server, with some caveates:
* <ul>
* <li>If installed, limit to apks signed by the same signer as the installed apk.</li>
* <li>Otherwise, limit to apks signed by the "preferred" signer (see {@link App#preferredSigner}).</li>
* <li>If installed, limit to apks signed by the same signer as the installed apk.</li>
* <li>Otherwise, limit to apks signed by the "preferred" signer (see {@link App#preferredSigner}).</li>
* </ul>
*/
public static Apk findSuggestedApk(Context context, App app) {
return findApkFromAnyRepo(context, app.packageName, app.suggestedVersionCode, app.getMostAppropriateSignature());
return findApkFromAnyRepo(context, app.packageName, app.suggestedVersionCode,
app.getMostAppropriateSignature());
}
public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode) {
return findApkFromAnyRepo(context, packageName, versionCode, null, Cols.ALL);
}
public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode, String signature) {
public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode,
String signature) {
return findApkFromAnyRepo(context, packageName, versionCode, signature, Cols.ALL);
}
@ -162,6 +164,9 @@ public class ApkProvider extends FDroidProvider {
@NonNull
public static List<Apk> findApksByHash(Context context, String apkHash) {
if (apkHash == null) {
return Collections.emptyList();
}
ContentResolver resolver = context.getContentResolver();
final Uri uri = getContentUri();
String selection = " apk." + Cols.HASH + " = ? ";
@ -185,7 +190,7 @@ public class ApkProvider extends FDroidProvider {
protected static final String PATH_REPO_APP = "repo-app";
private static final String PATH_APKS = "apks";
private static final String PATH_APP = "app";
private static final String PATH_REPO = "repo";
private static final String PATH_REPO = "repo";
private static final String PATH_APK_ROW_ID = "apk-rowId";
private static final UriMatcher MATCHER = new UriMatcher(-1);
@ -226,27 +231,27 @@ public class ApkProvider extends FDroidProvider {
public static Uri getAppUri(String packageName) {
return getContentUri()
.buildUpon()
.appendPath(PATH_APP)
.appendPath(packageName)
.build();
.buildUpon()
.appendPath(PATH_APP)
.appendPath(packageName)
.build();
}
public static Uri getRepoUri(long repoId) {
return getContentUri()
.buildUpon()
.appendPath(PATH_REPO)
.appendPath(Long.toString(repoId))
.build();
.buildUpon()
.appendPath(PATH_REPO)
.appendPath(Long.toString(repoId))
.build();
}
public static Uri getRepoUri(long repoId, String packageName) {
return getContentUri()
.buildUpon()
.appendPath(PATH_REPO_APP)
.appendPath(Long.toString(repoId))
.appendPath(packageName)
.build();
.buildUpon()
.appendPath(PATH_REPO_APP)
.appendPath(Long.toString(repoId))
.appendPath(packageName)
.build();
}
public static Uri getApkFromAnyRepoUri(Apk apk) {
@ -296,10 +301,13 @@ public class ApkProvider extends FDroidProvider {
private boolean antiFeaturesRequested;
/**
* If the query includes anti features, then we group by apk id. This is because joining onto the anti-features
* table will result in multiple result rows for each apk (potentially), so we will GROUP_CONCAT each of the
* anti features into a single comma separated list for each apk. If we are _not_ including anti features, then
* don't group by apk, because when doing a COUNT(*) this will result in the wrong result.
* If the query includes anti features, then we group by apk id. This
* is because joining onto the anti-features table will result in
* multiple result rows for each apk (potentially), so we will
* {@code GROUP_CONCAT} each of the anti-features into a single comma-
* separated list for each apk. If we are _not_ including anti-
* features, then don't group by apk, because when doing a COUNT(*)
* this will result in the wrong result.
*/
@Override
protected String groupBy() {
@ -313,8 +321,8 @@ public class ApkProvider extends FDroidProvider {
final String pkg = PackageTable.NAME;
return apk + " AS apk " +
" LEFT JOIN " + app + " AS app ON (app." + AppMetadataTable.Cols.ROW_ID + " = apk." + Cols.APP_ID + ")" +
" LEFT JOIN " + pkg + " AS pkg ON (pkg." + PackageTable.Cols.ROW_ID + " = app." + AppMetadataTable.Cols.PACKAGE_ID + ")";
" LEFT JOIN " + app + " AS app ON (app." + AppMetadataTable.Cols.ROW_ID + " = apk." + Cols.APP_ID + ")" + // NOPMD NOCHECKSTYLE LineLength
" LEFT JOIN " + pkg + " AS pkg ON (pkg." + PackageTable.Cols.ROW_ID + " = app." + AppMetadataTable.Cols.PACKAGE_ID + ")"; // NOPMD NOCHECKSTYLE LineLength
}
@Override
@ -357,9 +365,11 @@ public class ApkProvider extends FDroidProvider {
"apk." + Cols.ROW_ID + " = " + apkAntiFeature + "." + ApkAntiFeatureJoinTable.Cols.APK_ID);
leftJoin(AntiFeatureTable.NAME, antiFeature,
apkAntiFeature + "." + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + " = " + antiFeature + "." + AntiFeatureTable.Cols.ROW_ID);
apkAntiFeature + "." + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + " = "
+ antiFeature + "." + AntiFeatureTable.Cols.ROW_ID);
appendField("group_concat(" + antiFeature + "." + AntiFeatureTable.Cols.NAME + ") as " + Cols.AntiFeatures.ANTI_FEATURES);
appendField("group_concat(" + antiFeature + "." + AntiFeatureTable.Cols.NAME + ") as "
+ Cols.AntiFeatures.ANTI_FEATURES);
}
}
@ -378,11 +388,11 @@ public class ApkProvider extends FDroidProvider {
String selection =
alias + Cols.VERSION_CODE + " = ? AND " +
alias + Cols.APP_ID + " IN (" + getMetadataIdFromPackageNameQuery() + ")";
alias + Cols.APP_ID + " IN (" + getMetadataIdFromPackageNameQuery() + ")";
List<String> pathSegments = uri.getPathSegments();
List<String> args = new ArrayList<>(3);
args.add(pathSegments.get(1)); // First (0th) path segment is the word "apk" and we are not interested in it.
args.add(pathSegments.get(1)); // 0th path segment is the word "apk" and we are not interested in it.
args.add(pathSegments.get(2));
if (pathSegments.size() >= 4) {
@ -438,8 +448,8 @@ public class ApkProvider extends FDroidProvider {
final String[] apkDetails = apkKeys.split(",");
if (apkDetails.length > MAX_APKS_TO_QUERY) {
throw new IllegalArgumentException(
"Cannot query more than " + MAX_APKS_TO_QUERY + ". " +
"You tried to query " + apkDetails.length);
"Cannot query more than " + MAX_APKS_TO_QUERY + ". " +
"You tried to query " + apkDetails.length);
}
String alias = includeAlias ? "apk." : "";
final String[] args = new String[apkDetails.length * 2];
@ -569,7 +579,9 @@ public class ApkProvider extends FDroidProvider {
}
protected void ensureAntiFeatures(String[] antiFeatures, long apkId) {
db().delete(getApkAntiFeatureJoinTableName(), ApkAntiFeatureJoinTable.Cols.APK_ID + " = ?", new String[] {Long.toString(apkId)});
db().delete(getApkAntiFeatureJoinTableName(),
ApkAntiFeatureJoinTable.Cols.APK_ID + " = ?",
new String[]{Long.toString(apkId)});
if (antiFeatures != null) {
Set<String> antiFeatureSet = new HashSet<>();
for (String antiFeatureName : antiFeatures) {
@ -594,7 +606,8 @@ public class ApkProvider extends FDroidProvider {
protected long ensureAntiFeature(String antiFeatureName) {
long antiFeatureId = 0;
Cursor cursor = db().query(AntiFeatureTable.NAME, new String[] {AntiFeatureTable.Cols.ROW_ID}, AntiFeatureTable.Cols.NAME + " = ?", new String[]{antiFeatureName}, null, null, null);
Cursor cursor = db().query(AntiFeatureTable.NAME, new String[]{AntiFeatureTable.Cols.ROW_ID},
AntiFeatureTable.Cols.NAME + " = ?", new String[]{antiFeatureName}, null, null, null);
if (cursor != null) {
if (cursor.getCount() > 0) {
cursor.moveToFirst();

View File

@ -369,15 +369,33 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
/**
* Instantiate from a locally installed package.
* <p>
* Initializes an {@link App} instances from an APK file. Since the file
* could in the cache, and files can disappear from the cache at any time,
* this needs to be quite defensive ensuring that {@code apkFile} still
* exists.
*/
public App(Context context, PackageManager pm, String packageName)
@Nullable
public static App getInstance(Context context, PackageManager pm, String packageName)
throws CertificateEncodingException, IOException, PackageManager.NameNotFoundException {
App app = new App();
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
setFromPackageInfo(pm, packageInfo);
this.installedApk = new Apk();
SanitizedFile apkFile = SanitizedFile.knownSanitized(packageInfo.applicationInfo.publicSourceDir);
initApkFromApkFile(context, this.installedApk, packageInfo, apkFile);
if (apkFile.canRead()) {
String hashType = "SHA-256";
String hash = Utils.getBinaryHash(apkFile, hashType);
if (TextUtils.isEmpty(hash)) {
return null;
}
app.installedApk.hashType = hashType;
app.installedApk.hash = hash;
app.installedApk.sig = Utils.getPackageSig(packageInfo);
}
app.setFromPackageInfo(pm, packageInfo);
app.installedApk = new Apk();
app.initInstalledApk(context, app.installedApk, packageInfo, apkFile);
return app;
}
/**
@ -704,22 +722,6 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
this.compatible = true;
}
/**
* Initializes an {@link App} instances from an APK file. Since the file
* could in the cache, and files can disappear from the cache at any time,
* this needs to be quite defensive ensuring that {@code apkFile} still
* exists.
*/
private void initApkFromApkFile(Context context, Apk apk, PackageInfo packageInfo, SanitizedFile apkFile)
throws IOException, CertificateEncodingException {
if (apkFile.canRead()) {
apk.hashType = "sha256";
apk.hash = Utils.getBinaryHash(apkFile, apk.hashType);
apk.sig = Utils.getPackageSig(packageInfo);
}
initInstalledApk(context, apk, packageInfo, apkFile);
}
public static void initInstalledObbFiles(Apk apk) {
File obbdir = getObbDir(apk.packageName);
FileFilter filter = new RegexFileFilter("(main|patch)\\.[0-9-][0-9]*\\." + apk.packageName + "\\.obb");
@ -743,6 +745,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
}
}
@SuppressWarnings("EmptyForIteratorPad")
private void initInstalledApk(Context context, Apk apk, PackageInfo packageInfo, SanitizedFile apkFile)
throws IOException, CertificateEncodingException {
apk.compatible = true;
@ -775,7 +778,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
JarFile apkJar = new JarFile(apkFile);
HashSet<String> abis = new HashSet<>(3);
Pattern pattern = Pattern.compile("^lib/([a-z0-9-]+)/.*");
for (Enumeration<JarEntry> jarEntries = apkJar.entries(); jarEntries.hasMoreElements();) {
for (Enumeration<JarEntry> jarEntries = apkJar.entries(); jarEntries.hasMoreElements(); ) {
JarEntry jarEntry = jarEntries.nextElement();
Matcher matcher = pattern.matcher(jarEntry.getName());
if (matcher.matches()) {

View File

@ -1,6 +1,5 @@
package org.fdroid.fdroid.data;
import android.app.IntentService;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@ -8,7 +7,9 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Process;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.JobIntentService;
import android.util.Log;
import org.acra.ACRA;
import org.fdroid.fdroid.AppUpdateStatusManager;
@ -40,7 +41,7 @@ import java.util.concurrent.TimeUnit;
* of this stuff.
*/
@SuppressWarnings("LineLength")
public class InstalledAppProviderService extends IntentService {
public class InstalledAppProviderService extends JobIntentService {
private static final String TAG = "InstalledAppProviderSer";
private static final String ACTION_INSERT = "org.fdroid.fdroid.data.action.INSERT";
@ -56,10 +57,6 @@ public class InstalledAppProviderService extends IntentService {
*/
private PublishSubject<String> packageChangeNotifier;
public InstalledAppProviderService() {
super("InstalledAppProviderService");
}
@Override
public void onCreate() {
super.onCreate();
@ -121,7 +118,7 @@ public class InstalledAppProviderService extends IntentService {
intent.setAction(ACTION_INSERT);
intent.setData(uri);
intent.putExtra(EXTRA_PACKAGE_INFO, packageInfo);
context.startService(intent);
enqueueWork(context, intent);
}
/**
@ -138,7 +135,11 @@ public class InstalledAppProviderService extends IntentService {
Intent intent = new Intent(context, InstalledAppProviderService.class);
intent.setAction(ACTION_DELETE);
intent.setData(uri);
context.startService(intent);
enqueueWork(context, intent);
}
private static void enqueueWork(Context context, Intent intent) {
enqueueWork(context, InstalledAppProviderService.class, 0x192834, intent);
}
/**
@ -153,7 +154,7 @@ public class InstalledAppProviderService extends IntentService {
* 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
* {@link JobIntentService}. 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}.
* <p>
@ -218,11 +219,8 @@ public class InstalledAppProviderService extends IntentService {
}
@Override
protected void onHandleIntent(Intent intent) {
protected void onHandleWork(@NonNull Intent intent) {
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
if (intent == null) {
return;
}
String packageName = intent.getData().getSchemeSpecificPart();
final String action = intent.getAction();

View File

@ -24,6 +24,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.fdroid.fdroid.data.Apk;
/**
@ -37,7 +38,7 @@ public class DefaultInstaller extends Installer {
public static final String TAG = "DefaultInstaller";
DefaultInstaller(Context context, Apk apk) {
DefaultInstaller(Context context, @NonNull Apk apk) {
super(context, apk);
}

View File

@ -24,6 +24,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
@ -44,7 +45,7 @@ import java.io.File;
@Deprecated
public class ExtensionInstaller extends Installer {
ExtensionInstaller(Context context, Apk apk) {
ExtensionInstaller(Context context, @NonNull Apk apk) {
super(context, apk);
}

View File

@ -24,11 +24,12 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.fdroid.fdroid.data.Apk;
public class FileInstaller extends Installer {
public FileInstaller(Context context, Apk apk) {
public FileInstaller(Context context, @NonNull Apk apk) {
super(context, apk);
}

View File

@ -28,6 +28,7 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.PatternMatcher;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import org.fdroid.fdroid.Utils;
@ -77,7 +78,7 @@ public abstract class Installer {
* @param apk must be included so that all the phases of the install process
* can get all the data about the app, even after F-Droid was killed
*/
Installer(Context context, Apk apk) {
Installer(Context context, @NonNull Apk apk) {
this.context = context;
this.apk = apk;
}
@ -144,9 +145,10 @@ public abstract class Installer {
}
PackageManager pm = context.getPackageManager();
if (Build.VERSION.SDK_INT >= 24 && (
pm.getInstallerPackageName(apk.packageName).equals("com.android.packageinstaller")
|| pm.getInstallerPackageName(apk.packageName).equals("com.google.android.packageinstaller"))) {
String installerPackageName = pm.getInstallerPackageName(apk.packageName);
if (Build.VERSION.SDK_INT >= 24 &&
("com.android.packageinstaller".equals(installerPackageName)
|| "com.google.android.packageinstaller".equals(installerPackageName))) {
Utils.debugLog(TAG, "Falling back to default installer for uninstall");
Intent intent = new Intent(context, DefaultInstallerActivity.class);
intent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);

View File

@ -21,8 +21,8 @@
package org.fdroid.fdroid.installer;
import android.content.Context;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
@ -39,12 +39,11 @@ public class InstallerFactory {
* @param apk to be installed, always required.
* @return instance of an Installer
*/
public static Installer create(Context context, Apk apk) {
if (apk == null || TextUtils.isEmpty(apk.packageName)) {
public static Installer create(Context context, @NonNull Apk apk) {
if (TextUtils.isEmpty(apk.packageName)) {
throw new IllegalArgumentException("Apk.packageName must not be empty: " + apk);
}
Installer installer;
if (!apk.isApk()) {
Utils.debugLog(TAG, "Using FileInstaller for non-apk file");

View File

@ -20,11 +20,11 @@
package org.fdroid.fdroid.installer;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.fdroid.fdroid.Utils;
@ -37,11 +37,13 @@ import java.io.FileFilter;
* This service handles the install process of apk files and
* uninstall process of apps.
* <p>
* This service is based on an IntentService because:
* - no parallel installs/uninstalls should be allowed,
* i.e., runs sequentially
* - no cancel operation is needed. Cancelling an installation
* would be the same as starting uninstall afterwards
* This service is based on an JobIntentService because:
* <ul>
* <li>no parallel installs/uninstalls should be allowed,
* i.e., runs sequentially</li>
* <li>no cancel operation is needed. Cancelling an installation
* would be the same as starting uninstall afterwards</li>
* </ul>
* <p>
* The download URL is only used as the unique ID that represents this
* particular apk throughout the whole install process in
@ -52,23 +54,15 @@ import java.io.FileFilter;
* <a href="https://developer.android.com/google/play/expansion-files.html">
* APK Expansion Files</a> spec.
*/
public class InstallerService extends IntentService {
public class InstallerService extends JobIntentService {
public static final String TAG = "InstallerService";
private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.InstallerService.action.INSTALL";
private static final String ACTION_UNINSTALL = "org.fdroid.fdroid.installer.InstallerService.action.UNINSTALL";
public InstallerService() {
super("InstallerService");
}
@Override
protected void onHandleIntent(Intent intent) {
protected void onHandleWork(@NonNull Intent intent) {
final Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
if (apk == null) {
Utils.debugLog(TAG, "ignoring intent with null EXTRA_APK: " + intent);
return;
}
Installer installer = InstallerFactory.create(this, apk);
if (ACTION_INSTALL.equals(intent.getAction())) {
@ -117,7 +111,7 @@ public class InstallerService extends IntentService {
intent.setData(localApkUri);
intent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri);
intent.putExtra(Installer.EXTRA_APK, apk);
context.startService(intent);
enqueueWork(context, intent);
}
/**
@ -130,7 +124,10 @@ public class InstallerService extends IntentService {
Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_UNINSTALL);
intent.putExtra(Installer.EXTRA_APK, apk);
context.startService(intent);
enqueueWork(context, intent);
}
private static void enqueueWork(Context context, @NonNull Intent intent) {
enqueueWork(context, InstallerService.class, 0x872394, intent);
}
}

View File

@ -29,6 +29,7 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.util.Log;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
@ -256,7 +257,7 @@ public class PrivilegedInstaller extends Installer {
"device owner has marked the package as uninstallable.");
}
public PrivilegedInstaller(Context context, Apk apk) {
public PrivilegedInstaller(Context context, @NonNull Apk apk) {
super(context, apk);
}

View File

@ -74,8 +74,10 @@ public class CacheSwapAppsService extends IntentService {
try {
PackageManager pm = getPackageManager();
String packageName = intent.getData().getSchemeSpecificPart();
App app = new App(this, pm, packageName);
SwapService.putAppInCache(packageName, app);
App app = App.getInstance(this, pm, packageName);
if (app != null) {
SwapService.putAppInCache(packageName, app);
}
} catch (CertificateEncodingException | IOException | PackageManager.NameNotFoundException e) {
e.printStackTrace();
}

View File

@ -259,9 +259,9 @@ public final class LocalRepoManager {
try {
app = SwapService.getAppFromCache(packageName);
if (app == null) {
app = new App(context.getApplicationContext(), pm, packageName);
app = App.getInstance(context.getApplicationContext(), pm, packageName);
}
if (!app.isValid()) {
if (app == null || !app.isValid()) {
return;
}
} catch (PackageManager.NameNotFoundException | CertificateEncodingException | IOException e) {

View File

@ -94,7 +94,7 @@ public class SwapService extends Service {
return INSTALLED_APPS.get(packageName);
}
static void putAppInCache(String packageName, App app) {
static void putAppInCache(String packageName, @NonNull App app) {
INSTALLED_APPS.put(packageName, app);
}

View File

@ -1,6 +1,5 @@
package org.fdroid.fdroid.net;
import android.app.IntentService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -8,16 +7,18 @@ import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
/**
* An {@link IntentService} subclass for tracking whether there is metered or
* An {@link JobIntentService} subclass for tracking whether there is metered or
* unmetered internet available, based on
* {@link android.net.ConnectivityManager#CONNECTIVITY_ACTION}
*/
public class ConnectivityMonitorService extends IntentService {
public class ConnectivityMonitorService extends JobIntentService {
public static final String TAG = "ConnectivityMonitorServ";
public static final int FLAG_NET_UNAVAILABLE = 0;
@ -33,10 +34,10 @@ public class ConnectivityMonitorService extends IntentService {
}
};
public ConnectivityMonitorService() {
super("ConnectivityMonitorService");
}
/**
* Register the {@link BroadcastReceiver} which also starts this
* {@code Service} since it is a sticky broadcast.
*/
public static void registerAndStart(Context context) {
context.registerReceiver(CONNECTIVITY_RECEIVER, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
@ -44,7 +45,7 @@ public class ConnectivityMonitorService extends IntentService {
public static void start(Context context) {
Intent intent = new Intent(context, ConnectivityMonitorService.class);
intent.setAction(ACTION_START);
context.startService(intent);
enqueueWork(context, ConnectivityMonitorService.class, 0x982ae7b, intent);
}
/**
@ -77,8 +78,8 @@ public class ConnectivityMonitorService extends IntentService {
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null && ACTION_START.equals(intent.getAction())) {
protected void onHandleWork(@NonNull Intent intent) {
if (ACTION_START.equals(intent.getAction())) {
FDroidApp.networkState = getNetworkState(this);
ImageLoader.getInstance().denyNetworkDownloads(!Preferences.get().isBackgroundDownloadAllowed());
}

View File

@ -47,7 +47,7 @@ import java.util.Locale;
* This also schedules an update to encourage updates happening on
* unmetered networks like typical WiFi rather than networks that can
* cost money or have caps. The logic for checking the state of the
* internet connection is in {@link org.fdroid.fdroid.UpdateService#onHandleIntent(Intent)}
* internet connection is in {@link org.fdroid.fdroid.UpdateService#onHandleWork(Intent)}
* <p>
* Some devices send multiple copies of given events, like a Moto G often
* sends three {@code CONNECTED} events. So they have to be debounced to

View File

@ -450,19 +450,24 @@ public class AppDetailsRecyclerViewAdapter
}
}
});
if (app.antiFeatures != null) {
if (app.antiFeatures != null && app.antiFeatures.length > 0) {
StringBuilder sb = new StringBuilder();
sb.append("<ul>");
for (String af : app.antiFeatures) {
String afdesc = descAntiFeature(af);
sb.append("\t• ").append(afdesc).append('\n');
}
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
antiFeaturesView.setText(sb.toString());
} else {
antiFeaturesView.setVisibility(View.GONE);
sb.append("<li><a href=\"https://f-droid.org/wiki/page/Antifeature:")
.append(af)
.append("\">")
.append(afdesc)
.append("</a></li>");
}
sb.append("</ul>");
antiFeaturesView.setText(Html.fromHtml(sb.toString()));
antiFeaturesView.setMovementMethod(LinkMovementMethod.getInstance());
} else {
antiFeaturesView.setVisibility(View.GONE);
}
updateAntiFeaturesWarning();
buttonSecondaryView.setText(R.string.menu_uninstall);
buttonSecondaryView.setVisibility(app.isUninstallable(context) ? View.VISIBLE : View.INVISIBLE);
@ -538,6 +543,10 @@ public class AppDetailsRecyclerViewAdapter
return itemView.getContext().getString(R.string.antiupstreamnonfreelist);
case "NonFreeAssets":
return itemView.getContext().getString(R.string.antinonfreeassetslist);
case "DisabledAlgorithm":
return itemView.getContext().getString(R.string.antidisabledalgorithmlist);
case "KnownVuln":
return itemView.getContext().getString(R.string.antiknownvulnlist);
default:
return af;
}
@ -664,7 +673,8 @@ public class AppDetailsRecyclerViewAdapter
contentView = (LinearLayout) view.findViewById(R.id.ll_content);
}
protected abstract @DrawableRes int getIcon();
@DrawableRes
protected abstract int getIcon();
/**
* Depending on whether we are expanded or not, update the icon which indicates whether the
@ -694,7 +704,8 @@ public class AppDetailsRecyclerViewAdapter
updateExpandableItem(showVersions);
}
protected @DrawableRes int getIcon() {
@DrawableRes
protected int getIcon() {
return R.drawable.ic_access_time_24dp_grey600;
}
}
@ -794,7 +805,8 @@ public class AppDetailsRecyclerViewAdapter
contentView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
}
protected @DrawableRes int getIcon() {
@DrawableRes
protected int getIcon() {
return R.drawable.ic_lock_24dp_grey600;
}
}
@ -867,7 +879,8 @@ public class AppDetailsRecyclerViewAdapter
}
}
protected @DrawableRes int getIcon() {
@DrawableRes
protected int getIcon() {
return R.drawable.ic_website;
}
}

View File

@ -203,6 +203,8 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="antinonfreedeplist">This app depends on other non-free apps</string>
<string name="antiupstreamnonfreelist">The upstream source code is not entirely Free</string>
<string name="antinonfreeassetslist">This app contains non-free assets</string>
<string name="antidisabledalgorithmlist">This app has a weak security signature</string>
<string name="antiknownvulnlist">This app contains a known security vulnerability</string>
<string name="display">Display</string>