receivers = new HashMap<>(3);
- /**
- * Get the app name based on a {@code urlString} key. The app name needs
- * to be kept around for the final notification update, but {@link App}
- * and {@link Apk} instances have already removed by the time that final
- * notification update comes around. Once there is a proper
- * {@code InstallerService} and its integrated here, this must go away,
- * since the {@link App} and {@link Apk} instances will be available.
- *
- * TODO delete me once InstallerService exists
- */
- private static final HashMap TEMP_HACK_APP_NAMES = new HashMap<>(3);
-
private LocalBroadcastManager localBroadcastManager;
private NotificationManager notificationManager;
@@ -234,15 +222,18 @@ public class InstallManagerService extends Service {
BroadcastReceiver completeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- String urlString = intent.getDataString();
- // TODO these need to be removed based on whether they are fed to InstallerService or not
- Apk apk = removeFromActive(urlString);
- if (AppDetails.isAppVisible(apk.packageName)) {
- cancelNotification(urlString);
- } else {
- notifyDownloadComplete(urlString, apk);
- }
- unregisterDownloaderReceivers(urlString);
+ // elsewhere called urlString
+ Uri originatingUri = intent.getData();
+ File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
+ Uri localUri = Uri.fromFile(localFile);
+
+ Utils.debugLog(TAG, "download completed of " + originatingUri
+ + " to " + localUri);
+
+ unregisterDownloaderReceivers(intent.getDataString());
+
+ registerInstallerReceivers(localUri);
+ InstallerService.install(context, localUri, originatingUri);
}
};
BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
@@ -265,6 +256,69 @@ public class InstallManagerService extends Service {
receivers.put(urlString, new BroadcastReceiver[]{
startedReceiver, progressReceiver, completeReceiver, interruptedReceiver,
});
+
+
+ }
+
+ private void registerInstallerReceivers(Uri uri) {
+
+ BroadcastReceiver installReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case InstallHelper.ACTION_INSTALL_STARTED: {
+ Utils.debugLog(TAG, "ACTION_INSTALL_STARTED");
+
+ break;
+ }
+ case InstallHelper.ACTION_INSTALL_COMPLETE: {
+ Utils.debugLog(TAG, "ACTION_INSTALL_COMPLETE");
+
+ Uri originatingUri =
+ intent.getParcelableExtra(InstallHelper.EXTRA_ORIGINATING_URI);
+ String urlString = originatingUri.toString();
+ removeFromActive(urlString);
+
+ localBroadcastManager.unregisterReceiver(this);
+
+ break;
+ }
+ case InstallHelper.ACTION_INSTALL_INTERRUPTED: {
+ Utils.debugLog(TAG, "ACTION_INSTALL_INTERRUPTED");
+
+ localBroadcastManager.unregisterReceiver(this);
+
+ break;
+ }
+ case InstallHelper.ACTION_INSTALL_USER_INTERACTION: {
+ Utils.debugLog(TAG, "ACTION_INSTALL_USER_INTERACTION");
+
+ Uri originatingUri =
+ intent.getParcelableExtra(InstallHelper.EXTRA_ORIGINATING_URI);
+ PendingIntent installPendingIntent =
+ intent.getParcelableExtra(InstallHelper.EXTRA_USER_INTERACTION_PI);
+ // TODO
+ String urlString = originatingUri.toString();
+ Apk apk = getFromActive(urlString);
+ Utils.debugLog(TAG, "urlString: " + urlString);
+
+ if (AppDetails.isAppVisible(apk.packageName)) {
+ cancelNotification(urlString);
+ } else {
+ notifyDownloadComplete(apk, urlString, installPendingIntent);
+ }
+
+ break;
+ }
+ default: {
+ throw new RuntimeException("intent action not handled!");
+ }
+ }
+ }
+ };
+
+ localBroadcastManager.registerReceiver(installReceiver,
+ InstallerService.getInstallIntentFilter(uri));
}
private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) {
@@ -283,16 +337,7 @@ public class InstallManagerService extends Service {
private String getAppName(String urlString, Apk apk) {
App app = ACTIVE_APPS.get(apk.packageName);
- if (app == null || TextUtils.isEmpty(app.name)) {
- if (TEMP_HACK_APP_NAMES.containsKey(urlString)) {
- return TEMP_HACK_APP_NAMES.get(urlString);
- } else {
- // this is ugly, but its better than nothing as a failsafe
- return urlString;
- }
- } else {
- return app.name;
- }
+ return app.name;
}
/**
@@ -319,7 +364,7 @@ public class InstallManagerService extends Service {
* Removing the progress bar from a notification should cause the notification's content
* text to return to normal size
*/
- private void notifyDownloadComplete(String urlString, Apk apk) {
+ private void notifyDownloadComplete(Apk apk, String urlString, PendingIntent installPendingIntent) {
String title;
try {
PackageManager pm = getPackageManager();
@@ -335,7 +380,7 @@ public class InstallManagerService extends Service {
.setAutoCancel(true)
.setOngoing(false)
.setContentTitle(title)
- .setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
+ .setContentIntent(installPendingIntent)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentText(getString(R.string.tap_to_install))
.build();
@@ -354,7 +399,10 @@ public class InstallManagerService extends Service {
private static void addToActive(String urlString, App app, Apk apk) {
ACTIVE_APKS.put(urlString, apk);
ACTIVE_APPS.put(app.packageName, app);
- TEMP_HACK_APP_NAMES.put(urlString, app.name); // TODO delete me once InstallerService exists
+ }
+
+ private static Apk getFromActive(String urlString) {
+ return ACTIVE_APKS.get(urlString);
}
/**
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java
new file mode 100644
index 000000000..13fbc7694
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2016 Hans-Christoph Steiner
+ * Copyright (C) 2016 Dominik Schürmann
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fdroid.fdroid.installer;
+
+import android.app.PendingIntent;
+import android.app.Service;
+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.support.v4.content.LocalBroadcastManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.fdroid.fdroid.Utils;
+
+import java.io.File;
+
+/**
+ * InstallerService based on DownloaderService
+ */
+public class InstallerService extends Service {
+ private static final String TAG = "InstallerService";
+
+ private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.InstallerService.action.INSTALL";
+ private static final String ACTION_UNINSTALL = "org.fdroid.fdroid.installer.InstallerService.action.UNINSTALL";
+
+ private volatile Looper serviceLooper;
+ private static volatile ServiceHandler serviceHandler;
+ private LocalBroadcastManager localBroadcastManager;
+
+ private final class ServiceHandler extends Handler {
+ ServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ Utils.debugLog(TAG, "Handling message with ID of " + msg.what);
+ handleIntent((Intent) msg.obj);
+ stopSelf(msg.arg1);
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Utils.debugLog(TAG, "Creating installer service.");
+
+ HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+
+ serviceLooper = thread.getLooper();
+ serviceHandler = new ServiceHandler(serviceLooper);
+ localBroadcastManager = LocalBroadcastManager.getInstance(this);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Utils.debugLog(TAG, "Received Intent for installing/uninstalling: " + intent + " (with a startId of " + startId + ")");
+
+ if (ACTION_INSTALL.equals(intent.getAction())) {
+ Uri uri = intent.getData();
+
+ Message msg = serviceHandler.obtainMessage();
+ msg.arg1 = startId;
+ msg.obj = intent;
+ msg.what = uri.hashCode();
+ serviceHandler.sendMessage(msg);
+ Utils.debugLog(TAG, "Start install of " + uri.toString());
+ } else if (ACTION_UNINSTALL.equals(intent.getAction())) {
+ String packageName = intent.getStringExtra(InstallHelper.EXTRA_UNINSTALL_PACKAGE_NAME);
+
+ Message msg = serviceHandler.obtainMessage();
+ msg.arg1 = startId;
+ msg.obj = intent;
+ msg.what = packageName.hashCode();
+ serviceHandler.sendMessage(msg);
+ Utils.debugLog(TAG, "Start uninstall of " + packageName);
+ } else {
+ Log.e(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 installer 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;
+ }
+
+ protected void handleIntent(Intent intent) {
+ switch (intent.getAction()) {
+ case ACTION_INSTALL: {
+ Uri uri = intent.getData();
+ Uri originatingUri = intent.getParcelableExtra(InstallHelper.EXTRA_ORIGINATING_URI);
+ sendBroadcastInstall(uri, originatingUri, InstallHelper.ACTION_INSTALL_STARTED);
+
+ Utils.debugLog(TAG, "ACTION_INSTALL uri: " + uri + " file: " + new File(uri.getPath()));
+
+ // TODO: rework for uri
+ Uri sanitizedUri = null;
+ try {
+ File file = InstallHelper.preparePackage(this, new File(uri.getPath()), null,
+ originatingUri.toString());
+ sanitizedUri = Uri.fromFile(file);
+ } catch (Installer.InstallFailedException e) {
+ e.printStackTrace();
+ }
+ Utils.debugLog(TAG, "ACTION_INSTALL sanitizedUri: " + sanitizedUri);
+
+
+ Intent installIntent = new Intent(this, AndroidInstallerActivity.class);
+ installIntent.setAction(AndroidInstallerActivity.ACTION_INSTALL_PACKAGE);
+ installIntent.putExtra(AndroidInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri);
+ installIntent.setData(sanitizedUri);
+ PendingIntent installPendingIntent = PendingIntent.getActivity(this.getApplicationContext(),
+ uri.hashCode(),
+ installIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ sendBroadcastInstall(uri, originatingUri, InstallHelper.ACTION_INSTALL_USER_INTERACTION,
+ installPendingIntent);
+
+ break;
+ }
+
+ case ACTION_UNINSTALL: {
+ String packageName =
+ intent.getStringExtra(InstallHelper.EXTRA_UNINSTALL_PACKAGE_NAME);
+ sendBroadcastUninstall(packageName, InstallHelper.ACTION_UNINSTALL_STARTED);
+
+
+ Intent installIntent = new Intent(this, AndroidInstallerActivity.class);
+ installIntent.setAction(AndroidInstallerActivity.ACTION_UNINSTALL_PACKAGE);
+ installIntent.putExtra(
+ AndroidInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
+ PendingIntent uninstallPendingIntent = PendingIntent.getActivity(this.getApplicationContext(),
+ packageName.hashCode(),
+ installIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ sendBroadcastUninstall(packageName, InstallHelper.ACTION_UNINSTALL_USER_INTERACTION,
+ uninstallPendingIntent);
+
+
+ break;
+ }
+ }
+ }
+
+ private void sendBroadcastInstall(Uri uri, Uri originatingUri, String action,
+ PendingIntent pendingIntent) {
+ sendBroadcastInstall(uri, originatingUri, action, pendingIntent, null);
+ }
+
+ private void sendBroadcastInstall(Uri uri, Uri originatingUri, String action) {
+ sendBroadcastInstall(uri, originatingUri, action, null, null);
+ }
+
+ private void sendBroadcastInstall(Uri uri, Uri originatingUri, String action,
+ PendingIntent pendingIntent, String errorMessage) {
+ Intent intent = new Intent(action);
+ intent.setData(uri);
+ intent.putExtra(InstallHelper.EXTRA_ORIGINATING_URI, originatingUri);
+ intent.putExtra(InstallHelper.EXTRA_USER_INTERACTION_PI, pendingIntent);
+ if (!TextUtils.isEmpty(errorMessage)) {
+ intent.putExtra(InstallHelper.EXTRA_ERROR_MESSAGE, errorMessage);
+ }
+ localBroadcastManager.sendBroadcast(intent);
+ }
+
+ private void sendBroadcastUninstall(String packageName, String action) {
+ sendBroadcastUninstall(packageName, action, null, null);
+ }
+
+ private void sendBroadcastUninstall(String packageName, String action,
+ PendingIntent pendingIntent) {
+ sendBroadcastUninstall(packageName, action, pendingIntent, null);
+ }
+
+ private void sendBroadcastUninstall(String packageName, String action,
+ PendingIntent pendingIntent, String errorMessage) {
+ Uri uri = Uri.fromParts("package", packageName, null);
+
+ Intent intent = new Intent(action);
+ intent.setData(uri); // for broadcast filtering
+ intent.putExtra(InstallHelper.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
+ intent.putExtra(InstallHelper.EXTRA_USER_INTERACTION_PI, pendingIntent);
+ if (!TextUtils.isEmpty(errorMessage)) {
+ intent.putExtra(InstallHelper.EXTRA_ERROR_MESSAGE, errorMessage);
+ }
+ localBroadcastManager.sendBroadcast(intent);
+ }
+
+ public static void install(Context context, Uri uri, Uri originatingUri) {
+ Intent intent = new Intent(context, InstallerService.class);
+ intent.setAction(ACTION_INSTALL);
+ intent.setData(uri);
+ intent.putExtra(InstallHelper.EXTRA_ORIGINATING_URI, originatingUri);
+ context.startService(intent);
+ }
+
+ public static void uninstall(Context context, String packageName) {
+ Intent intent = new Intent(context, InstallerService.class);
+ intent.setAction(ACTION_UNINSTALL);
+ intent.putExtra(InstallHelper.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
+ context.startService(intent);
+ }
+
+ public static IntentFilter getInstallIntentFilter(Uri uri) {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(InstallHelper.ACTION_INSTALL_STARTED);
+ intentFilter.addAction(InstallHelper.ACTION_INSTALL_COMPLETE);
+ intentFilter.addAction(InstallHelper.ACTION_INSTALL_INTERRUPTED);
+ intentFilter.addAction(InstallHelper.ACTION_INSTALL_USER_INTERACTION);
+ intentFilter.addDataScheme(uri.getScheme());
+ intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
+ return intentFilter;
+ }
+
+ public static IntentFilter getUninstallIntentFilter(String packageName) {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(InstallHelper.ACTION_UNINSTALL_STARTED);
+ intentFilter.addAction(InstallHelper.ACTION_UNINSTALL_COMPLETE);
+ intentFilter.addAction(InstallHelper.ACTION_UNINSTALL_INTERRUPTED);
+ intentFilter.addAction(InstallHelper.ACTION_UNINSTALL_USER_INTERACTION);
+ intentFilter.addDataScheme("package");
+ intentFilter.addDataPath(packageName, PatternMatcher.PATTERN_LITERAL);
+ return intentFilter;
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3dade6788..d07727e72 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -370,6 +370,8 @@
Provided by %1$s.
Downloading…
Downloading %1$s
+ Installing…
+ Uninstalling…
Never
Hourly