put up a notification for each completed download
This makes background installs and updates a lot easier
This commit is contained in:
parent
d114f428e7
commit
74d1c9521d
@ -34,7 +34,7 @@ public class DownloaderServiceTest extends ServiceTestCase<DownloaderService> {
|
|||||||
}
|
}
|
||||||
}, new IntentFilter(Downloader.ACTION_PROGRESS));
|
}, new IntentFilter(Downloader.ACTION_PROGRESS));
|
||||||
for (String url : urls) {
|
for (String url : urls) {
|
||||||
DownloaderService.queue(getContext(), url);
|
DownloaderService.queue(getContext(), null, url);
|
||||||
}
|
}
|
||||||
Thread.sleep(30000);
|
Thread.sleep(30000);
|
||||||
}
|
}
|
||||||
|
@ -442,6 +442,9 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".net.DownloaderService"
|
android:name=".net.DownloaderService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<service
|
||||||
|
android:name=".net.DownloadCompleteService"
|
||||||
|
android:exported="false" />
|
||||||
<service android:name=".net.WifiStateChangeService" />
|
<service android:name=".net.WifiStateChangeService" />
|
||||||
<service android:name=".localrepo.SwapService" />
|
<service android:name=".localrepo.SwapService" />
|
||||||
</application>
|
</application>
|
||||||
|
@ -511,7 +511,7 @@ public class AppDetails extends AppCompatActivity {
|
|||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
|
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
|
||||||
try {
|
try {
|
||||||
installer.installPackage(localFile, app.packageName);
|
installer.installPackage(localFile, app.packageName, intent.getDataString());
|
||||||
} catch (InstallFailedException e) {
|
} catch (InstallFailedException e) {
|
||||||
Log.e(TAG, "Android not compatible with this Installer!", e);
|
Log.e(TAG, "Android not compatible with this Installer!", e);
|
||||||
}
|
}
|
||||||
@ -869,7 +869,7 @@ public class AppDetails extends AppCompatActivity {
|
|||||||
activeDownloadUrlString = urlString;
|
activeDownloadUrlString = urlString;
|
||||||
registerDownloaderReceivers();
|
registerDownloaderReceivers();
|
||||||
headerFragment.startProgress();
|
headerFragment.startProgress();
|
||||||
DownloaderService.queue(this, activeDownloadUrlString);
|
DownloaderService.queue(this, apk.packageName, activeDownloadUrlString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeApk(String packageName) {
|
public void removeApk(String packageName) {
|
||||||
|
@ -496,7 +496,7 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
ApkProvider.DataColumns.NAME,
|
ApkProvider.DataColumns.NAME,
|
||||||
});
|
});
|
||||||
String urlString = Utils.getApkUrl(repoAddress, apk);
|
String urlString = Utils.getApkUrl(repoAddress, apk);
|
||||||
DownloaderService.queue(this, urlString);
|
DownloaderService.queue(this, app.packageName, urlString);
|
||||||
cursor.moveToNext();
|
cursor.moveToNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ import java.util.Formatter;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.zip.Adler32;
|
||||||
|
|
||||||
public final class Utils {
|
public final class Utils {
|
||||||
|
|
||||||
@ -321,7 +322,7 @@ public final class Utils {
|
|||||||
* This location is only for caching, do not install directly from this location
|
* This location is only for caching, do not install directly from this location
|
||||||
* because if the file is on the External Storage, any other app could swap out
|
* because if the file is on the External Storage, any other app could swap out
|
||||||
* the APK while the install was in process, allowing malware to install things.
|
* the APK while the install was in process, allowing malware to install things.
|
||||||
* Using {@link org.fdroid.fdroid.installer.Installer#installPackage(File, String)}
|
* Using {@link org.fdroid.fdroid.installer.Installer#installPackage(File, String, String)}
|
||||||
* is fine since that does the right thing.
|
* is fine since that does the right thing.
|
||||||
*/
|
*/
|
||||||
public static SanitizedFile getApkCacheDir(Context context) {
|
public static SanitizedFile getApkCacheDir(Context context) {
|
||||||
@ -412,6 +413,15 @@ public final class Utils {
|
|||||||
return repoAddress + "/" + apk.apkName.replace(" ", "%20");
|
return repoAddress + "/" + apk.apkName.replace(" ", "%20");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This generates a unique, reproducible ID for notifications related to {@code urlString}
|
||||||
|
*/
|
||||||
|
public static int getApkUrlNotificationId(String urlString) {
|
||||||
|
Adler32 checksum = new Adler32();
|
||||||
|
checksum.update(urlString.getBytes());
|
||||||
|
return (int) checksum.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
public static final class CommaSeparatedList implements Iterable<String> {
|
public static final class CommaSeparatedList implements Iterable<String> {
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
package org.fdroid.fdroid.installer;
|
package org.fdroid.fdroid.installer;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
@ -170,7 +171,8 @@ public abstract class Installer {
|
|||||||
/**
|
/**
|
||||||
* This is the safe, single point of entry for submitting an APK file to be installed.
|
* This is the safe, single point of entry for submitting an APK file to be installed.
|
||||||
*/
|
*/
|
||||||
public void installPackage(File apkFile, String packageName) throws InstallFailedException {
|
public void installPackage(File apkFile, String packageName, String urlString)
|
||||||
|
throws InstallFailedException {
|
||||||
SanitizedFile apkToInstall = null;
|
SanitizedFile apkToInstall = null;
|
||||||
try {
|
try {
|
||||||
Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath());
|
Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath());
|
||||||
@ -228,6 +230,9 @@ public abstract class Installer {
|
|||||||
FileCompat.setReadable(apkToInstall, true, false);
|
FileCompat.setReadable(apkToInstall, true, false);
|
||||||
installPackageInternal(apkToInstall);
|
installPackageInternal(apkToInstall);
|
||||||
|
|
||||||
|
NotificationManager nm = (NotificationManager)
|
||||||
|
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
nm.cancel(Utils.getApkUrlNotificationId(urlString));
|
||||||
} catch (NumberFormatException | NoSuchAlgorithmException | IOException e) {
|
} catch (NumberFormatException | NoSuchAlgorithmException | IOException e) {
|
||||||
throw new InstallFailedException(e);
|
throw new InstallFailedException(e);
|
||||||
} catch (ClassCastException e) {
|
} catch (ClassCastException e) {
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Process;
|
||||||
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
import android.support.v4.app.TaskStackBuilder;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.AppDetails;
|
||||||
|
import org.fdroid.fdroid.R;
|
||||||
|
import org.fdroid.fdroid.Utils;
|
||||||
|
import org.fdroid.fdroid.data.App;
|
||||||
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
|
|
||||||
|
public class DownloadCompleteService extends IntentService {
|
||||||
|
private static final String TAG = "DownloadCompleteService";
|
||||||
|
|
||||||
|
private static final String ACTION_NOTIFY = "org.fdroid.fdroid.net.action.NOTIFY";
|
||||||
|
private static final String EXTRA_PACKAGE_NAME = "org.fdroid.fdroid.net.extra.PACKAGE_NAME";
|
||||||
|
|
||||||
|
public DownloadCompleteService() {
|
||||||
|
super("DownloadCompleteService");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void notify(Context context, String packageName, String urlString) {
|
||||||
|
Intent intent = new Intent(context, DownloadCompleteService.class);
|
||||||
|
intent.setAction(ACTION_NOTIFY);
|
||||||
|
intent.setData(Uri.parse(urlString));
|
||||||
|
intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
|
||||||
|
if (intent != null) {
|
||||||
|
final String action = intent.getAction();
|
||||||
|
if (!ACTION_NOTIFY.equals(action)) {
|
||||||
|
Utils.debugLog(TAG, "intent action is not ACTION_NOTIFY");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
|
||||||
|
if (TextUtils.isEmpty(packageName)) {
|
||||||
|
Utils.debugLog(TAG, "intent is missing EXTRA_PACKAGE_NAME");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String title;
|
||||||
|
try {
|
||||||
|
PackageManager pm = getPackageManager();
|
||||||
|
title = String.format(getString(R.string.tap_to_update_format),
|
||||||
|
pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)));
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
App app = AppProvider.Helper.findByPackageName(getContentResolver(), packageName,
|
||||||
|
new String[]{
|
||||||
|
AppProvider.DataColumns.NAME,
|
||||||
|
});
|
||||||
|
title = String.format(getString(R.string.tap_to_install_format), app.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent notifyIntent = new Intent(this, AppDetails.class);
|
||||||
|
notifyIntent.putExtra(AppDetails.EXTRA_APPID, packageName);
|
||||||
|
TaskStackBuilder stackBuilder = TaskStackBuilder
|
||||||
|
.create(this)
|
||||||
|
.addParentStack(AppDetails.class)
|
||||||
|
.addNextIntent(notifyIntent);
|
||||||
|
int requestCode = Utils.getApkUrlNotificationId(intent.getDataString());
|
||||||
|
PendingIntent pendingIntent = stackBuilder.getPendingIntent(requestCode,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
NotificationCompat.Builder builder =
|
||||||
|
new NotificationCompat.Builder(this)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setContentText(getString(R.string.tap_to_install));
|
||||||
|
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||||
|
nm.notify(Utils.getApkUrlNotificationId(intent.getDataString()), builder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,8 @@ import java.util.HashMap;
|
|||||||
public class DownloaderService extends Service {
|
public class DownloaderService extends Service {
|
||||||
public static final String TAG = "DownloaderService";
|
public static final String TAG = "DownloaderService";
|
||||||
|
|
||||||
|
static final String EXTRA_PACKAGE_NAME = "org.fdroid.fdroid.net.DownloaderService.extra.PACKAGE_NAME";
|
||||||
|
|
||||||
private static final String ACTION_QUEUE = "org.fdroid.fdroid.net.DownloaderService.action.QUEUE";
|
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";
|
private static final String ACTION_CANCEL = "org.fdroid.fdroid.net.DownloaderService.action.CANCEL";
|
||||||
|
|
||||||
@ -216,6 +218,8 @@ public class DownloaderService extends Service {
|
|||||||
});
|
});
|
||||||
downloader.download();
|
downloader.download();
|
||||||
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile);
|
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile);
|
||||||
|
DownloadCompleteService.notify(this, intent.getStringExtra(EXTRA_PACKAGE_NAME),
|
||||||
|
intent.getDataString());
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile);
|
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -250,14 +254,18 @@ public class DownloaderService extends Service {
|
|||||||
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
|
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
|
||||||
*
|
*
|
||||||
* @param context
|
* @param context
|
||||||
* @param urlString The URL to add to the download queue
|
* @param packageName The packageName of the app being downloaded
|
||||||
|
* @param urlString The URL to add to the download queue
|
||||||
* @see #cancel(Context, String)
|
* @see #cancel(Context, String)
|
||||||
*/
|
*/
|
||||||
public static void queue(Context context, String urlString) {
|
public static void queue(Context context, String packageName, String urlString) {
|
||||||
Log.i(TAG, "queue " + urlString);
|
Log.i(TAG, "queue " + urlString);
|
||||||
Intent intent = new Intent(context, DownloaderService.class);
|
Intent intent = new Intent(context, DownloaderService.class);
|
||||||
intent.setAction(ACTION_QUEUE);
|
intent.setAction(ACTION_QUEUE);
|
||||||
intent.setData(Uri.parse(urlString));
|
intent.setData(Uri.parse(urlString));
|
||||||
|
if (!TextUtils.isEmpty(EXTRA_PACKAGE_NAME)) {
|
||||||
|
intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
|
||||||
|
}
|
||||||
context.startService(intent);
|
context.startService(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,7 +276,7 @@ public class DownloaderService extends Service {
|
|||||||
*
|
*
|
||||||
* @param context
|
* @param context
|
||||||
* @param urlString The URL to remove from the download queue
|
* @param urlString The URL to remove from the download queue
|
||||||
* @see #queue(Context, String)
|
* @see #queue(Context, String, String)
|
||||||
*/
|
*/
|
||||||
public static void cancel(Context context, String urlString) {
|
public static void cancel(Context context, String urlString) {
|
||||||
Log.i(TAG, "cancel " + urlString);
|
Log.i(TAG, "cancel " + urlString);
|
||||||
|
@ -788,15 +788,15 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
String path = intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH);
|
String path = intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH);
|
||||||
handleDownloadComplete(new File(path), app.packageName);
|
handleDownloadComplete(new File(path), app.packageName, intent.getDataString());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
localBroadcastManager.registerReceiver(downloadCompleteReceiver,
|
localBroadcastManager.registerReceiver(downloadCompleteReceiver,
|
||||||
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE));
|
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE));
|
||||||
DownloaderService.queue(this, urlString);
|
DownloaderService.queue(this, app.packageName, urlString);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDownloadComplete(File apkFile, String packageName) {
|
private void handleDownloadComplete(File apkFile, String packageName, String urlString) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Installer.getActivityInstaller(this, new Installer.InstallerCallback() {
|
Installer.getActivityInstaller(this, new Installer.InstallerCallback() {
|
||||||
@ -811,7 +811,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
|||||||
public void onError(int operation, int errorCode) {
|
public void onError(int operation, int errorCode) {
|
||||||
// TODO: Boo!
|
// TODO: Boo!
|
||||||
}
|
}
|
||||||
}).installPackage(apkFile, packageName);
|
}).installPackage(apkFile, packageName, urlString);
|
||||||
localBroadcastManager.unregisterReceiver(downloadCompleteReceiver);
|
localBroadcastManager.unregisterReceiver(downloadCompleteReceiver);
|
||||||
} catch (Installer.InstallFailedException e) {
|
} catch (Installer.InstallFailedException e) {
|
||||||
// TODO: Handle exception properly
|
// TODO: Handle exception properly
|
||||||
|
@ -341,6 +341,8 @@
|
|||||||
<string name="swap_not_enabled">Swapping not enabled</string>
|
<string name="swap_not_enabled">Swapping not enabled</string>
|
||||||
<string name="swap_not_enabled_description">Before swapping, your device must be made visible.</string>
|
<string name="swap_not_enabled_description">Before swapping, your device must be made visible.</string>
|
||||||
|
|
||||||
|
<string name="tap_to_install_format">Tap to install %s</string>
|
||||||
|
<string name="tap_to_update_format">Tap to update %s</string>
|
||||||
<string name="install_confirm">Do you want to install this application?
|
<string name="install_confirm">Do you want to install this application?
|
||||||
It will get access to:</string>
|
It will get access to:</string>
|
||||||
<string name="install_confirm_no_perms">Do you want to install this application?
|
<string name="install_confirm_no_perms">Do you want to install this application?
|
||||||
|
Loading…
x
Reference in New Issue
Block a user