Merge branch 'InstallManagerService' into 'master'
InstallManagerService This provides an over-arching `Service` for managing the whole install process, from checking the cache, downloading files, handling the notification. Ultimately, it should probably also handle starting and tracking progress of the final installation steps. Note: this does undo some of the `Notification` handling stuff, putting it back to one notification per APK. I did that to get that part working OK for the short term, giving us time to figure out what the whole picture should look like. I think @pserwylo has it pretty well sketched out in #592. But I have no strong feelings about the notification stuff for 0.100, so I'm happy to shape this MR accordingly, provided its only a little work. See merge request !278
This commit is contained in:
commit
7389315dfa
@ -30,7 +30,7 @@ import java.util.UUID;
|
||||
|
||||
@SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics
|
||||
public class MultiRepoUpdaterTest extends InstrumentationTestCase {
|
||||
private static final String TAG = "RepoUpdaterTest";
|
||||
private static final String TAG = "MultiRepoUpdaterTest";
|
||||
|
||||
private static final String REPO_MAIN = "Test F-Droid repo";
|
||||
private static final String REPO_ARCHIVE = "Test F-Droid repo (Archive)";
|
||||
@ -406,7 +406,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase {
|
||||
private RepoUpdater createUpdater(String name, Context context) {
|
||||
Repo repo = new Repo();
|
||||
repo.signingCertificate = PUB_KEY;
|
||||
repo.address = UUID.randomUUID().toString();
|
||||
repo.address = "https://fake.url/" + UUID.randomUUID().toString() + "/fdroid/repo";
|
||||
repo.name = name;
|
||||
|
||||
ContentValues values = new ContentValues(2);
|
||||
|
@ -31,6 +31,7 @@ public class RepoUpdaterTest {
|
||||
context = instrumentation.getContext();
|
||||
testFilesDir = TestUtils.getWriteableDir(instrumentation);
|
||||
Repo repo = new Repo();
|
||||
repo.address = "https://fake.url/fdroid/repo";
|
||||
repo.signingCertificate = this.simpleIndexSigningCert;
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
}
|
||||
|
@ -447,6 +447,9 @@
|
||||
android:exported="false" />
|
||||
<service android:name=".net.WifiStateChangeService" />
|
||||
<service android:name=".localrepo.SwapService" />
|
||||
<service
|
||||
android:name=".installer.InstallManagerService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -37,7 +37,6 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.NavUtils;
|
||||
@ -86,8 +85,8 @@ import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.installer.InstallManagerService;
|
||||
import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.installer.Installer.InstallFailedException;
|
||||
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
|
||||
@ -111,6 +110,15 @@ public class AppDetails extends AppCompatActivity {
|
||||
private FDroidApp fdroidApp;
|
||||
private ApkListAdapter adapter;
|
||||
|
||||
/**
|
||||
* Check if {@code packageName} is currently visible to the user.
|
||||
*/
|
||||
public static boolean isAppVisible(String packageName) {
|
||||
return packageName != null && packageName.equals(visiblePackageName);
|
||||
}
|
||||
|
||||
private static String visiblePackageName;
|
||||
|
||||
private static class ViewHolder {
|
||||
TextView version;
|
||||
TextView status;
|
||||
@ -426,13 +434,19 @@ public class AppDetails extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onResumeFragments() {
|
||||
// Must be called before super.onResumeFragments(), as the fragments depend on the active
|
||||
// url being correctly set in order to know whether or not to show the download progress bar.
|
||||
calcActiveDownloadUrlString(app.packageName);
|
||||
|
||||
super.onResumeFragments();
|
||||
|
||||
headerFragment = (AppDetailsHeaderFragment) getSupportFragmentManager().findFragmentById(R.id.header);
|
||||
refreshApkList();
|
||||
supportInvalidateOptionsMenu();
|
||||
if (DownloaderService.isQueuedOrActive(activeDownloadUrlString)) {
|
||||
registerDownloaderReceivers();
|
||||
}
|
||||
visiblePackageName = app.packageName;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -454,6 +468,7 @@ public class AppDetails extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
visiblePackageName = null;
|
||||
// save the active URL for this app in case we come back
|
||||
PreferencesCompat.apply(getPreferences(MODE_PRIVATE)
|
||||
.edit()
|
||||
@ -582,13 +597,7 @@ public class AppDetails extends AppCompatActivity {
|
||||
Utils.debugLog(TAG, "Getting application details for " + packageName);
|
||||
App newApp = null;
|
||||
|
||||
String urlString = getPreferences(MODE_PRIVATE).getString(packageName, null);
|
||||
if (DownloaderService.isQueuedOrActive(urlString)) {
|
||||
activeDownloadUrlString = urlString;
|
||||
} else {
|
||||
// this URL is no longer active, remove it
|
||||
PreferencesCompat.apply(getPreferences(MODE_PRIVATE).edit().remove(packageName));
|
||||
}
|
||||
calcActiveDownloadUrlString(packageName);
|
||||
|
||||
if (!TextUtils.isEmpty(packageName)) {
|
||||
newApp = AppProvider.Helper.findByPackageName(getContentResolver(), packageName);
|
||||
@ -599,6 +608,16 @@ public class AppDetails extends AppCompatActivity {
|
||||
return this.app != null;
|
||||
}
|
||||
|
||||
private void calcActiveDownloadUrlString(String packageName) {
|
||||
String urlString = getPreferences(MODE_PRIVATE).getString(packageName, null);
|
||||
if (DownloaderService.isQueuedOrActive(urlString)) {
|
||||
activeDownloadUrlString = urlString;
|
||||
} else {
|
||||
// this URL is no longer active, remove it
|
||||
PreferencesCompat.apply(getPreferences(MODE_PRIVATE).edit().remove(packageName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If passed null, this will show a message to the user ("Could not find app ..." or something
|
||||
* like that) and then finish the activity.
|
||||
@ -815,9 +834,6 @@ public class AppDetails extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
final String repoaddress = getRepoAddress(apk);
|
||||
if (repoaddress == null) return;
|
||||
|
||||
if (!apk.compatible) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(R.string.installIncompatible);
|
||||
@ -826,7 +842,7 @@ public class AppDetails extends AppCompatActivity {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog,
|
||||
int whichButton) {
|
||||
startDownload(apk, repoaddress);
|
||||
startDownload(apk);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.no,
|
||||
@ -855,24 +871,14 @@ public class AppDetails extends AppCompatActivity {
|
||||
alert.show();
|
||||
return;
|
||||
}
|
||||
startDownload(apk, repoaddress);
|
||||
startDownload(apk);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getRepoAddress(Apk apk) {
|
||||
final String[] projection = {RepoProvider.DataColumns.ADDRESS};
|
||||
Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
|
||||
if (repo == null || repo.address == null) {
|
||||
return null;
|
||||
}
|
||||
return repo.address;
|
||||
}
|
||||
|
||||
private void startDownload(Apk apk, String repoAddress) {
|
||||
activeDownloadUrlString = Utils.getApkUrl(repoAddress, apk);
|
||||
private void startDownload(Apk apk) {
|
||||
activeDownloadUrlString = apk.getUrl();
|
||||
registerDownloaderReceivers();
|
||||
headerFragment.startProgress();
|
||||
DownloaderService.queue(this, apk.packageName, activeDownloadUrlString);
|
||||
InstallManagerService.queue(this, app, apk);
|
||||
}
|
||||
|
||||
private void removeApk(String packageName) {
|
||||
|
@ -59,10 +59,13 @@ public class CleanCacheService extends IntentService {
|
||||
* Delete index files which were downloaded, but not removed (e.g. due to F-Droid being
|
||||
* force closed during processing of the file, before getting a chance to delete). This
|
||||
* may include both "index-*-downloaded" and "index-*-extracted.xml" files.
|
||||
* <p/>
|
||||
* <p>
|
||||
* Note that if the SD card is not ready, then the cache directory will probably not be
|
||||
* available. In this situation no files will be deleted (and thus they may still exist
|
||||
* after the SD card becomes available).
|
||||
* <p>
|
||||
* This also deletes temp files that are created by
|
||||
* {@link org.fdroid.fdroid.net.DownloaderFactory#create(Context, String)}, e.g. "dl-*"
|
||||
*/
|
||||
private void deleteStrayIndexFiles() {
|
||||
File cacheDir = getCacheDir();
|
||||
@ -79,6 +82,9 @@ public class CleanCacheService extends IntentService {
|
||||
if (f.getName().startsWith("index-")) {
|
||||
FileUtils.deleteQuietly(f);
|
||||
}
|
||||
if (f.getName().startsWith("dl-")) {
|
||||
FileUtils.deleteQuietly(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,14 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
||||
public class ProgressBufferedInputStream extends BufferedInputStream {
|
||||
|
||||
private final ProgressListener progressListener;
|
||||
private final Bundle data;
|
||||
private final URL sourceUrl;
|
||||
private final int totalBytes;
|
||||
|
||||
private int currentBytes;
|
||||
@ -20,12 +17,10 @@ public class ProgressBufferedInputStream extends BufferedInputStream {
|
||||
* Reports progress to the specified {@link ProgressListener}, with the
|
||||
* progress based on the {@code totalBytes}.
|
||||
*/
|
||||
public ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, Repo repo, int totalBytes)
|
||||
throws IOException {
|
||||
public ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, URL sourceUrl, int totalBytes) {
|
||||
super(in);
|
||||
this.progressListener = progressListener;
|
||||
this.data = new Bundle(1);
|
||||
this.data.putString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS, repo.address);
|
||||
this.sourceUrl = sourceUrl;
|
||||
this.totalBytes = totalBytes;
|
||||
}
|
||||
|
||||
@ -37,10 +32,7 @@ public class ProgressBufferedInputStream extends BufferedInputStream {
|
||||
* the digits changing because it looks pretty, < 9000 since the reads won't
|
||||
* line up exactly */
|
||||
if (currentBytes % 333333 < 9000) {
|
||||
progressListener.onProgress(
|
||||
new ProgressListener.Event(
|
||||
RepoUpdater.PROGRESS_TYPE_PROCESS_XML,
|
||||
currentBytes, totalBytes, data));
|
||||
progressListener.onProgress(sourceUrl, currentBytes, totalBytes);
|
||||
}
|
||||
}
|
||||
return super.read(buffer, byteOffset, byteCount);
|
||||
|
@ -1,76 +1,15 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* This is meant only to send download progress for any URL (e.g. index
|
||||
* updates, APKs, etc). This also keeps this class pure Java so that classes
|
||||
* that use {@code ProgressListener} can be tested on the JVM, without requiring
|
||||
* an Android device or emulator.
|
||||
*/
|
||||
public interface ProgressListener {
|
||||
|
||||
void onProgress(Event event);
|
||||
|
||||
// I went a bit overboard with the overloaded constructors, but they all
|
||||
// seemed potentially useful and unambiguous, so I just put them in there
|
||||
// while I'm here.
|
||||
class Event implements Parcelable {
|
||||
|
||||
public static final int NO_VALUE = Integer.MIN_VALUE;
|
||||
|
||||
public final String type;
|
||||
public final Bundle data;
|
||||
|
||||
// These two are not final, so that you can create a template Event,
|
||||
// pass it into a function which performs something over time, and
|
||||
// that function can initialize "total" and progressively
|
||||
// update "progress"
|
||||
public int progress;
|
||||
public final int total;
|
||||
|
||||
public Event(String type) {
|
||||
this(type, NO_VALUE, NO_VALUE, null);
|
||||
}
|
||||
|
||||
public Event(String type, int progress, int total, Bundle data) {
|
||||
this.type = type;
|
||||
this.progress = progress;
|
||||
this.total = total;
|
||||
this.data = (data == null) ? new Bundle() : data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(type);
|
||||
dest.writeInt(progress);
|
||||
dest.writeInt(total);
|
||||
dest.writeBundle(data);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
|
||||
@Override
|
||||
public Event createFromParcel(Parcel in) {
|
||||
return new Event(in.readString(), in.readInt(), in.readInt(), in.readBundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Event[] newArray(int size) {
|
||||
return new Event[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Can help to provide context to the listener about what process is causing the event.
|
||||
* For example, the repo updater uses one listener to listen to multiple downloaders.
|
||||
* When it receives an event, it doesn't know which repo download is causing the event,
|
||||
* so we pass that through to the downloader when we set the progress listener. This way,
|
||||
* we can ask the event for the name of the repo.
|
||||
*/
|
||||
public Bundle getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
void onProgress(URL sourceUrl, int bytesRead, int totalBytes);
|
||||
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ import org.xml.sax.XMLReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.security.CodeSigner;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
@ -48,10 +50,6 @@ public class RepoUpdater {
|
||||
|
||||
private static final String TAG = "RepoUpdater";
|
||||
|
||||
public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml";
|
||||
public static final String PROGRESS_COMMITTING = "committing";
|
||||
public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress";
|
||||
|
||||
public final String indexUrl;
|
||||
|
||||
@NonNull
|
||||
@ -60,7 +58,8 @@ public class RepoUpdater {
|
||||
private final Repo repo;
|
||||
private boolean hasChanged;
|
||||
@Nullable
|
||||
private ProgressListener progressListener;
|
||||
private ProgressListener committingProgressListener;
|
||||
private ProgressListener processXmlProgressListener;
|
||||
private String cacheTag;
|
||||
private X509Certificate signingCertFromJar;
|
||||
|
||||
@ -84,8 +83,12 @@ public class RepoUpdater {
|
||||
this.indexUrl = url;
|
||||
}
|
||||
|
||||
public void setProgressListener(@Nullable ProgressListener progressListener) {
|
||||
this.progressListener = progressListener;
|
||||
public void setProcessXmlProgressListener(ProgressListener progressListener) {
|
||||
this.processXmlProgressListener = progressListener;
|
||||
}
|
||||
|
||||
public void setCommittingProgressListener(ProgressListener progressListener) {
|
||||
this.committingProgressListener = progressListener;
|
||||
}
|
||||
|
||||
public boolean hasChanged() {
|
||||
@ -177,7 +180,7 @@ public class RepoUpdater {
|
||||
JarFile jarFile = new JarFile(downloadedFile, true);
|
||||
JarEntry indexEntry = (JarEntry) jarFile.getEntry("index.xml");
|
||||
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
|
||||
progressListener, repo, (int) indexEntry.getSize());
|
||||
processXmlProgressListener, new URL(repo.address), (int) indexEntry.getSize());
|
||||
|
||||
// Process the index...
|
||||
final SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
|
||||
@ -207,8 +210,13 @@ public class RepoUpdater {
|
||||
|
||||
private void commitToDb() throws UpdateException {
|
||||
Log.i(TAG, "Repo signature verified, saving app metadata to database.");
|
||||
if (progressListener != null) {
|
||||
progressListener.onProgress(new ProgressListener.Event(PROGRESS_COMMITTING));
|
||||
if (committingProgressListener != null) {
|
||||
try {
|
||||
//TODO this should be an event, not a progress listener
|
||||
committingProgressListener.onProgress(new URL(indexUrl), 0, -1);
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
persister.commit(repoDetailsToSave);
|
||||
}
|
||||
|
@ -48,13 +48,15 @@ import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.installer.InstallManagerService;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class UpdateService extends IntentService implements ProgressListener {
|
||||
public class UpdateService extends IntentService {
|
||||
|
||||
private static final String TAG = "UpdateService";
|
||||
|
||||
@ -375,7 +377,8 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
RepoUpdater updater = new RepoUpdater(getBaseContext(), repo);
|
||||
localBroadcastManager.registerReceiver(downloadProgressReceiver,
|
||||
DownloaderService.getIntentFilter(updater.indexUrl, Downloader.ACTION_PROGRESS));
|
||||
updater.setProgressListener(this);
|
||||
updater.setProcessXmlProgressListener(processXmlProgressListener);
|
||||
updater.setCommittingProgressListener(committingProgressListener);
|
||||
try {
|
||||
updater.update();
|
||||
if (updater.hasChanged()) {
|
||||
@ -393,7 +396,7 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
|
||||
// now that downloading the index is done, start downloading updates
|
||||
if (changes && fdroidPrefs.isAutoDownloadEnabled()) {
|
||||
autoDownloadUpdates(repo.address);
|
||||
autoDownloadUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
@ -485,7 +488,7 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
return inboxStyle;
|
||||
}
|
||||
|
||||
private void autoDownloadUpdates(String repoAddress) {
|
||||
private void autoDownloadUpdates() {
|
||||
Cursor cursor = getContentResolver().query(
|
||||
AppProvider.getCanUpdateUri(),
|
||||
new String[]{
|
||||
@ -496,11 +499,8 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
cursor.moveToFirst();
|
||||
for (int i = 0; i < cursor.getCount(); i++) {
|
||||
App app = new App(cursor);
|
||||
Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode, new String[]{
|
||||
ApkProvider.DataColumns.NAME,
|
||||
});
|
||||
String urlString = Utils.getApkUrl(repoAddress, apk);
|
||||
DownloaderService.queue(this, app.packageName, urlString);
|
||||
Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode);
|
||||
InstallManagerService.queue(this, app, apk);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
cursor.close();
|
||||
@ -528,25 +528,25 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
notificationManager.notify(NOTIFY_ID_UPDATES_AVAILABLE, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Received progress event from the RepoXMLHandler. It could be progress
|
||||
* downloading from the repo, or perhaps processing the info from the repo.
|
||||
*/
|
||||
@Override
|
||||
public void onProgress(ProgressListener.Event event) {
|
||||
String message = "";
|
||||
String repoAddress = event.getData().getString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS);
|
||||
String downloadedSize = Utils.getFriendlySize(event.progress);
|
||||
String totalSize = Utils.getFriendlySize(event.total);
|
||||
int percent = event.total > 0 ? (int) ((double) event.progress / event.total * 100) : -1;
|
||||
switch (event.type) {
|
||||
case RepoUpdater.PROGRESS_TYPE_PROCESS_XML:
|
||||
message = getString(R.string.status_processing_xml_percent, repoAddress, downloadedSize, totalSize, percent);
|
||||
break;
|
||||
case RepoUpdater.PROGRESS_COMMITTING:
|
||||
message = getString(R.string.status_inserting_apps);
|
||||
break;
|
||||
private final ProgressListener processXmlProgressListener = new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
||||
String downloadedSize = Utils.getFriendlySize(bytesRead);
|
||||
String totalSize = Utils.getFriendlySize(totalBytes);
|
||||
int percent = -1;
|
||||
if (totalBytes > 0) {
|
||||
percent = (int) ((double) bytesRead / totalBytes * 100);
|
||||
}
|
||||
String message = getString(R.string.status_processing_xml_percent, sourceUrl, downloadedSize, totalSize, percent);
|
||||
sendStatus(getApplicationContext(), STATUS_INFO, message, percent);
|
||||
}
|
||||
sendStatus(this, STATUS_INFO, message, percent);
|
||||
}
|
||||
};
|
||||
|
||||
private final ProgressListener committingProgressListener = new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
||||
String message = getString(R.string.status_inserting_apps);
|
||||
sendStatus(getApplicationContext(), STATUS_INFO, message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -39,7 +39,6 @@ import com.nostra13.universalimageloader.utils.StorageUtils;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.fdroid.fdroid.compat.FileCompat;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
import org.xml.sax.XMLReader;
|
||||
@ -116,27 +115,13 @@ public final class Utils {
|
||||
return "/icons-120/";
|
||||
}
|
||||
|
||||
public static void copy(InputStream input, OutputStream output)
|
||||
throws IOException {
|
||||
copy(input, output, null, null);
|
||||
}
|
||||
|
||||
public static void copy(InputStream input, OutputStream output,
|
||||
ProgressListener progressListener,
|
||||
ProgressListener.Event templateProgressEvent)
|
||||
throws IOException {
|
||||
public static void copy(InputStream input, OutputStream output) throws IOException {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead = 0;
|
||||
while (true) {
|
||||
int count = input.read(buffer);
|
||||
if (count == -1) {
|
||||
break;
|
||||
}
|
||||
if (progressListener != null) {
|
||||
bytesRead += count;
|
||||
templateProgressEvent.progress = bytesRead;
|
||||
progressListener.onProgress(templateProgressEvent);
|
||||
}
|
||||
output.write(buffer, 0, count);
|
||||
}
|
||||
output.flush();
|
||||
@ -335,6 +320,17 @@ public final class Utils {
|
||||
return apkCacheDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path for where an APK URL will be downloaded into.
|
||||
*/
|
||||
public static SanitizedFile getApkDownloadPath(Context context, Uri uri) {
|
||||
File dir = new File(Utils.getApkCacheDir(context), uri.getHost() + "-" + uri.getPort());
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
}
|
||||
return new SanitizedFile(dir, uri.getLastPathSegment());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete files in {@code dir} that were last modified
|
||||
* {@code secondsAgo} seconds ago, e.g. when it was downloaded.
|
||||
@ -434,10 +430,6 @@ public final class Utils {
|
||||
return new Locale(languageTag);
|
||||
}
|
||||
|
||||
public static String getApkUrl(String repoAddress, Apk apk) {
|
||||
return repoAddress + "/" + apk.apkName.replace(" ", "%20");
|
||||
}
|
||||
|
||||
public static final class CommaSeparatedList implements Iterable<String> {
|
||||
private final String value;
|
||||
|
||||
|
@ -121,6 +121,13 @@ public class Apk extends ValueObject implements Comparable<Apk> {
|
||||
}
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
if (repoAddress == null || apkName == null) {
|
||||
throw new IllegalStateException("Apk needs to have both ApkProvider.DataColumns.REPO_ADDRESS and ApkProvider.DataColumns.NAME set in order to calculate URL.");
|
||||
}
|
||||
return repoAddress + "/" + apkName.replace(" ", "%20");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return packageName + " (version " + versionCode + ")";
|
||||
|
@ -0,0 +1,344 @@
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.fdroid.fdroid.AppDetails;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* <p>
|
||||
* Data is sent via {@link Intent}s so that Android handles the message queuing
|
||||
* and {@link Service} lifecycle for us, although it adds one layer of redirection
|
||||
* between the static method to send the {@code Intent} and the method to
|
||||
* actually process it.
|
||||
* <p>
|
||||
* The full 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
|
||||
* of {@link Intent#setData(Uri)}, where the core data of an {@code Intent} is
|
||||
* a {@code Uri}. The full download URL is guaranteed to be unique since it
|
||||
* points to files on a filesystem, where there cannot be multiple files with
|
||||
* the same name. This provides a unique ID beyond just {@code packageName}
|
||||
* and {@code versionCode} since there could be different copies of the same
|
||||
* APK on different servers, signed by different keys, or even different builds.
|
||||
* <p><ul>
|
||||
* <li>for a {@link Uri} ID, use {@code Uri}, {@link Intent#getData()}
|
||||
* <li>for a {@code String} ID, use {@code urlString}, {@link Uri#toString()}, or
|
||||
* {@link Intent#getDataString()}
|
||||
* <li>for an {@code int} ID, use {@link String#hashCode()}
|
||||
* </ul></p>
|
||||
*/
|
||||
public class InstallManagerService extends Service {
|
||||
public static final String TAG = "InstallManagerService";
|
||||
|
||||
private static final String ACTION_INSTALL = "org.fdroid.fdroid.InstallManagerService.action.INSTALL";
|
||||
|
||||
/**
|
||||
* The collection of {@link Apk}s that are actively going through this whole process,
|
||||
* matching the {@link App}s in {@code ACTIVE_APPS}. The key is the download URL, as
|
||||
* in {@link Apk#getUrl()} or {@code urlString}.
|
||||
*/
|
||||
private static final HashMap<String, Apk> ACTIVE_APKS = new HashMap<String, Apk>(3);
|
||||
|
||||
/**
|
||||
* The collection of {@link App}s that are actively going through this whole process,
|
||||
* matching the {@link Apk}s in {@code ACTIVE_APKS}. The key is the
|
||||
* {@code packageName} of the app.
|
||||
*/
|
||||
private static final HashMap<String, App> ACTIVE_APPS = new HashMap<String, App>(3);
|
||||
|
||||
/**
|
||||
* The array of active {@link BroadcastReceiver}s for each active APK. The key is the
|
||||
* download URL, as in {@link Apk#getUrl()} or {@code urlString}.
|
||||
*/
|
||||
private final HashMap<String, BroadcastReceiver[]> receivers = new HashMap<String, BroadcastReceiver[]>(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.
|
||||
* <p>
|
||||
* TODO <b>delete me once InstallerService exists</b>
|
||||
*/
|
||||
private static final HashMap<String, String> TEMP_HACK_APP_NAMES = new HashMap<String, String>(3);
|
||||
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
/**
|
||||
* This service does not use binding, so no need to implement this method
|
||||
*/
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Utils.debugLog(TAG, "creating Service");
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
BroadcastReceiver br = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String packageName = intent.getData().getSchemeSpecificPart();
|
||||
for (Map.Entry<String, Apk> entry : ACTIVE_APKS.entrySet()) {
|
||||
if (TextUtils.equals(packageName, entry.getValue().packageName)) {
|
||||
String urlString = entry.getKey();
|
||||
cancelNotification(urlString);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
|
||||
intentFilter.addDataScheme("package");
|
||||
registerReceiver(br, intentFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Utils.debugLog(TAG, "onStartCommand " + intent);
|
||||
String urlString = intent.getDataString();
|
||||
Apk apk = ACTIVE_APKS.get(urlString);
|
||||
|
||||
Notification notification = createNotification(intent.getDataString(), apk).build();
|
||||
notificationManager.notify(urlString.hashCode(), notification);
|
||||
|
||||
registerDownloaderReceivers(urlString);
|
||||
|
||||
File apkFilePath = Utils.getApkDownloadPath(this, intent.getData());
|
||||
long apkFileSize = apkFilePath.length();
|
||||
if (!apkFilePath.exists() || apkFileSize < apk.size) {
|
||||
Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath);
|
||||
DownloaderService.queue(this, urlString);
|
||||
} else if (apkFileSize == apk.size) {
|
||||
Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath);
|
||||
sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath);
|
||||
sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath);
|
||||
} else {
|
||||
Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath);
|
||||
apkFilePath.delete();
|
||||
DownloaderService.queue(this, urlString);
|
||||
}
|
||||
return START_REDELIVER_INTENT; // if killed before completion, retry Intent
|
||||
}
|
||||
|
||||
private void sendBroadcast(Uri uri, String action, File file) {
|
||||
Intent intent = new Intent(action);
|
||||
intent.setData(uri);
|
||||
intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath());
|
||||
localBroadcastManager.sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private void unregisterDownloaderReceivers(String urlString) {
|
||||
for (BroadcastReceiver receiver : receivers.get(urlString)) {
|
||||
localBroadcastManager.unregisterReceiver(receiver);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerDownloaderReceivers(String urlString) {
|
||||
BroadcastReceiver startedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
}
|
||||
};
|
||||
BroadcastReceiver progressReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String urlString = intent.getDataString();
|
||||
Apk apk = ACTIVE_APKS.get(urlString);
|
||||
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
|
||||
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
|
||||
Notification notification = createNotification(urlString, apk)
|
||||
.setProgress(totalBytes, bytesRead, false)
|
||||
.build();
|
||||
notificationManager.notify(urlString.hashCode(), notification);
|
||||
}
|
||||
};
|
||||
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 = ACTIVE_APKS.remove(urlString);
|
||||
ACTIVE_APPS.remove(apk.packageName);
|
||||
if (AppDetails.isAppVisible(apk.packageName)) {
|
||||
cancelNotification(urlString);
|
||||
} else {
|
||||
notifyDownloadComplete(apk, urlString);
|
||||
}
|
||||
unregisterDownloaderReceivers(urlString);
|
||||
}
|
||||
};
|
||||
BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String urlString = intent.getDataString();
|
||||
Apk apk = ACTIVE_APKS.remove(urlString);
|
||||
ACTIVE_APPS.remove(apk.packageName);
|
||||
unregisterDownloaderReceivers(urlString);
|
||||
if (AppDetails.isAppVisible(apk.packageName)) {
|
||||
cancelNotification(urlString);
|
||||
}
|
||||
}
|
||||
};
|
||||
localBroadcastManager.registerReceiver(startedReceiver,
|
||||
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_STARTED));
|
||||
localBroadcastManager.registerReceiver(progressReceiver,
|
||||
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_PROGRESS));
|
||||
localBroadcastManager.registerReceiver(completeReceiver,
|
||||
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE));
|
||||
localBroadcastManager.registerReceiver(interruptedReceiver,
|
||||
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_INTERRUPTED));
|
||||
receivers.put(urlString, new BroadcastReceiver[]{
|
||||
startedReceiver, progressReceiver, completeReceiver, interruptedReceiver,
|
||||
});
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createNotification(String urlString, Apk apk) {
|
||||
int downloadUrlId = urlString.hashCode();
|
||||
return new NotificationCompat.Builder(this)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
|
||||
.setContentTitle(getString(R.string.downloading_apk, getAppName(urlString, apk)))
|
||||
.addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
|
||||
DownloaderService.getCancelPendingIntent(this, urlString))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentText(urlString)
|
||||
.setProgress(100, 0, true);
|
||||
}
|
||||
|
||||
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 getString(R.string.downloading_apk, TEMP_HACK_APP_NAMES.get(urlString));
|
||||
} else {
|
||||
// this is ugly, but its better than nothing as a failsafe
|
||||
return getString(R.string.downloading_apk, urlString);
|
||||
}
|
||||
} else {
|
||||
return getString(R.string.downloading_apk, app.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link PendingIntent} for a {@link Notification} to send when it
|
||||
* is clicked. {@link AppDetails} handles {@code Intent}s that are missing
|
||||
* or bad {@link AppDetails#EXTRA_APPID}, so it does not need to be checked
|
||||
* here.
|
||||
*/
|
||||
private PendingIntent getAppDetailsIntent(int requestCode, Apk apk) {
|
||||
Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class)
|
||||
.putExtra(AppDetails.EXTRA_APPID, apk.packageName);
|
||||
return TaskStackBuilder.create(getApplicationContext())
|
||||
.addParentStack(AppDetails.class)
|
||||
.addNextIntent(notifyIntent)
|
||||
.getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post a notification about a completed download. {@code packageName} must be a valid
|
||||
* and currently in the app index database.
|
||||
*/
|
||||
private void notifyDownloadComplete(Apk apk, String urlString) {
|
||||
String title;
|
||||
try {
|
||||
PackageManager pm = getPackageManager();
|
||||
title = String.format(getString(R.string.tap_to_update_format),
|
||||
pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0)));
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
title = String.format(getString(R.string.tap_to_install_format), getAppName(urlString, apk));
|
||||
}
|
||||
|
||||
int downloadUrlId = urlString.hashCode();
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(this)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(title)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
|
||||
.setContentText(getString(R.string.tap_to_install));
|
||||
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
nm.notify(downloadUrlId, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the {@link Notification} tied to {@code urlString}, which is the
|
||||
* unique ID used to represent a given APK file. {@link String#hashCode()}
|
||||
* converts {@code urlString} to the required {@code int}.
|
||||
*/
|
||||
private void cancelNotification(String urlString) {
|
||||
notificationManager.cancel(urlString.hashCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an APK, checking the cache and downloading if necessary before starting the process.
|
||||
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
|
||||
*
|
||||
* @param context this app's {@link Context}
|
||||
*/
|
||||
public static void queue(Context context, App app, Apk apk) {
|
||||
String urlString = apk.getUrl();
|
||||
Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + urlString);
|
||||
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
|
||||
Intent intent = new Intent(context, InstallManagerService.class);
|
||||
intent.setAction(ACTION_INSTALL);
|
||||
intent.setData(Uri.parse(urlString));
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Set} of the {@code urlString}s that are currently active.
|
||||
* {@code urlString}s are used as unique IDs throughout the
|
||||
* {@code InstallManagerService} process, either as a {@code String} or as an
|
||||
* {@code int} from {@link String#hashCode()}.
|
||||
*/
|
||||
public static Set<String> getActiveDownloadUrls() {
|
||||
return ACTIVE_APKS.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Set} of the {@code packageName}s that are currently active.
|
||||
* {@code packageName}s are used as unique IDs for apps throughout all of
|
||||
* Android, F-Droid, and other apps stores.
|
||||
*/
|
||||
public static Set<String> getActivePackageNames() {
|
||||
return ACTIVE_APPS.keySet();
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.io.File;
|
||||
@ -36,19 +37,10 @@ public abstract class Downloader {
|
||||
protected final URL sourceUrl;
|
||||
protected String cacheTag;
|
||||
|
||||
/**
|
||||
* This is meant only to send progress to {@link DownloaderService}. This
|
||||
* also keeps this class pure Java so that it can be tested on the JVM,
|
||||
* without requiring an Android device or emulator.
|
||||
*/
|
||||
interface DownloaderProgressListener {
|
||||
void sendProgress(URL sourceUrl, int bytesRead, int totalBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* For sending download progress, should only be called in {@link #progressTask}
|
||||
*/
|
||||
private volatile DownloaderProgressListener downloaderProgressListener;
|
||||
private volatile ProgressListener downloaderProgressListener;
|
||||
|
||||
protected abstract InputStream getDownloadersInputStream() throws IOException;
|
||||
|
||||
@ -64,7 +56,7 @@ public abstract class Downloader {
|
||||
return new WrappedInputStream(getDownloadersInputStream());
|
||||
}
|
||||
|
||||
public void setListener(DownloaderProgressListener listener) {
|
||||
public void setListener(ProgressListener listener) {
|
||||
this.downloaderProgressListener = listener;
|
||||
}
|
||||
|
||||
@ -194,7 +186,7 @@ public abstract class Downloader {
|
||||
@Override
|
||||
public void run() {
|
||||
if (downloaderProgressListener != null) {
|
||||
downloaderProgressListener.sendProgress(sourceUrl, bytesRead, totalBytes);
|
||||
downloaderProgressListener.onProgress(sourceUrl, bytesRead, totalBytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -17,14 +17,11 @@
|
||||
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
@ -33,21 +30,12 @@ import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.PatternMatcher;
|
||||
import android.os.Process;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.AppDetails;
|
||||
import org.fdroid.fdroid.FDroid;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
|
||||
import java.io.File;
|
||||
@ -86,13 +74,9 @@ import java.net.URL;
|
||||
public class DownloaderService extends Service {
|
||||
private static final String TAG = "DownloaderService";
|
||||
|
||||
private 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_CANCEL = "org.fdroid.fdroid.net.DownloaderService.action.CANCEL";
|
||||
|
||||
private static final int NOTIFY_DOWNLOADING = 0x2344;
|
||||
|
||||
private volatile Looper serviceLooper;
|
||||
private static volatile ServiceHandler serviceHandler;
|
||||
private static volatile Downloader downloader;
|
||||
@ -137,6 +121,7 @@ public class DownloaderService extends Service {
|
||||
Integer whatToRemove = uriString.hashCode();
|
||||
if (serviceHandler.hasMessages(whatToRemove)) {
|
||||
serviceHandler.removeMessages(whatToRemove);
|
||||
sendBroadcast(intent.getData(), Downloader.ACTION_INTERRUPTED);
|
||||
} else if (isActive(uriString)) {
|
||||
downloader.cancelDownload();
|
||||
} else {
|
||||
@ -154,77 +139,27 @@ public class DownloaderService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createNotification(String urlString, @Nullable String packageName) {
|
||||
return new NotificationCompat.Builder(this)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(createAppDetailsIntent(0, packageName))
|
||||
.setContentTitle(getNotificationTitle(packageName))
|
||||
.addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
|
||||
createCancelDownloadIntent(this, 0, urlString))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentText(urlString)
|
||||
.setProgress(100, 0, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* If downloading an apk (i.e. <code>packageName != null</code>) then the title will indicate
|
||||
* the name of the app which the apk belongs to. Otherwise, it will be a generic "Downloading..."
|
||||
* message.
|
||||
*/
|
||||
private String getNotificationTitle(@Nullable String packageName) {
|
||||
if (packageName != null) {
|
||||
final App app = AppProvider.Helper.findByPackageName(
|
||||
getContentResolver(), packageName, new String[]{AppProvider.DataColumns.NAME});
|
||||
if (app != null) {
|
||||
return getString(R.string.downloading_apk, app.name);
|
||||
}
|
||||
}
|
||||
return getString(R.string.downloading);
|
||||
}
|
||||
|
||||
private PendingIntent createAppDetailsIntent(int requestCode, String packageName) {
|
||||
TaskStackBuilder stackBuilder;
|
||||
if (packageName != null) {
|
||||
Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class)
|
||||
.putExtra(AppDetails.EXTRA_APPID, packageName);
|
||||
|
||||
stackBuilder = TaskStackBuilder
|
||||
.create(getApplicationContext())
|
||||
.addParentStack(AppDetails.class)
|
||||
.addNextIntent(notifyIntent);
|
||||
} else {
|
||||
Intent notifyIntent = new Intent(getApplicationContext(), FDroid.class);
|
||||
stackBuilder = TaskStackBuilder
|
||||
.create(getApplicationContext())
|
||||
.addParentStack(FDroid.class)
|
||||
.addNextIntent(notifyIntent);
|
||||
}
|
||||
|
||||
return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
public static PendingIntent createCancelDownloadIntent(@NonNull Context context, int
|
||||
requestCode, @NonNull String urlString) {
|
||||
public static PendingIntent getCancelPendingIntent(Context context, String urlString) {
|
||||
Intent cancelIntent = new Intent(context.getApplicationContext(), DownloaderService.class)
|
||||
.setData(Uri.parse(urlString))
|
||||
.setAction(ACTION_CANCEL);
|
||||
.setAction(ACTION_CANCEL)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
return PendingIntent.getService(context.getApplicationContext(),
|
||||
requestCode,
|
||||
urlString.hashCode(),
|
||||
cancelIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
onStart(intent, startId);
|
||||
Utils.debugLog(TAG, "onStartCommand " + intent);
|
||||
onStart(intent, startId);
|
||||
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.");
|
||||
stopForeground(true);
|
||||
serviceLooper.quit(); //NOPMD - this is copied from IntentService, no super call needed
|
||||
}
|
||||
|
||||
@ -254,40 +189,23 @@ public class DownloaderService extends Service {
|
||||
*/
|
||||
protected void handleIntent(Intent intent) {
|
||||
final Uri uri = intent.getData();
|
||||
File downloadDir = new File(Utils.getApkCacheDir(this), uri.getHost() + "-" + uri.getPort());
|
||||
downloadDir.mkdirs();
|
||||
final SanitizedFile localFile = new SanitizedFile(downloadDir, uri.getLastPathSegment());
|
||||
final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
|
||||
final SanitizedFile localFile = Utils.getApkDownloadPath(this, uri);
|
||||
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile);
|
||||
|
||||
if (Preferences.get().isUpdateNotificationEnabled()) {
|
||||
Notification notification = createNotification(intent.getDataString(), intent.getStringExtra(EXTRA_PACKAGE_NAME)).build();
|
||||
startForeground(NOTIFY_DOWNLOADING, notification);
|
||||
}
|
||||
|
||||
try {
|
||||
downloader = DownloaderFactory.create(this, uri, localFile);
|
||||
downloader.setListener(new Downloader.DownloaderProgressListener() {
|
||||
downloader.setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void sendProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
||||
public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
||||
Intent intent = new Intent(Downloader.ACTION_PROGRESS);
|
||||
intent.setData(uri);
|
||||
intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead);
|
||||
intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes);
|
||||
localBroadcastManager.sendBroadcast(intent);
|
||||
|
||||
if (Preferences.get().isUpdateNotificationEnabled()) {
|
||||
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
Notification notification = createNotification(uri.toString(), packageName)
|
||||
.setProgress(totalBytes, bytesRead, false)
|
||||
.build();
|
||||
nm.notify(NOTIFY_DOWNLOADING, notification);
|
||||
}
|
||||
}
|
||||
});
|
||||
downloader.download();
|
||||
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile);
|
||||
notifyDownloadComplete(packageName, intent.getDataString());
|
||||
} catch (InterruptedException e) {
|
||||
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile);
|
||||
} catch (IOException e) {
|
||||
@ -302,34 +220,8 @@ public class DownloaderService extends Service {
|
||||
downloader = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post a notification about a completed download. {@code packageName} must be a valid
|
||||
* and currently in the app index database.
|
||||
*/
|
||||
private void notifyDownloadComplete(String packageName, String urlString) {
|
||||
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);
|
||||
}
|
||||
|
||||
int downloadUrlId = urlString.hashCode();
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(this)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(title)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentIntent(createAppDetailsIntent(downloadUrlId, packageName))
|
||||
.setContentText(getString(R.string.tap_to_install));
|
||||
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
nm.notify(downloadUrlId, builder.build());
|
||||
private void sendBroadcast(Uri uri, String action) {
|
||||
sendBroadcast(uri, action, null, null);
|
||||
}
|
||||
|
||||
private void sendBroadcast(Uri uri, String action, File file) {
|
||||
@ -339,7 +231,9 @@ public class DownloaderService extends Service {
|
||||
private void sendBroadcast(Uri uri, String action, File file, String errorMessage) {
|
||||
Intent intent = new Intent(action);
|
||||
intent.setData(uri);
|
||||
intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath());
|
||||
if (file != null) {
|
||||
intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath());
|
||||
}
|
||||
if (!TextUtils.isEmpty(errorMessage)) {
|
||||
intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, errorMessage);
|
||||
}
|
||||
@ -351,19 +245,15 @@ 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 packageName The packageName of the app being downloaded
|
||||
* @param urlString The URL to add to the download queue
|
||||
* @param context this app's {@link Context}
|
||||
* @param urlString The URL to add to the download queue
|
||||
* @see #cancel(Context, String)
|
||||
*/
|
||||
public static void queue(Context context, String packageName, String urlString) {
|
||||
public static void queue(Context context, String urlString) {
|
||||
Utils.debugLog(TAG, "Preparing " + urlString + " to go into the download queue");
|
||||
Intent intent = new Intent(context, DownloaderService.class);
|
||||
intent.setAction(ACTION_QUEUE);
|
||||
intent.setData(Uri.parse(urlString));
|
||||
if (!TextUtils.isEmpty(packageName)) {
|
||||
intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
|
||||
}
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
@ -374,7 +264,7 @@ public class DownloaderService extends Service {
|
||||
*
|
||||
* @param context this app's {@link Context}
|
||||
* @param urlString The URL to remove from the download queue
|
||||
* @see #queue(Context, String, String)
|
||||
* @see #queue(Context, String)
|
||||
*/
|
||||
public static void cancel(Context context, String urlString) {
|
||||
Utils.debugLog(TAG, "Preparing cancellation of " + urlString + " download");
|
||||
@ -393,6 +283,9 @@ public class DownloaderService extends Service {
|
||||
if (TextUtils.isEmpty(urlString)) { //NOPMD - suggests unreadable format
|
||||
return false;
|
||||
}
|
||||
if (serviceHandler == null) {
|
||||
return false; // this service is not even running
|
||||
}
|
||||
return serviceHandler.hasMessages(urlString.hashCode()) || isActive(urlString);
|
||||
}
|
||||
|
||||
|
@ -157,7 +157,7 @@ public class HttpDownloader extends Downloader {
|
||||
if (isCached()) {
|
||||
Utils.debugLog(TAG, sourceUrl + " is cached, so not downloading (HTTP " + statusCode + ")");
|
||||
} else {
|
||||
Utils.debugLog(TAG, "doDownload for " + sourceUrl + " " + resumable);
|
||||
Utils.debugLog(TAG, "Need to download " + sourceUrl + " (is resumable: " + resumable + ")");
|
||||
downloadFromStream(8192, resumable);
|
||||
updateCacheCheck();
|
||||
}
|
||||
|
@ -297,7 +297,7 @@ public class SwapAppsView extends ListView implements
|
||||
// TODO: Unregister receivers correctly...
|
||||
|
||||
Apk apk = getApkToInstall();
|
||||
String url = Utils.getApkUrl(apk.repoAddress, apk);
|
||||
String url = apk.getUrl();
|
||||
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(getActivity());
|
||||
localBroadcastManager.registerReceiver(appListViewResetReceiver,
|
||||
|
@ -41,6 +41,7 @@ import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.NewRepoConfig;
|
||||
import org.fdroid.fdroid.installer.InstallManagerService;
|
||||
import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.localrepo.LocalRepoManager;
|
||||
import org.fdroid.fdroid.localrepo.SwapService;
|
||||
@ -778,7 +779,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
|
||||
public void install(@NonNull final App app) {
|
||||
final Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode);
|
||||
String urlString = Utils.getApkUrl(apk.repoAddress, apk);
|
||||
String urlString = apk.getUrl();
|
||||
downloadCompleteReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
@ -788,7 +789,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
};
|
||||
localBroadcastManager.registerReceiver(downloadCompleteReceiver,
|
||||
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE));
|
||||
DownloaderService.queue(this, app.packageName, urlString);
|
||||
InstallManagerService.queue(this, app, apk);
|
||||
}
|
||||
|
||||
private void handleDownloadComplete(File apkFile, String packageName, String urlString) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
@ -48,9 +49,9 @@ public class HttpDownloaderTest {
|
||||
URL url = new URL(urlString);
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
final HttpDownloader httpDownloader = new HttpDownloader(url, destFile);
|
||||
httpDownloader.setListener(new Downloader.DownloaderProgressListener() {
|
||||
httpDownloader.setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void sendProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
||||
public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
||||
System.out.println("DownloaderProgressListener.sendProgress " + sourceUrl + " " + bytesRead + " / " + totalBytes);
|
||||
receivedProgress = true;
|
||||
}
|
||||
@ -111,9 +112,9 @@ public class HttpDownloaderTest {
|
||||
URL url = new URL("https://f-droid.org/repo/index.jar");
|
||||
File destFile = File.createTempFile("dl-", "");
|
||||
final HttpDownloader httpDownloader = new HttpDownloader(url, destFile);
|
||||
httpDownloader.setListener(new Downloader.DownloaderProgressListener() {
|
||||
httpDownloader.setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void sendProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
||||
public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
||||
System.out.println("DownloaderProgressListener.sendProgress " + bytesRead + " / " + totalBytes);
|
||||
receivedProgress = true;
|
||||
latch.countDown();
|
||||
|
Loading…
x
Reference in New Issue
Block a user