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_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS"/> <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.NFC"/> <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) <!-- Indicate that F-Droid may request root access (introduced by Koush's Superuser app)
This permission is deprecated, but necessary for some old superuser This permission is deprecated, but necessary for some old superuser
@ -251,7 +252,10 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<service android:name=".UpdateService"/> <service
android:name=".UpdateService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service <service
android:name=".UpdateJobService" android:name=".UpdateJobService"
android:exported="false" android:exported="false"
@ -261,18 +265,26 @@
android:exported="false"/> android:exported="false"/>
<service <service
android:name=".installer.InstallerService" android:name=".installer.InstallerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/> android:exported="false"/>
<service <service
android:name=".CleanCacheService" 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"/> android:exported="false"/>
<service <service
android:name=".DeleteCacheService" android:name=".DeleteCacheService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/> android:exported="false"/>
<service <service
android:name=".net.WifiStateChangeService" android:name=".net.WifiStateChangeService"
android:exported="false"/> android:exported="false"/>
<service <service
android:name=".net.ConnectivityMonitorService" android:name=".net.ConnectivityMonitorService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/> android:exported="false"/>
<service android:name=".localrepo.SwapService"/> <service android:name=".localrepo.SwapService"/>
<service <service
@ -286,6 +298,7 @@
android:exported="false"/> android:exported="false"/>
<service <service
android:name=".data.InstalledAppProviderService" android:name=".data.InstalledAppProviderService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/> android:exported="false"/>
<service <service
android:name=".AppUpdateStatusService" 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; package org.fdroid.fdroid;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent; 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.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.Process; import android.os.Process;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.installer.ApkCache; 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()} * and newer. On older Android, last modified time from {@link File#lastModified()}
* is used. * 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 * Schedule or cancel this service to update the app index, according to the
@ -41,28 +48,36 @@ public class CleanCacheService extends IntentService {
interval = keepTime; interval = keepTime;
} }
Intent intent = new Intent(context, CleanCacheService.class); if (Build.VERSION.SDK_INT < 21) {
PendingIntent pending = PendingIntent.getService(context, 0, intent, 0); Intent intent = new Intent(context, CleanCacheService.class);
PendingIntent pending = PendingIntent.getService(context, 0, intent, 0);
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarm.cancel(pending); alarm.cancel(pending);
alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 5000, interval, pending); 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) { public static void start(Context context) {
context.startService(new Intent(context, CleanCacheService.class)); enqueueWork(context, CleanCacheService.class, JOB_ID, new Intent(context, CleanCacheService.class));
}
public CleanCacheService() {
super("CleanCacheService");
} }
@Override @Override
protected void onHandleIntent(Intent intent) { protected void onHandleWork(@NonNull Intent intent) {
if (intent == null) {
return;
}
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
deleteExpiredApksFromCache(); deleteExpiredApksFromCache();
deleteStrayIndexFiles(); deleteStrayIndexFiles();

View File

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

View File

@ -3,15 +3,10 @@ package org.fdroid.fdroid;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.job.JobParameters; import android.app.job.JobParameters;
import android.app.job.JobService; import android.app.job.JobService;
import android.content.Intent;
/** /**
* Interface between the new {@link android.app.job.JobScheduler} API and * Interface between the new {@link android.app.job.JobScheduler} API and
* our old {@link UpdateService}, which is based on {@link android.app.IntentService}. * {@link UpdateService}, which is based on {@link android.support.v4.app.JobIntentService}.
* 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.
* *
* @see <a href="https://developer.android.com/about/versions/android-5.0.html#Power">Project Volta: Scheduling jobs</a> * @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 { public class UpdateJobService extends JobService {
@Override @Override
public boolean onStartJob(final JobParameters params) { public boolean onStartJob(final JobParameters params) {
new Thread() { UpdateService.updateNow(this);
@Override return false;
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;
} }
@Override @Override
public boolean onStopJob(JobParameters params) { 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.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Process; import android.os.Process;
import android.os.SystemClock; 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.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.preference.PreferenceManager; import android.support.v7.preference.PreferenceManager;
@ -59,7 +62,7 @@ import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class UpdateService extends IntentService { public class UpdateService extends JobIntentService {
private static final String TAG = "UpdateService"; 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_MESSAGE = "msg";
public static final String EXTRA_REPO_ERRORS = "repoErrors"; public static final String EXTRA_REPO_ERRORS = "repoErrors";
public static final String EXTRA_STATUS_CODE = "status"; 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_MANUAL_UPDATE = "manualUpdate";
public static final String EXTRA_FORCED_UPDATE = "forcedUpdate"; public static final String EXTRA_FORCED_UPDATE = "forcedUpdate";
public static final String EXTRA_PROGRESS = "progress"; public static final String EXTRA_PROGRESS = "progress";
@ -81,6 +83,7 @@ public class UpdateService extends IntentService {
public static final int STATUS_INFO = 5; public static final int STATUS_INFO = 5;
private static final String STATE_LAST_UPDATED = "lastUpdateCheck"; private static final String STATE_LAST_UPDATED = "lastUpdateCheck";
private static final int JOB_ID = 0xfedcba;
private static final int NOTIFY_ID_UPDATING = 0; private static final int NOTIFY_ID_UPDATING = 0;
@ -92,10 +95,6 @@ public class UpdateService extends IntentService {
private static boolean updating; private static boolean updating;
public UpdateService() {
super("UpdateService");
}
public static void updateNow(Context context) { public static void updateNow(Context context) {
updateRepoNow(context, null); updateRepoNow(context, null);
} }
@ -104,9 +103,9 @@ public class UpdateService extends IntentService {
Intent intent = new Intent(context, UpdateService.class); Intent intent = new Intent(context, UpdateService.class);
intent.putExtra(EXTRA_MANUAL_UPDATE, true); intent.putExtra(EXTRA_MANUAL_UPDATE, true);
if (!TextUtils.isEmpty(address)) { 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) { public static void forceUpdateRepo(Context context) {
Intent intent = new Intent(context, UpdateService.class); Intent intent = new Intent(context, UpdateService.class);
intent.putExtra(EXTRA_FORCED_UPDATE, true); 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"); Utils.debugLog(TAG, "Using android-21 JobScheduler for updates");
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName componentName = new ComponentName(context, UpdateJobService.class); 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) .setRequiresDeviceIdle(true)
.setPeriodic(interval); .setPeriodic(interval);
if (Build.VERSION.SDK_INT >= 26) { if (Build.VERSION.SDK_INT >= 26) {
@ -368,18 +376,13 @@ public class UpdateService extends IntentService {
} }
@Override @Override
protected void onHandleIntent(Intent intent) { protected void onHandleWork(@NonNull Intent intent) {
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
final long startTime = System.currentTimeMillis(); final long startTime = System.currentTimeMillis();
boolean manualUpdate = false; boolean manualUpdate = intent.getBooleanExtra(EXTRA_MANUAL_UPDATE, false);
boolean forcedUpdate = false; boolean forcedUpdate = intent.getBooleanExtra(EXTRA_FORCED_UPDATE, false);
String address = null; String address = intent.getDataString();
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);
}
try { try {
final Preferences fdroidPrefs = Preferences.get(); 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 * probably warranted. See https://www.gitlab.com/fdroid/fdroidclient/issues/855
* for more detail. * for more detail.
*/ */
@Nullable
public static String getBinaryHash(File apk, String algo) { public static String getBinaryHash(File apk, String algo) {
FileInputStream fis = null; FileInputStream fis = null;
try { try {
@ -514,12 +515,12 @@ public final class Utils {
} else if (message.contains(" ENOENT ")) { } else if (message.contains(" ENOENT ")) {
Utils.debugLog(TAG, apk + " vanished: " + message); Utils.debugLog(TAG, apk + " vanished: " + message);
} }
throw new IllegalArgumentException(e);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} finally { } finally {
closeQuietly(fis); closeQuietly(fis);
} }
return null;
} }
/** /**

View File

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

View File

@ -369,15 +369,33 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
/** /**
* Instantiate from a locally installed package. * 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 { throws CertificateEncodingException, IOException, PackageManager.NameNotFoundException {
App app = new App();
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
setFromPackageInfo(pm, packageInfo);
this.installedApk = new Apk();
SanitizedFile apkFile = SanitizedFile.knownSanitized(packageInfo.applicationInfo.publicSourceDir); 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; 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) { public static void initInstalledObbFiles(Apk apk) {
File obbdir = getObbDir(apk.packageName); File obbdir = getObbDir(apk.packageName);
FileFilter filter = new RegexFileFilter("(main|patch)\\.[0-9-][0-9]*\\." + apk.packageName + "\\.obb"); 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) private void initInstalledApk(Context context, Apk apk, PackageInfo packageInfo, SanitizedFile apkFile)
throws IOException, CertificateEncodingException { throws IOException, CertificateEncodingException {
apk.compatible = true; apk.compatible = true;
@ -775,7 +778,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
JarFile apkJar = new JarFile(apkFile); JarFile apkJar = new JarFile(apkFile);
HashSet<String> abis = new HashSet<>(3); HashSet<String> abis = new HashSet<>(3);
Pattern pattern = Pattern.compile("^lib/([a-z0-9-]+)/.*"); 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(); JarEntry jarEntry = jarEntries.nextElement();
Matcher matcher = pattern.matcher(jarEntry.getName()); Matcher matcher = pattern.matcher(jarEntry.getName());
if (matcher.matches()) { if (matcher.matches()) {

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@ import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.PatternMatcher; import android.os.PatternMatcher;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils; import android.text.TextUtils;
import org.fdroid.fdroid.Utils; 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 * @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 * 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.context = context;
this.apk = apk; this.apk = apk;
} }
@ -144,9 +145,10 @@ public abstract class Installer {
} }
PackageManager pm = context.getPackageManager(); PackageManager pm = context.getPackageManager();
if (Build.VERSION.SDK_INT >= 24 && ( String installerPackageName = pm.getInstallerPackageName(apk.packageName);
pm.getInstallerPackageName(apk.packageName).equals("com.android.packageinstaller") if (Build.VERSION.SDK_INT >= 24 &&
|| pm.getInstallerPackageName(apk.packageName).equals("com.google.android.packageinstaller"))) { ("com.android.packageinstaller".equals(installerPackageName)
|| "com.google.android.packageinstaller".equals(installerPackageName))) {
Utils.debugLog(TAG, "Falling back to default installer for uninstall"); Utils.debugLog(TAG, "Falling back to default installer for uninstall");
Intent intent = new Intent(context, DefaultInstallerActivity.class); Intent intent = new Intent(context, DefaultInstallerActivity.class);
intent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE); intent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);

View File

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

View File

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

View File

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

View File

@ -259,9 +259,9 @@ public final class LocalRepoManager {
try { try {
app = SwapService.getAppFromCache(packageName); app = SwapService.getAppFromCache(packageName);
if (app == null) { 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; return;
} }
} catch (PackageManager.NameNotFoundException | CertificateEncodingException | IOException e) { } catch (PackageManager.NameNotFoundException | CertificateEncodingException | IOException e) {

View File

@ -94,7 +94,7 @@ public class SwapService extends Service {
return INSTALLED_APPS.get(packageName); 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); INSTALLED_APPS.put(packageName, app);
} }

View File

@ -1,6 +1,5 @@
package org.fdroid.fdroid.net; package org.fdroid.fdroid.net;
import android.app.IntentService;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -8,16 +7,18 @@ import android.content.IntentFilter;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.os.Build; import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences; 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 * unmetered internet available, based on
* {@link android.net.ConnectivityManager#CONNECTIVITY_ACTION} * {@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 String TAG = "ConnectivityMonitorServ";
public static final int FLAG_NET_UNAVAILABLE = 0; 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) { public static void registerAndStart(Context context) {
context.registerReceiver(CONNECTIVITY_RECEIVER, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); context.registerReceiver(CONNECTIVITY_RECEIVER, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
} }
@ -44,7 +45,7 @@ public class ConnectivityMonitorService extends IntentService {
public static void start(Context context) { public static void start(Context context) {
Intent intent = new Intent(context, ConnectivityMonitorService.class); Intent intent = new Intent(context, ConnectivityMonitorService.class);
intent.setAction(ACTION_START); intent.setAction(ACTION_START);
context.startService(intent); enqueueWork(context, ConnectivityMonitorService.class, 0x982ae7b, intent);
} }
/** /**
@ -77,8 +78,8 @@ public class ConnectivityMonitorService extends IntentService {
} }
@Override @Override
protected void onHandleIntent(Intent intent) { protected void onHandleWork(@NonNull Intent intent) {
if (intent != null && ACTION_START.equals(intent.getAction())) { if (ACTION_START.equals(intent.getAction())) {
FDroidApp.networkState = getNetworkState(this); FDroidApp.networkState = getNetworkState(this);
ImageLoader.getInstance().denyNetworkDownloads(!Preferences.get().isBackgroundDownloadAllowed()); 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 * This also schedules an update to encourage updates happening on
* unmetered networks like typical WiFi rather than networks that can * unmetered networks like typical WiFi rather than networks that can
* cost money or have caps. The logic for checking the state of the * 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> * <p>
* Some devices send multiple copies of given events, like a Moto G often * 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 * 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(); StringBuilder sb = new StringBuilder();
sb.append("<ul>");
for (String af : app.antiFeatures) { for (String af : app.antiFeatures) {
String afdesc = descAntiFeature(af); String afdesc = descAntiFeature(af);
sb.append("\t• ").append(afdesc).append('\n'); sb.append("<li><a href=\"https://f-droid.org/wiki/page/Antifeature:")
} .append(af)
if (sb.length() > 0) { .append("\">")
sb.setLength(sb.length() - 1); .append(afdesc)
antiFeaturesView.setText(sb.toString()); .append("</a></li>");
} else {
antiFeaturesView.setVisibility(View.GONE);
} }
sb.append("</ul>");
antiFeaturesView.setText(Html.fromHtml(sb.toString()));
antiFeaturesView.setMovementMethod(LinkMovementMethod.getInstance());
} else {
antiFeaturesView.setVisibility(View.GONE);
} }
updateAntiFeaturesWarning(); updateAntiFeaturesWarning();
buttonSecondaryView.setText(R.string.menu_uninstall); buttonSecondaryView.setText(R.string.menu_uninstall);
buttonSecondaryView.setVisibility(app.isUninstallable(context) ? View.VISIBLE : View.INVISIBLE); buttonSecondaryView.setVisibility(app.isUninstallable(context) ? View.VISIBLE : View.INVISIBLE);
@ -538,6 +543,10 @@ public class AppDetailsRecyclerViewAdapter
return itemView.getContext().getString(R.string.antiupstreamnonfreelist); return itemView.getContext().getString(R.string.antiupstreamnonfreelist);
case "NonFreeAssets": case "NonFreeAssets":
return itemView.getContext().getString(R.string.antinonfreeassetslist); 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: default:
return af; return af;
} }
@ -664,7 +673,8 @@ public class AppDetailsRecyclerViewAdapter
contentView = (LinearLayout) view.findViewById(R.id.ll_content); 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 * Depending on whether we are expanded or not, update the icon which indicates whether the
@ -694,7 +704,8 @@ public class AppDetailsRecyclerViewAdapter
updateExpandableItem(showVersions); updateExpandableItem(showVersions);
} }
protected @DrawableRes int getIcon() { @DrawableRes
protected int getIcon() {
return R.drawable.ic_access_time_24dp_grey600; return R.drawable.ic_access_time_24dp_grey600;
} }
} }
@ -794,7 +805,8 @@ public class AppDetailsRecyclerViewAdapter
contentView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL)); contentView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
} }
protected @DrawableRes int getIcon() { @DrawableRes
protected int getIcon() {
return R.drawable.ic_lock_24dp_grey600; 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; 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="antinonfreedeplist">This app depends on other non-free apps</string>
<string name="antiupstreamnonfreelist">The upstream source code is not entirely Free</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="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> <string name="display">Display</string>