Merge branch 'package-installer' into 'master'
Add new PackageInstaller API and use with basic Closes #2542, #2592, #2560, and #1836 See merge request fdroid/fdroidclient!1216
This commit is contained in:
commit
2a28f2bca7
|
@ -42,12 +42,10 @@
|
|||
android:title="@string/over_data"
|
||||
android:defaultValue="@integer/defaultOverData"
|
||||
android:layout="@layout/preference_seekbar"/>
|
||||
<!-- TODO re-enable once basic can do auto-downloads
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/update_auto_download"
|
||||
android:summary="@string/update_auto_download_summary"
|
||||
android:key="updateAutoDownload"/>
|
||||
-->
|
||||
<org.fdroid.fdroid.views.LiveSeekBarPreference
|
||||
android:key="updateIntervalSeekBarPosition"
|
||||
android:title="@string/update_interval"
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
@ -470,7 +471,8 @@
|
|||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service
|
||||
android:name=".net.DownloaderService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service
|
||||
android:name=".installer.InstallerService"
|
||||
android:exported="false"
|
||||
|
@ -486,13 +488,10 @@
|
|||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".installer.InstallManagerService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".installer.InstallHistoryService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service
|
||||
android:name=".installer.ObfInstallerService"
|
||||
android:exported="false" />
|
||||
|
|
|
@ -20,7 +20,6 @@ import org.fdroid.fdroid.data.Apk;
|
|||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.DBHelper;
|
||||
import org.fdroid.fdroid.installer.ErrorDialogActivity;
|
||||
import org.fdroid.fdroid.installer.InstallManagerService;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
import org.fdroid.fdroid.views.AppDetailsActivity;
|
||||
|
||||
|
@ -311,7 +310,6 @@ public final class AppUpdateStatusManager {
|
|||
notifyChange(entry, isStatusUpdate);
|
||||
|
||||
if (status == Status.Installed) {
|
||||
InstallManagerService.removePendingInstall(context, entry.getCanonicalUrl());
|
||||
// After an app got installed, update available updates
|
||||
checkForUpdates();
|
||||
}
|
||||
|
@ -324,10 +322,6 @@ public final class AppUpdateStatusManager {
|
|||
setEntryContentIntentIfEmpty(entry);
|
||||
appMapping.put(entry.getCanonicalUrl(), entry);
|
||||
notifyAdd(entry);
|
||||
|
||||
if (status == Status.Installed) {
|
||||
InstallManagerService.removePendingInstall(context, entry.getCanonicalUrl());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyChange(String reason) {
|
||||
|
@ -479,7 +473,6 @@ public final class AppUpdateStatusManager {
|
|||
*/
|
||||
public void removeApk(String canonicalUrl) {
|
||||
synchronized (appMapping) {
|
||||
InstallManagerService.removePendingInstall(context, canonicalUrl);
|
||||
AppUpdateStatus entry = appMapping.remove(canonicalUrl);
|
||||
if (entry != null) {
|
||||
Utils.debugLog(LOGTAG, "Remove APK " + entry.apk.getApkPath());
|
||||
|
@ -535,8 +528,6 @@ public final class AppUpdateStatusManager {
|
|||
entry.errorText = errorText;
|
||||
entry.intent = getAppErrorIntent(entry);
|
||||
notifyChange(entry, false);
|
||||
|
||||
InstallManagerService.removePendingInstall(context, entry.getCanonicalUrl());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ import org.fdroid.fdroid.data.App;
|
|||
import org.fdroid.fdroid.data.DBHelper;
|
||||
import org.fdroid.fdroid.installer.ApkFileProvider;
|
||||
import org.fdroid.fdroid.installer.InstallHistoryService;
|
||||
import org.fdroid.fdroid.installer.SessionInstallManager;
|
||||
import org.fdroid.fdroid.nearby.PublicSourceDirProvider;
|
||||
import org.fdroid.fdroid.nearby.SDCardScannerService;
|
||||
import org.fdroid.fdroid.nearby.WifiStateChangeService;
|
||||
|
@ -108,6 +109,7 @@ public class FDroidApp extends Application implements androidx.work.Configuratio
|
|||
public static volatile Repository repo;
|
||||
|
||||
public static volatile List<Repository> repos;
|
||||
public static volatile SessionInstallManager sessionInstallManager;
|
||||
|
||||
public static volatile int networkState = ConnectivityMonitorService.FLAG_NET_UNAVAILABLE;
|
||||
|
||||
|
@ -341,6 +343,7 @@ public class FDroidApp extends Application implements androidx.work.Configuratio
|
|||
|
||||
CleanCacheWorker.schedule(this);
|
||||
|
||||
sessionInstallManager = new SessionInstallManager(getApplicationContext());
|
||||
notificationHelper = new NotificationHelper(getApplicationContext());
|
||||
|
||||
if (preferences.isIndexNeverUpdated()) {
|
||||
|
|
|
@ -24,7 +24,6 @@ import androidx.core.app.NotificationManagerCompat;
|
|||
import androidx.core.content.ContextCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
|
@ -203,7 +202,6 @@ public class NotificationHelper {
|
|||
} else if (installed.size() == 1) {
|
||||
notification = createInstalledNotification(entry);
|
||||
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
|
||||
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
|
||||
notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification);
|
||||
}
|
||||
} else {
|
||||
|
@ -213,7 +211,6 @@ public class NotificationHelper {
|
|||
notificationManager.notify(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES, notification);
|
||||
} else if (updates.size() == 1) {
|
||||
notification = createUpdateNotification(entry);
|
||||
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
|
||||
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
|
||||
notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification);
|
||||
}
|
||||
|
@ -526,48 +523,34 @@ public class NotificationHelper {
|
|||
NotificationCompat.Builder notificationBuilder,
|
||||
int notificationId,
|
||||
String notificationTag) {
|
||||
final Point largeIconSize = getLargeIconSize();
|
||||
App.loadBitmapWithGlide(context, entry.app.repoId, entry.app.iconFile)
|
||||
.fallback(R.drawable.ic_notification_download)
|
||||
.error(R.drawable.ic_notification_download)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
// update the loaded large icon, but don't expand
|
||||
notificationBuilder.setLargeIcon(resource);
|
||||
Notification notification = notificationBuilder.build();
|
||||
notificationManager.notify(notificationTag, notificationId, notification);
|
||||
}
|
||||
|
||||
if (entry.status == AppUpdateStatusManager.Status.Downloading
|
||||
|| entry.status == AppUpdateStatusManager.Status.Installing) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(largeIconSize.x, largeIconSize.y, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
Drawable downloadIcon = ContextCompat.getDrawable(context, R.drawable.ic_notification_download);
|
||||
if (downloadIcon != null) {
|
||||
downloadIcon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
downloadIcon.draw(canvas);
|
||||
}
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(bitmap)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
// update the loaded large icon, but don't expand
|
||||
notificationBuilder.setLargeIcon(resource);
|
||||
Notification notification = notificationBuilder.build();
|
||||
notificationManager.notify(notificationTag, notificationId, notification);
|
||||
}
|
||||
@Override
|
||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||
if (errorDrawable == null) return;
|
||||
final Point largeIconSize = getLargeIconSize();
|
||||
Bitmap bitmap = Bitmap.createBitmap(largeIconSize.x, largeIconSize.y, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
errorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
errorDrawable.draw(canvas);
|
||||
notificationBuilder.setLargeIcon(bitmap);
|
||||
Notification notification = notificationBuilder.build();
|
||||
notificationManager.notify(notificationTag, notificationId, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable drawable) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
App.loadBitmapWithGlide(context, entry.app.repoId, entry.app.iconFile)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
// update the loaded large icon, but don't expand
|
||||
notificationBuilder.setLargeIcon(resource);
|
||||
Notification notification = notificationBuilder.build();
|
||||
notificationManager.notify(notificationTag, notificationId, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable drawable) {
|
||||
}
|
||||
});
|
||||
}
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable drawable) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -209,7 +209,9 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
|||
* @see org.fdroid.fdroid.views.PreferencesFragment#initPrivilegedInstallerPreference()
|
||||
*/
|
||||
public boolean isPrivilegedInstallerEnabled() {
|
||||
return preferences.getBoolean(PREF_PRIVILEGED_INSTALLER, true);
|
||||
// only use priv-ext by default with full flavor, because basic isn't allowed to use it
|
||||
// and there's a bug with auto-detection: https://gitlab.com/fdroid/fdroidclient/-/issues/2593
|
||||
return preferences.getBoolean(PREF_PRIVILEGED_INSTALLER, BuildConfig.FLAVOR.equals("full"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -432,8 +434,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
|||
}
|
||||
|
||||
public boolean isAutoDownloadEnabled() {
|
||||
return !"basic".equals(BuildConfig.FLAVOR) // TODO remove once basic can do auto-downloads
|
||||
&& preferences.getBoolean(PREF_AUTO_DOWNLOAD_INSTALL_UPDATES, IGNORED_B);
|
||||
return preferences.getBoolean(PREF_AUTO_DOWNLOAD_INSTALL_UPDATES, IGNORED_B);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,6 +28,7 @@ public class PackageManagerCompat {
|
|||
private static final String TAG = "PackageManagerCompat";
|
||||
|
||||
public static void setInstaller(Context context, PackageManager mPm, String packageName) {
|
||||
if (Build.VERSION.SDK_INT >= 30) return; // not working anymore on this SDK level
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= 24 && PrivilegedInstaller.isDefault(context)) {
|
||||
mPm.setInstallerPackageName(packageName, PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME);
|
||||
|
|
|
@ -24,6 +24,9 @@ import android.content.pm.ApplicationInfo;
|
|||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
|
@ -36,12 +39,27 @@ public class ApkCache {
|
|||
|
||||
private static final String CACHE_DIR = "apks";
|
||||
|
||||
public enum ApkCacheState { MISS_OR_PARTIAL, CACHED, CORRUPTED }
|
||||
|
||||
@WorkerThread
|
||||
static Pair<ApkCacheState, SanitizedFile> getApkCacheState(Context context, Apk apk) {
|
||||
SanitizedFile apkFilePath = getApkDownloadPath(context, apk.getCanonicalUrl());
|
||||
long apkFileSize = apkFilePath.length();
|
||||
if (!apkFilePath.exists() || apkFileSize < apk.size) {
|
||||
return new Pair<>(ApkCacheState.MISS_OR_PARTIAL, apkFilePath);
|
||||
} else if (apkIsCached(apkFilePath, apk)) {
|
||||
return new Pair<>(ApkCacheState.CACHED, apkFilePath);
|
||||
} else {
|
||||
return new Pair<>(ApkCacheState.CORRUPTED, apkFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #copyApkFromCacheToFiles(Context, File, Apk)}, except it does not need to
|
||||
* verify the hash after copying. This is because we are copying from an installed apk, which
|
||||
* other apps do not have permission to modify.
|
||||
*/
|
||||
public static SanitizedFile copyInstalledApkToFiles(Context context, PackageInfo packageInfo)
|
||||
static SanitizedFile copyInstalledApkToFiles(Context context, PackageInfo packageInfo)
|
||||
throws IOException {
|
||||
ApplicationInfo appInfo = packageInfo.applicationInfo;
|
||||
CharSequence name = context.getPackageManager().getApplicationLabel(appInfo);
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
@ -38,15 +37,18 @@ import java.io.PrintWriter;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.JobIntentService;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
/**
|
||||
* Saves all activity of installs and uninstalls to the database for later use, like
|
||||
* Saves all activity of installs and uninstalls to a file for later use, like
|
||||
* displaying in some kind of history viewer or reporting to a "popularity contest"
|
||||
* app tracker.
|
||||
*/
|
||||
public class InstallHistoryService extends IntentService {
|
||||
public class InstallHistoryService extends JobIntentService {
|
||||
public static final String TAG = "InstallHistoryService";
|
||||
private static final int JOB_ID = TAG.hashCode();
|
||||
|
||||
public static final Uri LOG_URI = Uri.parse("content://" + Installer.AUTHORITY + "/install_history/all");
|
||||
|
||||
|
@ -85,10 +87,10 @@ public class InstallHistoryService extends IntentService {
|
|||
broadcastReceiver = null;
|
||||
}
|
||||
|
||||
public static void queue(Context context, Intent intent) {
|
||||
private static void queue(Context context, Intent intent) {
|
||||
Utils.debugLog(TAG, "queue " + intent);
|
||||
intent.setClass(context, InstallHistoryService.class);
|
||||
context.startService(intent);
|
||||
JobIntentService.enqueueWork(context, InstallHistoryService.class, JOB_ID, intent);
|
||||
}
|
||||
|
||||
public static File getInstallHistoryFile(Context context) {
|
||||
|
@ -97,16 +99,9 @@ public class InstallHistoryService extends IntentService {
|
|||
return new File(installHistoryDir, "all");
|
||||
}
|
||||
|
||||
public InstallHistoryService() {
|
||||
super("InstallHistoryService");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
protected void onHandleWork(@NonNull Intent intent) {
|
||||
Utils.debugLog(TAG, "onHandleIntent " + intent);
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -20,6 +20,7 @@ import org.fdroid.fdroid.Utils;
|
|||
import org.fdroid.fdroid.compat.PackageManagerCompat;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -35,7 +36,7 @@ import static vendored.org.apache.commons.codec.digest.MessageDigestAlgorithms.S
|
|||
* Manages the whole process when a background update triggers an install or the user
|
||||
* requests an APK to be installed. It handles checking whether the APK is cached,
|
||||
* downloading it, putting up and maintaining a {@link Notification}, and more. This
|
||||
* {@code Service} tracks packages that are in the process as "Pending Installs".
|
||||
* class tracks packages that are in the process as "Pending Installs".
|
||||
* Then {@link DownloaderService} and {@link InstallerService} individually track
|
||||
* packages for those phases of the whole install process. Each of those
|
||||
* {@code Services} have their own related events. For tracking status during the
|
||||
|
@ -45,15 +46,9 @@ import static vendored.org.apache.commons.codec.digest.MessageDigestAlgorithms.S
|
|||
* The {@link App} and {@link Apk} instances are sent via
|
||||
* {@link Intent#putExtra(String, android.os.Bundle)}
|
||||
* so that Android handles the message queuing and {@link Service} lifecycle for us.
|
||||
* For example, if this {@code InstallManagerService} gets killed, Android will cache
|
||||
* For example, if {@link DownloaderService} gets killed, Android will cache
|
||||
* and then redeliver the {@link Intent} for us, which includes all of the data needed
|
||||
* for {@code InstallManagerService} to do its job for the whole lifecycle of an install.
|
||||
* This {@code Service} never stops itself after completing the action, e.g.
|
||||
* {@code {@link #stopSelf(int)}}, so {@code Intent}s are sometimes redelivered even
|
||||
* though they are no longer valid. {@link #onStartCommand(Intent, int, int)} checks
|
||||
* first that the incoming {@code Intent} is not an invalid, redelivered {@code Intent}.
|
||||
* {@link #isPendingInstall(String)} and other checks are used to check whether to
|
||||
* process the redelivered {@code Intent} or not.
|
||||
* <p>
|
||||
* The canonical URL for the APK file to download is also used as the unique ID to
|
||||
* represent the download itself throughout F-Droid. This follows the model
|
||||
|
@ -87,57 +82,55 @@ import static vendored.org.apache.commons.codec.digest.MessageDigestAlgorithms.S
|
|||
* @see <a href="https://gitlab.com/fdroid/fdroidclient/-/merge_requests/1089#note_822501322">forced to vendor Apache Commons Codec</a>
|
||||
*/
|
||||
@SuppressWarnings("LineLength")
|
||||
public class InstallManagerService extends Service {
|
||||
public class InstallManagerService {
|
||||
private static final String TAG = "InstallManagerService";
|
||||
|
||||
private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.action.INSTALL";
|
||||
private static final String ACTION_CANCEL = "org.fdroid.fdroid.installer.action.CANCEL";
|
||||
|
||||
private static final String EXTRA_APP = "org.fdroid.fdroid.installer.extra.APP";
|
||||
private static final String EXTRA_APK = "org.fdroid.fdroid.installer.extra.APK";
|
||||
@SuppressLint("StaticFieldLeak") // we are using ApplicationContext, so hopefully that's fine
|
||||
private static InstallManagerService instance;
|
||||
|
||||
private static SharedPreferences pendingInstalls;
|
||||
private final Context context;
|
||||
private final LocalBroadcastManager localBroadcastManager;
|
||||
private final AppUpdateStatusManager appUpdateStatusManager;
|
||||
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
private AppUpdateStatusManager appUpdateStatusManager;
|
||||
private boolean running = false;
|
||||
|
||||
/**
|
||||
* This service does not use binding, so no need to implement this method
|
||||
*/
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
public static InstallManagerService getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
instance = new InstallManagerService(context.getApplicationContext());
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
appUpdateStatusManager = AppUpdateStatusManager.getInstance(this);
|
||||
running = true;
|
||||
pendingInstalls = getPendingInstalls(this);
|
||||
public InstallManagerService(Context context) {
|
||||
this.context = context;
|
||||
this.localBroadcastManager = LocalBroadcastManager.getInstance(context);
|
||||
this.appUpdateStatusManager = AppUpdateStatusManager.getInstance(context);
|
||||
// cancel intent can't use LocalBroadcastManager, because it comes from system process
|
||||
IntentFilter cancelFilter = new IntentFilter();
|
||||
cancelFilter.addAction(ACTION_CANCEL);
|
||||
context.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "Received cancel intent: " + intent);
|
||||
if (!ACTION_CANCEL.equals(intent.getAction())) return;
|
||||
cancel(context, intent.getStringExtra(DownloaderService.EXTRA_CANONICAL_URL));
|
||||
}
|
||||
}, cancelFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this {@link Service} is stopped, then all of the various
|
||||
* {@link BroadcastReceiver}s need to unregister themselves if they get
|
||||
* called. There can be multiple {@code BroadcastReceiver}s registered,
|
||||
* so it can't be done with a simple call here. So {@link #running} is the
|
||||
* signal to all the existing {@code BroadcastReceiver}s to unregister.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
running = false;
|
||||
super.onDestroy();
|
||||
private void onCancel(String canonicalUrl) {
|
||||
DownloaderService.cancel(canonicalUrl);
|
||||
Apk apk = appUpdateStatusManager.getApk(canonicalUrl);
|
||||
if (apk != null) {
|
||||
Utils.debugLog(TAG, "also canceling OBB downloads");
|
||||
DownloaderService.cancel(apk.getPatchObbUrl());
|
||||
DownloaderService.cancel(apk.getMainObbUrl());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This goes through a series of checks to make sure that the incoming
|
||||
* {@link Intent} is still valid. The default {@link Intent#getAction() action}
|
||||
* in the logic is {@link #ACTION_INSTALL} since it is the most complicate
|
||||
* case. Since the {@code Intent} will be redelivered by Android if the
|
||||
* app was killed, this needs to check that it still makes sense to handle.
|
||||
* {@link Intent} is still valid.
|
||||
* <p>
|
||||
* For example, if F-Droid is killed while installing, it might not receive
|
||||
* the message that the install completed successfully. The checks need to be
|
||||
|
@ -145,93 +138,93 @@ public class InstallManagerService extends Service {
|
|||
* with the same {@link PackageInfo#versionCode}, which happens sometimes,
|
||||
* and is allowed by Android.
|
||||
*/
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Utils.debugLog(TAG, "onStartCommand " + intent);
|
||||
private void queue(String canonicalUrl, String packageName, @NonNull App app, @NonNull Apk apk) {
|
||||
Utils.debugLog(TAG, "queue " + packageName);
|
||||
|
||||
String canonicalUrl = intent.getDataString();
|
||||
if (TextUtils.isEmpty(canonicalUrl)) {
|
||||
Utils.debugLog(TAG, "empty canonicalUrl, nothing to do");
|
||||
return START_NOT_STICKY;
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
|
||||
if (ACTION_CANCEL.equals(action)) {
|
||||
DownloaderService.cancel(this, canonicalUrl);
|
||||
Apk apk = appUpdateStatusManager.getApk(canonicalUrl);
|
||||
if (apk != null) {
|
||||
Utils.debugLog(TAG, "also canceling OBB downloads");
|
||||
DownloaderService.cancel(this, apk.getPatchObbUrl());
|
||||
DownloaderService.cancel(this, apk.getMainObbUrl());
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
} else if (ACTION_INSTALL.equals(action)) {
|
||||
if (!isPendingInstall(canonicalUrl)) {
|
||||
Log.i(TAG, "Ignoring INSTALL that is not Pending Install: " + intent);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Ignoring unknown intent action: " + intent);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
if (!intent.hasExtra(EXTRA_APP) || !intent.hasExtra(EXTRA_APK)) {
|
||||
Utils.debugLog(TAG, canonicalUrl + " did not include both an App and Apk instance, ignoring");
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
if ((flags & START_FLAG_REDELIVERY) == START_FLAG_REDELIVERY
|
||||
&& !DownloaderService.isQueuedOrActive(canonicalUrl)) {
|
||||
Utils.debugLog(TAG, canonicalUrl + " finished downloading while InstallManagerService was killed.");
|
||||
appUpdateStatusManager.removeApk(canonicalUrl);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
App app = intent.getParcelableExtra(EXTRA_APP);
|
||||
Apk apk = intent.getParcelableExtra(EXTRA_APK);
|
||||
if (app == null || apk == null) {
|
||||
Utils.debugLog(TAG, "Intent had null EXTRA_APP and/or EXTRA_APK: " + intent);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
PackageInfo packageInfo = Utils.getPackageInfo(this, apk.packageName);
|
||||
if ((flags & START_FLAG_REDELIVERY) == START_FLAG_REDELIVERY
|
||||
&& packageInfo != null && packageInfo.versionCode == apk.versionCode
|
||||
PackageInfo packageInfo = Utils.getPackageInfo(context, packageName);
|
||||
if (packageInfo != null && packageInfo.versionCode == apk.versionCode
|
||||
&& TextUtils.equals(packageInfo.versionName, apk.versionName)) {
|
||||
Log.i(TAG, "INSTALL Intent no longer valid since its installed, ignoring: " + intent);
|
||||
return START_NOT_STICKY;
|
||||
Log.i(TAG, "Install action no longer valid since its installed, ignoring: " + packageName);
|
||||
return;
|
||||
}
|
||||
|
||||
appUpdateStatusManager.addApk(app, apk, AppUpdateStatusManager.Status.Downloading, null);
|
||||
|
||||
registerPackageDownloaderReceivers(canonicalUrl);
|
||||
getMainObb(canonicalUrl, apk);
|
||||
getPatchObb(canonicalUrl, apk);
|
||||
|
||||
File apkFilePath = ApkCache.getApkDownloadPath(this, apk.getCanonicalUrl());
|
||||
long apkFileSize = apkFilePath.length();
|
||||
if (!apkFilePath.exists() || apkFileSize < apk.size) {
|
||||
Utils.debugLog(TAG, "download " + canonicalUrl + " " + apkFilePath);
|
||||
DownloaderService.queue(this, apk.repoId, canonicalUrl, apk.getDownloadUrl(), apk.apkFile);
|
||||
} else if (ApkCache.apkIsCached(apkFilePath, apk)) {
|
||||
Utils.debugLog(TAG, "skip download, we have it, straight to install " + canonicalUrl + " " + apkFilePath);
|
||||
sendBroadcast(intent.getData(), DownloaderService.ACTION_STARTED, apkFilePath);
|
||||
sendBroadcast(intent.getData(), DownloaderService.ACTION_COMPLETE, apkFilePath);
|
||||
} else {
|
||||
Utils.debugLog(TAG, "delete and download again " + canonicalUrl + " " + apkFilePath);
|
||||
apkFilePath.delete();
|
||||
DownloaderService.queue(this, apk.repoId, canonicalUrl, apk.getDownloadUrl(), apk.apkFile);
|
||||
}
|
||||
|
||||
return START_REDELIVER_INTENT; // if killed before completion, retry Intent
|
||||
Utils.runOffUiThread(() -> ApkCache.getApkCacheState(context, apk), pair -> {
|
||||
ApkCache.ApkCacheState state = pair.first;
|
||||
SanitizedFile apkFilePath = pair.second;
|
||||
if (state == ApkCache.ApkCacheState.MISS_OR_PARTIAL) {
|
||||
Utils.debugLog(TAG, "download " + canonicalUrl + " " + apkFilePath);
|
||||
DownloaderService.queue(context, canonicalUrl, app, apk);
|
||||
} else if (state == ApkCache.ApkCacheState.CACHED) {
|
||||
Utils.debugLog(TAG, "skip download, we have it, straight to install " + canonicalUrl + " " + apkFilePath);
|
||||
Uri canonicalUri = Uri.parse(canonicalUrl);
|
||||
onDownloadStarted(canonicalUri);
|
||||
onDownloadComplete(canonicalUri, apkFilePath, app, apk);
|
||||
} else {
|
||||
Utils.debugLog(TAG, "delete and download again " + canonicalUrl + " " + apkFilePath);
|
||||
Utils.runOffUiThread(apkFilePath::delete);
|
||||
DownloaderService.queue(context, canonicalUrl, app, apk);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendBroadcast(Uri uri, String action, File file) {
|
||||
Intent intent = new Intent(action);
|
||||
intent.setData(uri);
|
||||
intent.putExtra(DownloaderService.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath());
|
||||
localBroadcastManager.sendBroadcast(intent);
|
||||
public void onDownloadStarted(Uri canonicalUri) {
|
||||
// App should currently be in the "PendingDownload" state, so this changes it to "Downloading".
|
||||
appUpdateStatusManager.updateApk(canonicalUri.toString(),
|
||||
AppUpdateStatusManager.Status.Downloading, getDownloadCancelIntent(canonicalUri));
|
||||
}
|
||||
|
||||
public void onDownloadProgress(Uri canonicalUri, App app, Apk apk, long bytesRead, long totalBytes) {
|
||||
if (appUpdateStatusManager.get(canonicalUri.toString()) == null) {
|
||||
// if our app got killed, we need to re-add the APK here
|
||||
appUpdateStatusManager.addApk(app, apk, AppUpdateStatusManager.Status.Downloading,
|
||||
getDownloadCancelIntent(canonicalUri));
|
||||
}
|
||||
appUpdateStatusManager.updateApkProgress(canonicalUri.toString(), totalBytes, bytesRead);
|
||||
}
|
||||
|
||||
public void onDownloadComplete(Uri canonicalUri, File file, App intentApp, Apk intentApk) {
|
||||
String canonicalUrl = canonicalUri.toString();
|
||||
Uri localApkUri = Uri.fromFile(file);
|
||||
|
||||
Utils.debugLog(TAG, "download completed of " + canonicalUri + " to " + localApkUri);
|
||||
appUpdateStatusManager.updateApk(canonicalUrl,
|
||||
AppUpdateStatusManager.Status.ReadyToInstall, null);
|
||||
|
||||
App app = appUpdateStatusManager.getApp(canonicalUrl);
|
||||
Apk apk = appUpdateStatusManager.getApk(canonicalUrl);
|
||||
if (app == null || apk == null) {
|
||||
// These may be null if our app was killed and the download job restarted.
|
||||
// Then, we can take the objects we saved in the intent which survive app death.
|
||||
app = intentApp;
|
||||
apk = intentApk;
|
||||
}
|
||||
if (app != null && apk != null) {
|
||||
registerInstallReceiver(canonicalUrl);
|
||||
InstallerService.install(context, localApkUri, canonicalUri, app, apk);
|
||||
} else {
|
||||
Log.e(TAG, "Could not install " + canonicalUrl + " because no app or apk available.");
|
||||
}
|
||||
}
|
||||
|
||||
public void onDownloadFailed(Uri canonicalUri, String errorMsg) {
|
||||
appUpdateStatusManager.setDownloadError(canonicalUri.toString(), errorMsg);
|
||||
}
|
||||
|
||||
private PendingIntent getDownloadCancelIntent(Uri canonicalUri) {
|
||||
Intent intentObject = new Intent(ACTION_CANCEL);
|
||||
intentObject.putExtra(DownloaderService.EXTRA_CANONICAL_URL, canonicalUri.toString());
|
||||
return PendingIntent.getBroadcast(context, 0, intentObject,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
private void getMainObb(final String canonicalUrl, Apk apk) {
|
||||
|
@ -257,10 +250,6 @@ public class InstallManagerService extends Service {
|
|||
final BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (!running) {
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
return;
|
||||
}
|
||||
String action = intent.getAction();
|
||||
if (DownloaderService.ACTION_STARTED.equals(action)) {
|
||||
Utils.debugLog(TAG, action + " " + intent);
|
||||
|
@ -306,90 +295,19 @@ public class InstallManagerService extends Service {
|
|||
}
|
||||
}
|
||||
};
|
||||
DownloaderService.queue(this, repoId, obbUrlString, obbUrlString, null);
|
||||
DownloaderService.queue(context, repoId, obbUrlString, obbUrlString);
|
||||
localBroadcastManager.registerReceiver(downloadReceiver,
|
||||
DownloaderService.getIntentFilter(obbUrlString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link BroadcastReceiver} for tracking download progress for a
|
||||
* give {@code canonicalUrl}. There can be multiple of these registered at a time.
|
||||
*/
|
||||
private void registerPackageDownloaderReceivers(String canonicalUrl) {
|
||||
|
||||
BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (!running) {
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
return;
|
||||
}
|
||||
Uri canonicalUri = intent.getData();
|
||||
String canonicalUrl = intent.getDataString();
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case DownloaderService.ACTION_STARTED:
|
||||
// App should currently be in the "PendingDownload" state, so this changes it to "Downloading".
|
||||
Intent intentObject = new Intent(context, InstallManagerService.class);
|
||||
intentObject.setAction(ACTION_CANCEL);
|
||||
intentObject.setData(canonicalUri);
|
||||
PendingIntent action =
|
||||
PendingIntent.getService(context, 0, intentObject, PendingIntent.FLAG_IMMUTABLE);
|
||||
appUpdateStatusManager.updateApk(canonicalUrl,
|
||||
AppUpdateStatusManager.Status.Downloading, action);
|
||||
break;
|
||||
case DownloaderService.ACTION_PROGRESS:
|
||||
long bytesRead = intent.getLongExtra(DownloaderService.EXTRA_BYTES_READ, 0);
|
||||
long totalBytes = intent.getLongExtra(DownloaderService.EXTRA_TOTAL_BYTES, 0);
|
||||
appUpdateStatusManager.updateApkProgress(canonicalUrl, totalBytes, bytesRead);
|
||||
break;
|
||||
case DownloaderService.ACTION_COMPLETE:
|
||||
File localFile = new File(intent.getStringExtra(DownloaderService.EXTRA_DOWNLOAD_PATH));
|
||||
Uri localApkUri = Uri.fromFile(localFile);
|
||||
|
||||
Utils.debugLog(TAG, "download completed of "
|
||||
+ intent.getStringExtra(DownloaderService.EXTRA_MIRROR_URL) + " to " + localApkUri);
|
||||
appUpdateStatusManager.updateApk(canonicalUrl,
|
||||
AppUpdateStatusManager.Status.ReadyToInstall, null);
|
||||
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
registerInstallReceiver(canonicalUrl);
|
||||
|
||||
App app = appUpdateStatusManager.getApp(canonicalUrl);
|
||||
Apk apk = appUpdateStatusManager.getApk(canonicalUrl);
|
||||
if (apk != null) {
|
||||
InstallerService.install(context, localApkUri, canonicalUri, app, apk);
|
||||
}
|
||||
break;
|
||||
case DownloaderService.ACTION_INTERRUPTED:
|
||||
case DownloaderService.ACTION_CONNECTION_FAILED:
|
||||
appUpdateStatusManager.setDownloadError(canonicalUrl,
|
||||
intent.getStringExtra(DownloaderService.EXTRA_ERROR_MESSAGE));
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("intent action not handled!");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
localBroadcastManager.registerReceiver(downloadReceiver,
|
||||
DownloaderService.getIntentFilter(canonicalUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link BroadcastReceiver} for tracking install progress for a
|
||||
* give {@link Uri}. There can be multiple of these registered at a time.
|
||||
*/
|
||||
private void registerInstallReceiver(String canonicalUrl) {
|
||||
|
||||
BroadcastReceiver installReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (!running) {
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
return;
|
||||
}
|
||||
String canonicalUrl = intent.getDataString();
|
||||
App app;
|
||||
Apk apk;
|
||||
|
@ -405,7 +323,7 @@ public class InstallManagerService extends Service {
|
|||
|
||||
if (apkComplete != null && apkComplete.isApk()) {
|
||||
try {
|
||||
PackageManagerCompat.setInstaller(context, getPackageManager(), apkComplete.packageName);
|
||||
PackageManagerCompat.setInstaller(context, context.getPackageManager(), apkComplete.packageName);
|
||||
} catch (SecurityException e) {
|
||||
// Will happen if we fell back to DefaultInstaller for some reason.
|
||||
}
|
||||
|
@ -437,7 +355,7 @@ public class InstallManagerService extends Service {
|
|||
};
|
||||
|
||||
localBroadcastManager.registerReceiver(installReceiver,
|
||||
Installer.getInstallIntentFilter(canonicalUrl));
|
||||
Installer.getInstallIntentFilter(Uri.parse(canonicalUrl)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -455,57 +373,11 @@ public class InstallManagerService extends Service {
|
|||
public static void queue(Context context, @NonNull App app, @NonNull Apk apk) {
|
||||
String canonicalUrl = apk.getCanonicalUrl();
|
||||
AppUpdateStatusManager.getInstance(context).addApk(app, apk, AppUpdateStatusManager.Status.PendingInstall, null);
|
||||
putPendingInstall(context, canonicalUrl, apk.packageName);
|
||||
Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + canonicalUrl);
|
||||
Intent intent = new Intent(context, InstallManagerService.class);
|
||||
intent.setAction(ACTION_INSTALL);
|
||||
intent.setData(Uri.parse(canonicalUrl));
|
||||
intent.putExtra(EXTRA_APP, app);
|
||||
intent.putExtra(EXTRA_APK, apk);
|
||||
context.startService(intent);
|
||||
InstallManagerService.getInstance(context).queue(canonicalUrl, apk.packageName, app, apk);
|
||||
}
|
||||
|
||||
public static void cancel(Context context, String canonicalUrl) {
|
||||
removePendingInstall(context, canonicalUrl);
|
||||
Intent intent = new Intent(context, InstallManagerService.class);
|
||||
intent.setAction(ACTION_CANCEL);
|
||||
intent.setData(Uri.parse(canonicalUrl));
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the APK that matches the provided {@code hash} still waiting to be
|
||||
* installed? This restarts the install process for this APK if it was
|
||||
* interrupted somehow, like if F-Droid was killed before the download
|
||||
* completed, or the device lost power in the middle of the install
|
||||
* process.
|
||||
*/
|
||||
public boolean isPendingInstall(String canonicalUrl) {
|
||||
return pendingInstalls.contains(canonicalUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a given APK as in the process of being installed, with
|
||||
* the {@code canonicalUrl} of the download used as the unique ID,
|
||||
* and the file hash used to verify that things are the same.
|
||||
*
|
||||
* @see #isPendingInstall(String)
|
||||
*/
|
||||
public static void putPendingInstall(Context context, String canonicalUrl, String packageName) {
|
||||
if (pendingInstalls == null) {
|
||||
pendingInstalls = getPendingInstalls(context);
|
||||
}
|
||||
pendingInstalls.edit().putString(canonicalUrl, packageName).apply();
|
||||
}
|
||||
|
||||
public static void removePendingInstall(Context context, String canonicalUrl) {
|
||||
if (pendingInstalls == null) {
|
||||
pendingInstalls = getPendingInstalls(context);
|
||||
}
|
||||
pendingInstalls.edit().remove(canonicalUrl).apply();
|
||||
}
|
||||
|
||||
private static SharedPreferences getPendingInstalls(Context context) {
|
||||
return context.getSharedPreferences("pending-installs", Context.MODE_PRIVATE);
|
||||
InstallManagerService.getInstance(context).onCancel(canonicalUrl);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -210,7 +210,7 @@ public abstract class Installer {
|
|||
sendBroadcastUninstall(context, app, apk, action, null, null);
|
||||
}
|
||||
|
||||
private static void sendBroadcastUninstall(Context context, App app, Apk apk, String action,
|
||||
static void sendBroadcastUninstall(Context context, App app, Apk apk, String action,
|
||||
PendingIntent pendingIntent, String errorMessage) {
|
||||
Uri uri = Uri.fromParts("package", apk.packageName, null);
|
||||
|
||||
|
@ -233,10 +233,22 @@ public abstract class Installer {
|
|||
* @see InstallManagerService for more about {@code canonicalUri}
|
||||
*/
|
||||
public static IntentFilter getInstallIntentFilter(Uri canonicalUri) {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
IntentFilter intentFilter = getInstallInteractionIntentFilter(canonicalUri);
|
||||
intentFilter.addAction(Installer.ACTION_INSTALL_STARTED);
|
||||
intentFilter.addAction(Installer.ACTION_INSTALL_COMPLETE);
|
||||
intentFilter.addAction(Installer.ACTION_INSTALL_INTERRUPTED);
|
||||
return intentFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an {@link IntentFilter} for user interaction needed events from the install
|
||||
* process based on {@code canonicalUri}, which is the global unique
|
||||
* ID for a package going through the install process.
|
||||
*
|
||||
* @see InstallManagerService for more about {@code canonicalUri}
|
||||
*/
|
||||
public static IntentFilter getInstallInteractionIntentFilter(Uri canonicalUri) {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(Installer.ACTION_INSTALL_USER_INTERACTION);
|
||||
intentFilter.addDataScheme(canonicalUri.getScheme());
|
||||
intentFilter.addDataAuthority(canonicalUri.getHost(), String.valueOf(canonicalUri.getPort()));
|
||||
|
|
|
@ -23,12 +23,12 @@ package org.fdroid.fdroid.installer;
|
|||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
|
||||
public class InstallerFactory {
|
||||
|
||||
private static final String TAG = "InstallerFactory";
|
||||
|
@ -55,6 +55,10 @@ public class InstallerFactory {
|
|||
} else if (PrivilegedInstaller.isDefault(context)) {
|
||||
Utils.debugLog(TAG, "privileged extension correctly installed -> PrivilegedInstaller");
|
||||
installer = new PrivilegedInstaller(context, app, apk);
|
||||
} else if (SessionInstallManager.isTargetSdkSupported(apk.targetSdkVersion)
|
||||
&& SessionInstallManager.canBeUsed()) {
|
||||
Utils.debugLog(TAG, "using experimental SessionInstaller, because app targets " + apk.targetSdkVersion);
|
||||
installer = new SessionInstaller(context, app, apk);
|
||||
} else {
|
||||
installer = new DefaultInstaller(context, app, apk);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.IntentSender;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class SessionInstallManager extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = "SessionInstallManager";
|
||||
private static final String INSTALLER_ACTION_INSTALL =
|
||||
"org.fdroid.fdroid.installer.SessionInstallManager.install";
|
||||
private static final String INSTALLER_ACTION_UNINSTALL =
|
||||
"org.fdroid.fdroid.installer.SessionInstallManager.uninstall";
|
||||
/**
|
||||
* An intent extra needed only due to a bug in Android 12 (#2599) where our App parcelable in the confirmation
|
||||
* intent causes a crash.
|
||||
* To prevent this, we wrap the App and Apk parcelables in this bundle.
|
||||
*/
|
||||
private static final String EXTRA_BUNDLE =
|
||||
"org.fdroid.fdroid.installer.SessionInstallManager.bundle";
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SessionInstallManager(Context context) {
|
||||
this.context = context;
|
||||
context.registerReceiver(this, new IntentFilter(INSTALLER_ACTION_INSTALL));
|
||||
context.registerReceiver(this, new IntentFilter(INSTALLER_ACTION_UNINSTALL));
|
||||
PackageInstaller installer = context.getPackageManager().getPackageInstaller();
|
||||
// abandon old sessions, because there's a limit
|
||||
// that will throw IllegalStateException when we try to open new sessions
|
||||
Utils.runOffUiThread(() -> {
|
||||
for (PackageInstaller.SessionInfo session : installer.getMySessions()) {
|
||||
Utils.debugLog(TAG, "Abandon session " + session.getSessionId());
|
||||
installer.abandonSession(session.getSessionId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public void install(App app, Apk apk, Uri localApkUri, Uri canonicalUri) {
|
||||
DocumentFile documentFile = ObjectsCompat.requireNonNull(DocumentFile.fromSingleUri(context, localApkUri));
|
||||
long size = documentFile.length();
|
||||
PackageInstaller.SessionParams params =
|
||||
new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
|
||||
params.setAppPackageName(app.packageName);
|
||||
params.setSize(size);
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
params.setPackageSource(PackageInstaller.PACKAGE_SOURCE_STORE);
|
||||
}
|
||||
PackageInstaller installer = context.getPackageManager().getPackageInstaller();
|
||||
try {
|
||||
int sessionId = installer.createSession(params);
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
try (PackageInstaller.Session session = installer.openSession(sessionId)) {
|
||||
try (InputStream inputStream = contentResolver.openInputStream(localApkUri)) {
|
||||
try (OutputStream outputStream = session.openWrite(app.packageName, 0, size)) {
|
||||
IOUtils.copy(inputStream, outputStream);
|
||||
session.fsync(outputStream);
|
||||
}
|
||||
}
|
||||
session.commit(getInstallIntentSender(sessionId, app, apk, canonicalUri));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "I/O Error during install session: ", e);
|
||||
Installer.sendBroadcastInstall(context, canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED, app, apk,
|
||||
null, e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public void uninstall(String packageName) {
|
||||
PackageInstaller installer = context.getPackageManager().getPackageInstaller();
|
||||
installer.uninstall(packageName, getUninstallIntentSender(packageName));
|
||||
}
|
||||
|
||||
private IntentSender getInstallIntentSender(int sessionId, App app, Apk apk, Uri canonicalUri) {
|
||||
Intent broadcastIntent = new Intent(INSTALLER_ACTION_INSTALL);
|
||||
broadcastIntent.setPackage(context.getPackageName());
|
||||
broadcastIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(Installer.EXTRA_APP, app);
|
||||
bundle.putParcelable(Installer.EXTRA_APK, apk);
|
||||
broadcastIntent.putExtra(EXTRA_BUNDLE, bundle);
|
||||
broadcastIntent.putExtra(DownloaderService.EXTRA_CANONICAL_URL, canonicalUri);
|
||||
// we are stuffing this intent pretty full, hopefully won't run into the size limit
|
||||
broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
|
||||
// intent flag needs to be mutable, otherwise the intent has no extras
|
||||
int flags = Build.VERSION.SDK_INT >= 31 ?
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE :
|
||||
PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, sessionId, broadcastIntent, flags);
|
||||
return pendingIntent.getIntentSender();
|
||||
}
|
||||
|
||||
private IntentSender getUninstallIntentSender(String packageName) {
|
||||
Intent broadcastIntent = new Intent(INSTALLER_ACTION_UNINSTALL);
|
||||
broadcastIntent.setPackage(context.getPackageName());
|
||||
broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
|
||||
broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
|
||||
// intent flag needs to be mutable, otherwise the intent has no extras
|
||||
int flags = Build.VERSION.SDK_INT >= 31 ?
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE :
|
||||
PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
PendingIntent pendingIntent =
|
||||
PendingIntent.getBroadcast(context, packageName.hashCode(), broadcastIntent, flags);
|
||||
return pendingIntent.getIntentSender();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (INSTALLER_ACTION_INSTALL.equals(intent.getAction())) {
|
||||
onInstallReceived(intent);
|
||||
} else if (INSTALLER_ACTION_UNINSTALL.equals(intent.getAction())) {
|
||||
onUninstallReceived(intent);
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported broadcast action: " + intent.getAction());
|
||||
}
|
||||
}
|
||||
|
||||
private void onInstallReceived(Intent intent) {
|
||||
int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
|
||||
Intent confirmIntent = (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
|
||||
Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE);
|
||||
App app = bundle.getParcelable(Installer.EXTRA_APP);
|
||||
Apk apk = bundle.getParcelable(Installer.EXTRA_APK);
|
||||
Uri canonicalUri = intent.getParcelableExtra(DownloaderService.EXTRA_CANONICAL_URL);
|
||||
|
||||
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE);
|
||||
String msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
|
||||
Log.i(TAG, "Received install broadcast for " + app.packageName + " " + status + ": " + msg);
|
||||
|
||||
if (status == PackageInstaller.STATUS_SUCCESS) {
|
||||
String action = Installer.ACTION_INSTALL_COMPLETE;
|
||||
Installer.sendBroadcastInstall(context, canonicalUri, action, app, apk, null, null);
|
||||
} else if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
|
||||
int flags = Build.VERSION.SDK_INT >= 31 ?
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
||||
PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, sessionId, confirmIntent, flags);
|
||||
String action = Installer.ACTION_INSTALL_USER_INTERACTION;
|
||||
Installer.sendBroadcastInstall(context, canonicalUri, action, app, apk, pendingIntent, null);
|
||||
} else {
|
||||
String action = Installer.ACTION_INSTALL_INTERRUPTED;
|
||||
Installer.sendBroadcastInstall(context, canonicalUri, action, app, apk, null, msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void onUninstallReceived(Intent intent) {
|
||||
String packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME);
|
||||
Intent confirmIntent = (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE);
|
||||
String msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
|
||||
Log.i(TAG, "Received uninstall broadcast for " + packageName + " " + status + ": " + msg);
|
||||
|
||||
if (status == PackageInstaller.STATUS_SUCCESS) {
|
||||
String action = Installer.ACTION_UNINSTALL_COMPLETE;
|
||||
sendBroadcastUninstall(packageName, action, null, null);
|
||||
} else if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
|
||||
int flags = Build.VERSION.SDK_INT >= 31 ?
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
||||
PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
PendingIntent pendingIntent =
|
||||
PendingIntent.getActivity(context, packageName.hashCode(), confirmIntent, flags);
|
||||
String action = Installer.ACTION_UNINSTALL_USER_INTERACTION;
|
||||
sendBroadcastUninstall(packageName, action, pendingIntent, null);
|
||||
} else {
|
||||
String action = Installer.ACTION_UNINSTALL_INTERRUPTED;
|
||||
sendBroadcastUninstall(packageName, action, null, msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBroadcastUninstall(String packageName, String action, @Nullable PendingIntent pendingIntent,
|
||||
@Nullable String errorMessage) {
|
||||
App app = new App();
|
||||
app.packageName = packageName;
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = packageName;
|
||||
Installer.sendBroadcastUninstall(context, app, apk, action, pendingIntent, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the {@link SessionInstaller} can be used on this device.
|
||||
*/
|
||||
public static boolean canBeUsed() {
|
||||
// We could use the SessionInstaller also with the full flavor,
|
||||
// but for now we limit it to basic to limit potential damage.
|
||||
if (!BuildConfig.FLAVOR.equals("basic")) return false;
|
||||
// We could use the SessionInstaller also on lower versions,
|
||||
// but the benefit of unattended updates only starts with SDK 31.
|
||||
// Before the extra bugs it has aren't worth it.
|
||||
if (Build.VERSION.SDK_INT < 31) return false;
|
||||
// Xiaomi MIUI (at least in version 12) is known to break the PackageInstaller API in several ways.
|
||||
// Disabling MIUI "optimizations" in developer options fixes it, but we can't ask users to do this (bad UX).
|
||||
// Therefore, we have no choice, but to disable it completely for those devices.
|
||||
// See: https://github.com/vvb2060/PackageInstallerTest
|
||||
return !isXiaomiDevice();
|
||||
}
|
||||
|
||||
private static boolean isXiaomiDevice() {
|
||||
return "Xiaomi".equalsIgnoreCase(Build.BRAND) || "Redmi".equalsIgnoreCase(Build.BRAND);
|
||||
}
|
||||
|
||||
public static boolean isTargetSdkSupported(int targetSdk) {
|
||||
if (Build.VERSION.SDK_INT < 31) return false; // not supported below Android 12
|
||||
if (Build.VERSION.SDK_INT == 31 && targetSdk >= 29) return true;
|
||||
if (Build.VERSION.SDK_INT == 32 && targetSdk >= 29) return true;
|
||||
if (Build.VERSION.SDK_INT == 33 && targetSdk >= 30) return true;
|
||||
// This needs to be adjusted as new Android versions are released
|
||||
// https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int)
|
||||
// https://cs.android.com/android/platform/superproject/+/android-13.0.0_r42:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java;l=2095;drc=6aba151873bfae198ef9eceb10f943e18b52d58c
|
||||
// TODO check targetSdk Android 14 sources have been published, just a guess so far
|
||||
return Build.VERSION.SDK_INT == 34 && targetSdk >= 31;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
|
||||
class SessionInstaller extends Installer {
|
||||
|
||||
private final SessionInstallManager sessionInstallManager = FDroidApp.sessionInstallManager;
|
||||
|
||||
SessionInstaller(Context context, @NonNull App app, @NonNull Apk apk) {
|
||||
super(context, app, apk);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void installPackageInternal(Uri localApkUri, Uri canonicalUri) {
|
||||
sessionInstallManager.install(app, apk, localApkUri, canonicalUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void uninstallPackage() {
|
||||
sessionInstallManager.uninstall(app.packageName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getUninstallScreen() {
|
||||
// we handle uninstall on our own, no need for special screen
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isUnattended() {
|
||||
// may not always be unattended, but no easy way to find out up-front
|
||||
return SessionInstallManager.canBeUsed();
|
||||
}
|
||||
|
||||
}
|
|
@ -22,28 +22,26 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.PatternMatcher;
|
||||
import android.os.Process;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.LogPrinter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.JobIntentService;
|
||||
|
||||
import org.fdroid.database.FDroidDatabase;
|
||||
import org.fdroid.database.Repository;
|
||||
import org.fdroid.download.Downloader;
|
||||
import org.fdroid.download.NotFoundException;
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.DBHelper;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
import org.fdroid.fdroid.installer.ApkCache;
|
||||
import org.fdroid.fdroid.installer.InstallManagerService;
|
||||
import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.index.v2.FileV1;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -68,16 +66,13 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|||
* through {@link #queue(Context, long, String, String, FileV1)} calls. The
|
||||
* service is started as needed, it handles each {@code Intent} using a worker
|
||||
* thread, and stops itself when it runs out of work. Requests can be canceled
|
||||
* using {@link #cancel(Context, String)}. If this service is killed during
|
||||
* operation, it will receive the queued {@link #queue(Context, long, String, String, FileV1)}
|
||||
* and {@link #cancel(Context, String)} requests again due to
|
||||
* {@link Service#START_REDELIVER_INTENT}. Bad requests will be ignored,
|
||||
* using {@link #cancel(Context, String)}. Bad requests will be ignored,
|
||||
* including on restart after killing via {@link Service#START_NOT_STICKY}.
|
||||
* <p>
|
||||
* This "work queue processor" pattern is commonly used to offload tasks
|
||||
* from an application's main thread. The DownloaderService class exists to
|
||||
* simplify this pattern and take care of the mechanics. DownloaderService
|
||||
* will receive the Intents, launch a worker thread, and stop the service as
|
||||
* will receive the Intents, use a worker thread, and stop the service as
|
||||
* appropriate.
|
||||
* <p>
|
||||
* All requests are handled on a single worker thread -- they may take as
|
||||
|
@ -94,14 +89,14 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|||
* than with APKs since there is not reliable standard for a unique ID for
|
||||
* media files, unlike APKs with {@code packageName} and {@code versionCode}.
|
||||
*
|
||||
* @see android.app.IntentService
|
||||
* @see androidx.core.app.JobIntentService
|
||||
* @see org.fdroid.fdroid.installer.InstallManagerService
|
||||
*/
|
||||
public class DownloaderService extends Service {
|
||||
public class DownloaderService extends JobIntentService {
|
||||
private static final String TAG = "DownloaderService";
|
||||
private static final int JOB_ID = TAG.hashCode();
|
||||
|
||||
private static final String ACTION_QUEUE = "org.fdroid.fdroid.net.DownloaderService.action.QUEUE";
|
||||
private static final String ACTION_CANCEL = "org.fdroid.fdroid.net.DownloaderService.action.CANCEL";
|
||||
|
||||
public static final String ACTION_STARTED = "org.fdroid.fdroid.net.Downloader.action.STARTED";
|
||||
public static final String ACTION_PROGRESS = "org.fdroid.fdroid.net.Downloader.action.PROGRESS";
|
||||
|
@ -125,106 +120,39 @@ public class DownloaderService extends Service {
|
|||
* @see android.content.Intent#EXTRA_ORIGINATING_URI
|
||||
*/
|
||||
public static final String EXTRA_CANONICAL_URL = "org.fdroid.fdroid.net.Downloader.extra.CANONICAL_URL";
|
||||
private static final String EXTRA_INDEX_FILE_V1 = "org.fdroid.fdroid.net.Downloader.extra.INDEX_FILE_V1";
|
||||
|
||||
private volatile Looper serviceLooper;
|
||||
private static volatile ServiceHandler serviceHandler;
|
||||
private static volatile Downloader downloader;
|
||||
private static volatile String activeCanonicalUrl;
|
||||
private InstallManagerService installManagerService;
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
|
||||
private final class ServiceHandler extends Handler {
|
||||
static final String TAG = "ServiceHandler";
|
||||
|
||||
ServiceHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
Utils.debugLog(TAG, "Handling download message with ID of " + msg.what);
|
||||
handleIntent((Intent) msg.obj);
|
||||
stopSelf(msg.arg1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Utils.debugLog(TAG, "Creating downloader service.");
|
||||
|
||||
HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
|
||||
thread.start();
|
||||
|
||||
serviceLooper = thread.getLooper();
|
||||
if (BuildConfig.DEBUG) {
|
||||
serviceLooper.setMessageLogging(new LogPrinter(Log.DEBUG, ServiceHandler.TAG));
|
||||
}
|
||||
serviceHandler = new ServiceHandler(serviceLooper);
|
||||
installManagerService = InstallManagerService.getInstance(this);
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Utils.debugLog(TAG, "Received Intent for downloading: " + intent + " (with a startId of " + startId + ")");
|
||||
protected void onHandleWork(@NonNull Intent intent) {
|
||||
Utils.debugLog(TAG, "Received Intent for downloading: " + intent);
|
||||
|
||||
if (intent == null) {
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
String downloadUrl = intent.getDataString();
|
||||
if (downloadUrl == null) {
|
||||
Utils.debugLog(TAG, "Received Intent with no URI: " + intent);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
String canonicalUrl = intent.getStringExtra(DownloaderService.EXTRA_CANONICAL_URL);
|
||||
String canonicalUrl = intent.getDataString();
|
||||
if (canonicalUrl == null) {
|
||||
Utils.debugLog(TAG, "Received Intent with no EXTRA_CANONICAL_URL: " + intent);
|
||||
return START_NOT_STICKY;
|
||||
Utils.debugLog(TAG, "Received Intent with no URI: " + intent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ACTION_CANCEL.equals(intent.getAction())) {
|
||||
Utils.debugLog(TAG, "Cancelling download of " + canonicalUrl.hashCode() + "/" + canonicalUrl
|
||||
+ " downloading from " + downloadUrl);
|
||||
Integer whatToRemove = canonicalUrl.hashCode();
|
||||
if (serviceHandler.hasMessages(whatToRemove)) {
|
||||
Utils.debugLog(TAG, "Removing download with ID of " + whatToRemove
|
||||
+ " from service handler, then sending interrupted event.");
|
||||
serviceHandler.removeMessages(whatToRemove);
|
||||
sendCancelledBroadcast(intent.getData(), canonicalUrl);
|
||||
} else if (isActive(canonicalUrl)) {
|
||||
downloader.cancelDownload();
|
||||
} else {
|
||||
Utils.debugLog(TAG, "ACTION_CANCEL called on something not queued or running"
|
||||
+ " (expected to find message with ID of " + whatToRemove + " in queue).");
|
||||
}
|
||||
} else if (ACTION_QUEUE.equals(intent.getAction())) {
|
||||
Message msg = serviceHandler.obtainMessage();
|
||||
msg.arg1 = startId;
|
||||
msg.obj = intent;
|
||||
msg.what = canonicalUrl.hashCode();
|
||||
serviceHandler.sendMessage(msg);
|
||||
Utils.debugLog(TAG, "Queued download of " + canonicalUrl.hashCode() + "/" + canonicalUrl
|
||||
+ " using " + downloadUrl);
|
||||
if (ACTION_QUEUE.equals(intent.getAction())) {
|
||||
handleIntent(intent);
|
||||
} else {
|
||||
Utils.debugLog(TAG, "Received Intent with unknown action: " + intent);
|
||||
}
|
||||
|
||||
return START_REDELIVER_INTENT; // if killed before completion, retry Intent
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Utils.debugLog(TAG, "Destroying downloader service. Will move to background and stop our Looper.");
|
||||
serviceLooper.quit(); //NOPMD - this is copied from IntentService, no super call needed
|
||||
}
|
||||
|
||||
/**
|
||||
* This service does not use binding, so no need to implement this method
|
||||
*/
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
Utils.debugLog(TAG, "Destroying downloader service.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -244,14 +172,20 @@ public class DownloaderService extends Service {
|
|||
* android.content.Context#startService(Intent)}.
|
||||
*/
|
||||
private void handleIntent(Intent intent) {
|
||||
final Uri uri = intent.getData();
|
||||
final long repoId = intent.getLongExtra(DownloaderService.EXTRA_REPO_ID, 0);
|
||||
final Uri canonicalUrl = intent.getData();
|
||||
final Uri downloadUrl =
|
||||
Uri.parse(intent.getStringExtra(DownloaderService.EXTRA_CANONICAL_URL));
|
||||
final FileV1 fileV1 = FileV1.deserialize(intent.getStringExtra(DownloaderService.EXTRA_INDEX_FILE_V1));
|
||||
final App app = intent.getParcelableExtra(Installer.EXTRA_APP);
|
||||
final Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||
final long repoId = intent.getLongExtra(DownloaderService.EXTRA_REPO_ID, apk.repoId);
|
||||
final String extraUrl = intent.getStringExtra(DownloaderService.EXTRA_CANONICAL_URL);
|
||||
final Uri downloadUrl = Uri.parse(extraUrl == null ? apk.getDownloadUrl() : extraUrl);
|
||||
final FileV1 fileV1 = apk.apkFile;
|
||||
final SanitizedFile localFile = ApkCache.getApkDownloadPath(this, canonicalUrl);
|
||||
sendBroadcast(uri, DownloaderService.ACTION_STARTED, localFile, repoId, canonicalUrl);
|
||||
|
||||
Utils.debugLog(TAG, "Queued download of " + canonicalUrl.hashCode() + "/" + canonicalUrl
|
||||
+ " using " + downloadUrl);
|
||||
|
||||
sendBroadcast(canonicalUrl, DownloaderService.ACTION_STARTED, localFile, repoId, canonicalUrl);
|
||||
installManagerService.onDownloadStarted(canonicalUrl);
|
||||
|
||||
try {
|
||||
activeCanonicalUrl = canonicalUrl.toString();
|
||||
|
@ -269,30 +203,38 @@ public class DownloaderService extends Service {
|
|||
}
|
||||
}
|
||||
downloader = DownloaderFactory.INSTANCE.create(repo, downloadUrl, fileV1, localFile);
|
||||
downloader.setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(long bytesRead, long totalBytes) {
|
||||
Intent intent = new Intent(DownloaderService.ACTION_PROGRESS);
|
||||
intent.setData(canonicalUrl);
|
||||
intent.putExtra(DownloaderService.EXTRA_BYTES_READ, bytesRead);
|
||||
intent.putExtra(DownloaderService.EXTRA_TOTAL_BYTES, totalBytes);
|
||||
localBroadcastManager.sendBroadcast(intent);
|
||||
}
|
||||
final long[] lastProgressSent = {0};
|
||||
downloader.setListener((bytesRead, totalBytes) -> {
|
||||
// don't send a progress updates out to frequently, to not hit notification rate-limiting
|
||||
// this can cause us to miss critical notification updates
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastProgressSent[0] < 1_000) return;
|
||||
lastProgressSent[0] = now;
|
||||
Intent intent1 = new Intent(DownloaderService.ACTION_PROGRESS);
|
||||
intent1.setData(canonicalUrl);
|
||||
intent1.putExtra(DownloaderService.EXTRA_BYTES_READ, bytesRead);
|
||||
intent1.putExtra(DownloaderService.EXTRA_TOTAL_BYTES, totalBytes);
|
||||
localBroadcastManager.sendBroadcast(intent1);
|
||||
installManagerService.onDownloadProgress(canonicalUrl, app, apk, bytesRead, totalBytes);
|
||||
});
|
||||
downloader.download();
|
||||
sendBroadcast(uri, DownloaderService.ACTION_COMPLETE, localFile, repoId, canonicalUrl);
|
||||
sendBroadcast(canonicalUrl, DownloaderService.ACTION_COMPLETE, localFile, repoId, canonicalUrl);
|
||||
installManagerService.onDownloadComplete(canonicalUrl, localFile, app, apk);
|
||||
} catch (InterruptedException e) {
|
||||
sendBroadcast(uri, DownloaderService.ACTION_INTERRUPTED, localFile, repoId, canonicalUrl);
|
||||
sendBroadcast(canonicalUrl, DownloaderService.ACTION_INTERRUPTED, localFile, repoId, canonicalUrl);
|
||||
installManagerService.onDownloadFailed(canonicalUrl, null);
|
||||
} catch (ConnectException | HttpRetryException | NoRouteToHostException | SocketTimeoutException
|
||||
| SSLHandshakeException | SSLKeyException | SSLPeerUnverifiedException | SSLProtocolException
|
||||
| ProtocolException | UnknownHostException | NotFoundException e) {
|
||||
| SSLHandshakeException | SSLKeyException | SSLPeerUnverifiedException | SSLProtocolException
|
||||
| ProtocolException | UnknownHostException | NotFoundException e) {
|
||||
// if the above list of exceptions changes, also change it in IndexV1Updater.update()
|
||||
Log.e(TAG, "CONNECTION_FAILED: " + e.getLocalizedMessage());
|
||||
sendBroadcast(uri, DownloaderService.ACTION_CONNECTION_FAILED, localFile, repoId, canonicalUrl);
|
||||
sendBroadcast(canonicalUrl, DownloaderService.ACTION_CONNECTION_FAILED, localFile, repoId, canonicalUrl);
|
||||
installManagerService.onDownloadFailed(canonicalUrl, e.getLocalizedMessage());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
sendBroadcast(uri, DownloaderService.ACTION_INTERRUPTED, localFile,
|
||||
Log.e(TAG, "Error downloading: ", e);
|
||||
sendBroadcast(canonicalUrl, DownloaderService.ACTION_INTERRUPTED, localFile,
|
||||
e.getLocalizedMessage(), repoId, canonicalUrl);
|
||||
installManagerService.onDownloadFailed(canonicalUrl, e.getLocalizedMessage());
|
||||
} finally {
|
||||
if (downloader != null) {
|
||||
downloader.close();
|
||||
|
@ -302,10 +244,6 @@ public class DownloaderService extends Service {
|
|||
activeCanonicalUrl = null;
|
||||
}
|
||||
|
||||
private void sendCancelledBroadcast(Uri uri, String canonicalUrl) {
|
||||
sendBroadcast(uri, DownloaderService.ACTION_INTERRUPTED, null, 0, Uri.parse(canonicalUrl));
|
||||
}
|
||||
|
||||
private void sendBroadcast(Uri uri, String action, File file, long repoId, Uri canonicalUrl) {
|
||||
sendBroadcast(uri, action, file, null, repoId, canonicalUrl);
|
||||
}
|
||||
|
@ -335,10 +273,10 @@ public class DownloaderService extends Service {
|
|||
* @param context this app's {@link Context}
|
||||
* @param repoId the database ID number representing one repo
|
||||
* @param canonicalUrl the URL used as the unique ID throughout F-Droid
|
||||
* @see #cancel(Context, String)
|
||||
* @see #cancel(String)
|
||||
*/
|
||||
public static void queue(Context context, long repoId, String canonicalUrl,
|
||||
String downloadUrl, FileV1 fileV1) {
|
||||
String downloadUrl) {
|
||||
if (TextUtils.isEmpty(canonicalUrl)) {
|
||||
return;
|
||||
}
|
||||
|
@ -348,8 +286,20 @@ public class DownloaderService extends Service {
|
|||
intent.setData(Uri.parse(canonicalUrl));
|
||||
intent.putExtra(DownloaderService.EXTRA_REPO_ID, repoId);
|
||||
intent.putExtra(DownloaderService.EXTRA_CANONICAL_URL, downloadUrl);
|
||||
intent.putExtra(DownloaderService.EXTRA_INDEX_FILE_V1, fileV1.serialize());
|
||||
context.startService(intent);
|
||||
JobIntentService.enqueueWork(context, DownloaderService.class, JOB_ID, intent);
|
||||
}
|
||||
|
||||
public static void queue(Context context, String canonicalUrl, @NonNull App app, @NonNull Apk apk) {
|
||||
if (TextUtils.isEmpty(canonicalUrl) || apk.apkFile == null) {
|
||||
return;
|
||||
}
|
||||
Utils.debugLog(TAG, "Queue download " + canonicalUrl.hashCode() + "/" + canonicalUrl);
|
||||
Intent intent = new Intent(context, DownloaderService.class);
|
||||
intent.setAction(ACTION_QUEUE);
|
||||
intent.setData(Uri.parse(canonicalUrl));
|
||||
intent.putExtra(Installer.EXTRA_APP, app);
|
||||
intent.putExtra(Installer.EXTRA_APK, apk);
|
||||
JobIntentService.enqueueWork(context, DownloaderService.class, JOB_ID, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -357,35 +307,22 @@ public class DownloaderService extends Service {
|
|||
* <p>
|
||||
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
|
||||
*
|
||||
* @param context this app's {@link Context}
|
||||
* @param canonicalUrl The URL to remove from the download queue
|
||||
* @see #queue(Context, long, String, String, FileV1
|
||||
* @see #queue(Context, String, App, Apk)
|
||||
*/
|
||||
public static void cancel(Context context, String canonicalUrl) {
|
||||
public static void cancel(String canonicalUrl) {
|
||||
if (TextUtils.isEmpty(canonicalUrl)) {
|
||||
return;
|
||||
}
|
||||
Utils.debugLog(TAG, "Send cancel for " + canonicalUrl.hashCode() + "/" + canonicalUrl);
|
||||
Intent intent = new Intent(context, DownloaderService.class);
|
||||
intent.setAction(ACTION_CANCEL);
|
||||
intent.setData(Uri.parse(canonicalUrl));
|
||||
intent.putExtra(DownloaderService.EXTRA_CANONICAL_URL, canonicalUrl);
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL is waiting in the queue for downloading or if actively being downloaded.
|
||||
* This is useful for checking whether to re-register {@link android.content.BroadcastReceiver}s
|
||||
* in {@link android.app.AppCompatActivity#onResume()}.
|
||||
*/
|
||||
public static boolean isQueuedOrActive(String canonicalUrl) {
|
||||
if (TextUtils.isEmpty(canonicalUrl)) { //NOPMD - suggests unreadable format
|
||||
return false;
|
||||
Utils.debugLog(TAG, "Cancelling download of " + canonicalUrl.hashCode() + "/" + canonicalUrl
|
||||
+ " downloading from " + canonicalUrl);
|
||||
int whatToRemove = canonicalUrl.hashCode();
|
||||
if (isActive(canonicalUrl)) {
|
||||
downloader.cancelDownload();
|
||||
} else {
|
||||
Utils.debugLog(TAG, "ACTION_CANCEL called on something not queued or running"
|
||||
+ " (expected to find message with ID of " + whatToRemove + " in queue).");
|
||||
}
|
||||
if (serviceHandler == null) {
|
||||
return false; // this service is not even running
|
||||
}
|
||||
return serviceHandler.hasMessages(canonicalUrl.hashCode()) || isActive(canonicalUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -189,11 +189,13 @@ public class AppDetailsActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
visiblePackageName = packageName;
|
||||
|
||||
updateNotificationsForApp();
|
||||
// don't call this in onResume() because while install confirmation dialog gets shown we pause/resume,
|
||||
// so it would get called twice for the same state
|
||||
refreshStatus();
|
||||
registerAppStatusReceiver();
|
||||
|
||||
|
@ -211,20 +213,18 @@ public class AppDetailsActivity extends AppCompatActivity
|
|||
if (statuses.hasNext()) {
|
||||
AppUpdateStatusManager.AppUpdateStatus status = statuses.next();
|
||||
updateAppStatus(status, false);
|
||||
} else {
|
||||
// no status found, so we should update to reflect that as well
|
||||
updateAppStatus(null, false);
|
||||
}
|
||||
|
||||
currentStatus = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
unregisterAppStatusReceiver();
|
||||
}
|
||||
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
visiblePackageName = null;
|
||||
unregisterAppStatusReceiver();
|
||||
|
||||
// When leaving the app details, make sure to refresh app status for this app, since
|
||||
// we might want to show notifications for it now.
|
||||
|
@ -473,6 +473,10 @@ public class AppDetailsActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
private void updateAppStatus(@Nullable AppUpdateStatusManager.AppUpdateStatus newStatus, boolean justReceived) {
|
||||
if (!justReceived && newStatus == null && currentStatus != null) {
|
||||
// clear progress if the state got removed in the meantime (e.g. download canceled)
|
||||
adapter.clearProgress();
|
||||
}
|
||||
this.currentStatus = newStatus;
|
||||
if (this.currentStatus == null) {
|
||||
return;
|
||||
|
@ -495,6 +499,17 @@ public class AppDetailsActivity extends AppCompatActivity
|
|||
adapter.setIndeterminateProgress(R.string.installing);
|
||||
localBroadcastManager.registerReceiver(installReceiver,
|
||||
Installer.getInstallIntentFilter(newStatus.getCanonicalUrl()));
|
||||
} else {
|
||||
try {
|
||||
if (newStatus.intent != null) {
|
||||
localBroadcastManager.registerReceiver(installReceiver,
|
||||
Installer.getInstallIntentFilter(newStatus.getCanonicalUrl()));
|
||||
newStatus.intent.send();
|
||||
}
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.e(TAG, "PI canceled", e);
|
||||
}
|
||||
adapter.clearProgress();
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -510,9 +525,8 @@ public class AppDetailsActivity extends AppCompatActivity
|
|||
Toast.makeText(this, R.string.download_error, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
adapter.clearProgress();
|
||||
}
|
||||
adapter.clearProgress();
|
||||
break;
|
||||
|
||||
case Installing:
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.text.Spannable;
|
|||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.format.Formatter;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
|
@ -60,6 +61,7 @@ import org.fdroid.fdroid.Utils;
|
|||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.installer.SessionInstallManager;
|
||||
import org.fdroid.fdroid.privileged.views.AppDiff;
|
||||
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
|
||||
import org.fdroid.fdroid.views.appdetails.AntiFeaturesListingView;
|
||||
|
@ -376,6 +378,7 @@ public class AppDetailsRecyclerViewAdapter
|
|||
final TextView titleView;
|
||||
final TextView authorView;
|
||||
final TextView lastUpdateView;
|
||||
final TextView warningView;
|
||||
final TextView summaryView;
|
||||
final TextView whatsNewView;
|
||||
final TextView descriptionView;
|
||||
|
@ -400,6 +403,7 @@ public class AppDetailsRecyclerViewAdapter
|
|||
titleView = (TextView) view.findViewById(R.id.title);
|
||||
authorView = (TextView) view.findViewById(R.id.author);
|
||||
lastUpdateView = (TextView) view.findViewById(R.id.text_last_update);
|
||||
warningView = (TextView) view.findViewById(R.id.warning);
|
||||
summaryView = (TextView) view.findViewById(R.id.summary);
|
||||
whatsNewView = (TextView) view.findViewById(R.id.latest);
|
||||
descriptionView = (TextView) view.findViewById(R.id.description);
|
||||
|
@ -490,12 +494,27 @@ public class AppDetailsRecyclerViewAdapter
|
|||
}
|
||||
if (app.lastUpdated != null) {
|
||||
Resources res = lastUpdateView.getContext().getResources();
|
||||
lastUpdateView.setText(Utils.formatLastUpdated(res, app.lastUpdated));
|
||||
String lastUpdated = Utils.formatLastUpdated(res, app.lastUpdated);
|
||||
String text;
|
||||
if (Preferences.get().expertMode() && suggestedApk != null && suggestedApk.apkFile != null
|
||||
&& suggestedApk.apkFile.getSize() != null) {
|
||||
String size = Formatter.formatFileSize(context, suggestedApk.apkFile.getSize());
|
||||
text = lastUpdated + " (" + size + ")";
|
||||
} else {
|
||||
text = lastUpdated;
|
||||
}
|
||||
lastUpdateView.setText(text);
|
||||
lastUpdateView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
lastUpdateView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (SessionInstallManager.canBeUsed() && suggestedApk != null
|
||||
&& !SessionInstallManager.isTargetSdkSupported(suggestedApk.targetSdkVersion)) {
|
||||
warningView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
warningView.setVisibility(View.GONE);
|
||||
}
|
||||
if (!TextUtils.isEmpty(app.summary)) {
|
||||
summaryView.setText(app.summary);
|
||||
summaryView.setVisibility(View.VISIBLE);
|
||||
|
|
|
@ -43,7 +43,6 @@ import com.bumptech.glide.RequestManager;
|
|||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Languages;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
|
@ -437,7 +436,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat
|
|||
|
||||
String versionName = Utils.getVersionName(context);
|
||||
if (versionName != null) {
|
||||
((TextView) view.findViewById(R.id.version)).setText(versionName);
|
||||
TextView versionNameView = view.findViewById(R.id.version);
|
||||
versionNameView.setText(versionName);
|
||||
versionNameView.setOnLongClickListener(v -> {
|
||||
throw new RuntimeException("BOOM!");
|
||||
});
|
||||
}
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setView(view)
|
||||
|
@ -565,7 +568,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat
|
|||
|
||||
currentKeepCacheTime = Preferences.get().getKeepCacheTime();
|
||||
|
||||
if (!"basic".equals(BuildConfig.FLAVOR)) initAutoFetchUpdatesPreference(); // TODO remove once basic can do it
|
||||
initAutoFetchUpdatesPreference();
|
||||
initPrivilegedInstallerPreference();
|
||||
initUseTorPreference(getActivity().getApplicationContext());
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.graphics.Outline;
|
|||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.widget.Button;
|
||||
|
@ -513,32 +514,29 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity);
|
||||
final BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Installer.ACTION_INSTALL_USER_INTERACTION.equals(intent.getAction())) {
|
||||
broadcastManager.unregisterReceiver(this);
|
||||
PendingIntent pendingIntent =
|
||||
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
|
||||
try {
|
||||
pendingIntent.send();
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.e(TAG, "Error starting pending intent: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
|
||||
String canonicalUrl = currentStatus.apk.getCanonicalUrl();
|
||||
File apkFilePath = ApkCache.getApkDownloadPath(activity, canonicalUrl);
|
||||
Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getCanonicalUrl() +
|
||||
" to " + apkFilePath);
|
||||
|
||||
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity);
|
||||
final BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
broadcastManager.unregisterReceiver(this);
|
||||
|
||||
if (Installer.ACTION_INSTALL_USER_INTERACTION.equals(intent.getAction())) {
|
||||
PendingIntent pendingIntent =
|
||||
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
|
||||
try {
|
||||
pendingIntent.send();
|
||||
} catch (PendingIntent.CanceledException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Uri canonicalUri = Uri.parse(canonicalUrl);
|
||||
broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(canonicalUri));
|
||||
broadcastManager.registerReceiver(receiver, Installer.getInstallInteractionIntentFilter(canonicalUri));
|
||||
Installer installer = InstallerFactory.create(activity, currentStatus.app, currentStatus.apk);
|
||||
installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), canonicalUri);
|
||||
} else {
|
||||
|
@ -553,7 +551,13 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
|
|||
Repository repo = FDroidApp.getRepo(version.getRepoId());
|
||||
return new Apk(version, repo);
|
||||
}, receivedApk -> {
|
||||
if (receivedApk != null) InstallManagerService.queue(activity, app, receivedApk);
|
||||
if (receivedApk != null) {
|
||||
String canonicalUrl = receivedApk.getCanonicalUrl();
|
||||
Uri canonicalUri = Uri.parse(canonicalUrl);
|
||||
broadcastManager.registerReceiver(receiver,
|
||||
Installer.getInstallInteractionIntentFilter(canonicalUri));
|
||||
InstallManagerService.queue(activity, app, receivedApk);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,7 +130,6 @@
|
|||
android:layout_below="@id/icon_and_name"
|
||||
android:clipToPadding="false"
|
||||
android:gravity="end"
|
||||
android:paddingBottom="4dp"
|
||||
android:visibility="visible">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
@ -158,6 +157,19 @@
|
|||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="@string/warning_target_sdk"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/design_default_color_on_primary"
|
||||
android:textSize="15sp"
|
||||
android:visibility="gone"
|
||||
android:background="@color/warning"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/summary"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
<color name="fdroid_green">#ff8ab000</color>
|
||||
|
||||
<color name="shadow">#cc222222</color>
|
||||
<color name="warning">#827717</color>
|
||||
|
||||
<color name="perms_costs_money">#fff4511e</color>
|
||||
|
||||
|
|
|
@ -702,6 +702,7 @@ This often occurs with apps installed via Google Play or other sources, if they
|
|||
|
||||
<string name="details_last_updated_today">Updated today</string>
|
||||
<string name="warning_scaning_qr_code">Your camera doesn\'t seem to have an autofocus. It might be difficult to scan the code.</string>
|
||||
<string name="warning_target_sdk">This app was built for an older version of Android and cannot be updated automatically.</string>
|
||||
<string name="undo">Undo</string>
|
||||
<string name="app_list__dismiss_installing_app">Installation cancelled</string>
|
||||
<plurals name="details_last_update_days">
|
||||
|
|
Loading…
Reference in New Issue