Merge branch 'new-downloader-service' into 'master'
New DownloaderService This merge request is too big, but I have all this work complete, so I'm submitting it now for review. If there are bigger issues with parts, I can rebase it down to the uncontroversial bits to get things merged. This replaces `ApkDownloader`, `AsyncDownload`, and `AsyncDownloadWrapper`, and puts the whole APK download procedure into a custom `Service` that is based on the source code for `IntentService`. It can't be a subclass of `IntentService` because it needs to be cancelable. This does not yet add back a notification for the downloading, e.g. #592. That was handled before by the Android DownloadManager stuff, and was not replaced since that was disabled. My current implementation does not filter out duplicate requests like discussed in #601. While its possible to do, I think it'll complicate the code a lot, and I really think that should be handled elsewhere. The UI should prevent the possibility of the user being able to submit duplicate install requests. If not, even if `DownloaderService` filtered them out, it would still be a buggy UX since the user would be clicking install again or something like that even though the install was in progress. This also moves the APK verification logic to the `Installer` side. The downloading side can check the file size to see if the whole thing is downloaded. And to be extra safe, it should not be possible to submit an APK for installation without it going through the verification procedure. So the only method for installing APKs, `Installer.install()`, is where the verification now happens. Also, the installer now always copies the APK to be installed into the safe location RE: the Cure53 audit issue. This way, the APK download and cache dirs can be merged into one, making resumable downloads and cache management easy. ping @mvdan @pserwylo @dschuermann more comments in the commit messages See merge request !248
This commit is contained in:
commit
158668f378
@ -0,0 +1,41 @@
|
||||
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.test.ServiceTestCase;
|
||||
import android.util.Log;
|
||||
|
||||
@SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics
|
||||
public class DownloaderServiceTest extends ServiceTestCase<DownloaderService> {
|
||||
public static final String TAG = "DownloaderServiceTest";
|
||||
|
||||
String[] urls = {
|
||||
"https://en.wikipedia.org/wiki/Index.html",
|
||||
"https://mirrors.kernel.org/debian/dists/stable/Release",
|
||||
"https://f-droid.org/archive/de.we.acaldav_5.apk",
|
||||
// sites that use SNI for HTTPS
|
||||
"https://guardianproject.info/fdroid/repo/index.jar",
|
||||
};
|
||||
|
||||
public DownloaderServiceTest() {
|
||||
super(DownloaderService.class);
|
||||
}
|
||||
|
||||
public void testQueueingDownload() throws InterruptedException {
|
||||
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getContext());
|
||||
localBroadcastManager.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "onReceive " + intent);
|
||||
}
|
||||
}, new IntentFilter(Downloader.ACTION_PROGRESS));
|
||||
for (String url : urls) {
|
||||
DownloaderService.queue(getContext(), null, url);
|
||||
}
|
||||
Thread.sleep(30000);
|
||||
}
|
||||
}
|
@ -439,6 +439,12 @@
|
||||
</receiver>
|
||||
|
||||
<service android:name=".UpdateService" />
|
||||
<service
|
||||
android:name=".net.DownloaderService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".net.DownloadCompleteService"
|
||||
android:exported="false" />
|
||||
<service android:name=".net.WifiStateChangeService" />
|
||||
<service android:name=".localrepo.SwapService" />
|
||||
</application>
|
||||
|
177
app/src/main/java/org/fdroid/fdroid/AndroidXMLDecompress.java
Normal file
177
app/src/main/java/org/fdroid/fdroid/AndroidXMLDecompress.java
Normal file
@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Hans-Christoph Steiner <hans@eds.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright (c) 2016, Liu Dong
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of apk-parser nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
/**
|
||||
* Parse the 'compressed' binary form of Android XML docs such as for
|
||||
* {@code AndroidManifest.xml} in APK files. This is a very truncated
|
||||
* version of apk-parser since currently, we only need the header from
|
||||
* the binary XML AndroidManifest.xml. apk-parser provides full APK
|
||||
* parsing, which is a lot more than what is needed.
|
||||
*
|
||||
* @see <a href="https://github.com/caoqianli/apk-parser">apk-parser</a>
|
||||
* @see <a href="https://justanapplication.wordpress.com/category/android/android-binary-xml">Android Internals: Binary XML</a>
|
||||
* @see <a href="https://stackoverflow.com/a/4761689">a binary XML parser</a>
|
||||
*/
|
||||
public class AndroidXMLDecompress {
|
||||
public static int startTag = 0x00100102;
|
||||
|
||||
/**
|
||||
* Just get the XML attributes from the {@code <manifest>} element.
|
||||
*
|
||||
* @return A key value map of the attributes, with values as {@link Object}s
|
||||
*/
|
||||
public static Map<String, Object> getManifestHeaderAttributes(String filename) throws IOException {
|
||||
byte[] binaryXml = getManifestFromFilename(filename);
|
||||
int numbStrings = littleEndianWord(binaryXml, 4 * 4);
|
||||
int stringIndexTableOffset = 0x24;
|
||||
int stringTableOffset = stringIndexTableOffset + numbStrings * 4;
|
||||
int xmlTagOffset = littleEndianWord(binaryXml, 3 * 4);
|
||||
for (int i = xmlTagOffset; i < binaryXml.length - 4; i += 4) {
|
||||
if (littleEndianWord(binaryXml, i) == startTag) {
|
||||
xmlTagOffset = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int offset = xmlTagOffset;
|
||||
|
||||
while (offset < binaryXml.length) {
|
||||
int tag0 = littleEndianWord(binaryXml, offset);
|
||||
int nameStringIndex = littleEndianWord(binaryXml, offset + 5 * 4);
|
||||
|
||||
if (tag0 == startTag) {
|
||||
int numbAttrs = littleEndianWord(binaryXml, offset + 7 * 4);
|
||||
offset += 9 * 4;
|
||||
|
||||
HashMap<String, Object> attributes = new HashMap<String, Object>(3);
|
||||
for (int i = 0; i < numbAttrs; i++) {
|
||||
int attributeNameStringIndex = littleEndianWord(binaryXml, offset + 1 * 4);
|
||||
int attributeValueStringIndex = littleEndianWord(binaryXml, offset + 2 * 4);
|
||||
int attributeResourceId = littleEndianWord(binaryXml, offset + 4 * 4);
|
||||
offset += 5 * 4;
|
||||
|
||||
String attributeName = getString(binaryXml, stringIndexTableOffset, stringTableOffset, attributeNameStringIndex);
|
||||
Object attributeValue;
|
||||
if (attributeValueStringIndex != -1) {
|
||||
attributeValue = getString(binaryXml, stringIndexTableOffset, stringTableOffset, attributeValueStringIndex);
|
||||
} else {
|
||||
attributeValue = attributeResourceId;
|
||||
}
|
||||
attributes.put(attributeName, attributeValue);
|
||||
}
|
||||
return attributes;
|
||||
} else {
|
||||
// we only need the first <manifest> start tag
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new HashMap<String, Object>(0);
|
||||
}
|
||||
|
||||
public static byte[] getManifestFromFilename(String filename) throws IOException {
|
||||
InputStream is = null;
|
||||
ZipFile zip = null;
|
||||
int size = 0;
|
||||
|
||||
if (filename.endsWith(".apk")) {
|
||||
zip = new ZipFile(filename);
|
||||
ZipEntry ze = zip.getEntry("AndroidManifest.xml");
|
||||
is = zip.getInputStream(ze);
|
||||
size = (int) ze.getSize();
|
||||
} else {
|
||||
throw new RuntimeException("This only works on APK files!");
|
||||
}
|
||||
byte[] buf = new byte[size];
|
||||
is.read(buf);
|
||||
|
||||
is.close();
|
||||
if (zip != null) {
|
||||
zip.close();
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static String getString(byte[] bytes, int stringIndexTableOffset, int stringTableOffset, int stringIndex) {
|
||||
if (stringIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
int stringOffset = stringTableOffset + littleEndianWord(bytes, stringIndexTableOffset + stringIndex * 4);
|
||||
return getStringAt(bytes, stringOffset);
|
||||
}
|
||||
|
||||
public static String getStringAt(byte[] bytes, int stringOffset) {
|
||||
int length = bytes[stringOffset + 1] << 8 & 0xff00 | bytes[stringOffset] & 0xff;
|
||||
byte[] chars = new byte[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
chars[i] = bytes[stringOffset + 2 + i * 2];
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the little endian 32-bit word from the byte array at offset
|
||||
*/
|
||||
public static int littleEndianWord(byte[] bytes, int offset) {
|
||||
return bytes[offset + 3]
|
||||
<< 24 & 0xff000000
|
||||
| bytes[offset + 2]
|
||||
<< 16 & 0xff0000
|
||||
| bytes[offset + 1]
|
||||
<< 8 & 0xff00
|
||||
| bytes[offset] & 0xFF;
|
||||
}
|
||||
}
|
@ -29,7 +29,6 @@ import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.ContentObserver;
|
||||
import android.graphics.Bitmap;
|
||||
@ -80,6 +79,7 @@ import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||
|
||||
import org.fdroid.fdroid.Utils.CommaSeparatedList;
|
||||
import org.fdroid.fdroid.compat.PackageManagerCompat;
|
||||
import org.fdroid.fdroid.compat.PreferencesCompat;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
@ -88,36 +88,16 @@ import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException;
|
||||
import org.fdroid.fdroid.installer.Installer.InstallFailedException;
|
||||
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
|
||||
import org.fdroid.fdroid.net.ApkDownloader;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
interface AppDetailsData {
|
||||
App getApp();
|
||||
|
||||
AppDetails.ApkListAdapter getApks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface which allows the apk list fragment to communicate with the activity when
|
||||
* a user requests to install/remove an apk by clicking on an item in the list.
|
||||
*
|
||||
* NOTE: This is <em>not</em> to do with with the sudo/packagemanager/other installer
|
||||
* stuff which allows multiple ways to install apps. It is only here to make fragment-
|
||||
* activity communication possible.
|
||||
*/
|
||||
interface AppInstallListener {
|
||||
void install(final Apk apk);
|
||||
|
||||
void removeApk(String packageName);
|
||||
}
|
||||
|
||||
public class AppDetails extends AppCompatActivity implements ProgressListener, AppDetailsData, AppInstallListener {
|
||||
public class AppDetails extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "AppDetails";
|
||||
|
||||
@ -324,7 +304,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
|
||||
private App app;
|
||||
private PackageManager packageManager;
|
||||
private ApkDownloader downloadHandler;
|
||||
private String activeDownloadUrlString;
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
|
||||
private boolean startingIgnoreAll;
|
||||
@ -344,17 +324,15 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
*/
|
||||
private static class ConfigurationChangeHelper {
|
||||
|
||||
public final ApkDownloader downloader;
|
||||
public final String urlString;
|
||||
public final App app;
|
||||
|
||||
ConfigurationChangeHelper(ApkDownloader downloader, App app) {
|
||||
this.downloader = downloader;
|
||||
ConfigurationChangeHelper(String urlString, App app) {
|
||||
this.urlString = urlString;
|
||||
this.app = app;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean inProcessOfChangingConfiguration;
|
||||
|
||||
/**
|
||||
* Attempt to extract the packageName from the intent which launched this activity.
|
||||
* @return May return null, if we couldn't find the packageName. This should
|
||||
@ -395,8 +373,8 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
ConfigurationChangeHelper previousData = (ConfigurationChangeHelper) getLastCustomNonConfigurationInstance();
|
||||
if (previousData != null) {
|
||||
Utils.debugLog(TAG, "Recreating view after configuration change.");
|
||||
downloadHandler = previousData.downloader;
|
||||
if (downloadHandler != null) {
|
||||
activeDownloadUrlString = previousData.urlString;
|
||||
if (activeDownloadUrlString != null) {
|
||||
Utils.debugLog(TAG, "Download was in progress before the configuration change, so we will start to listen to its events again.");
|
||||
}
|
||||
app = previousData.app;
|
||||
@ -451,16 +429,8 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
refreshApkList();
|
||||
refreshHeader();
|
||||
supportInvalidateOptionsMenu();
|
||||
|
||||
if (downloadHandler != null) {
|
||||
if (downloadHandler.isComplete()) {
|
||||
downloadCompleteInstallApk();
|
||||
} else {
|
||||
localBroadcastManager.registerReceiver(downloaderProgressReceiver,
|
||||
new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
|
||||
downloadHandler.setProgressListener(this);
|
||||
headerFragment.startProgress();
|
||||
}
|
||||
if (DownloaderService.isQueuedOrActive(activeDownloadUrlString)) {
|
||||
registerDownloaderReceivers();
|
||||
}
|
||||
}
|
||||
|
||||
@ -468,22 +438,9 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
* Remove progress listener, suppress progress bar, set downloadHandler to null.
|
||||
*/
|
||||
private void cleanUpFinishedDownload() {
|
||||
if (downloadHandler != null) {
|
||||
downloadHandler.removeProgressListener();
|
||||
activeDownloadUrlString = null;
|
||||
headerFragment.removeProgress();
|
||||
downloadHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the download completes successfully, call this method to start the install process
|
||||
* with the file that was downloaded.
|
||||
*/
|
||||
private void downloadCompleteInstallApk() {
|
||||
if (downloadHandler != null) {
|
||||
installApk(downloadHandler.localFile());
|
||||
cleanUpFinishedDownload();
|
||||
}
|
||||
unregisterDownloaderReceivers();
|
||||
}
|
||||
|
||||
protected void onStop() {
|
||||
@ -494,21 +451,49 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// save the active URL for this app in case we come back
|
||||
PreferencesCompat.apply(getPreferences(MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(getPackageNameFromIntent(getIntent()), activeDownloadUrlString));
|
||||
if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
|
||||
|| app.ignoreThisUpdate != startingIgnoreThis)) {
|
||||
Utils.debugLog(TAG, "Updating 'ignore updates', as it has changed since we started the activity...");
|
||||
setIgnoreUpdates(app.packageName, app.ignoreAllUpdates, app.ignoreThisUpdate);
|
||||
}
|
||||
|
||||
localBroadcastManager.unregisterReceiver(downloaderProgressReceiver);
|
||||
if (downloadHandler != null) {
|
||||
downloadHandler.removeProgressListener();
|
||||
unregisterDownloaderReceivers();
|
||||
}
|
||||
|
||||
headerFragment.removeProgress();
|
||||
private void unregisterDownloaderReceivers() {
|
||||
localBroadcastManager.unregisterReceiver(startedReceiver);
|
||||
localBroadcastManager.unregisterReceiver(progressReceiver);
|
||||
localBroadcastManager.unregisterReceiver(completeReceiver);
|
||||
localBroadcastManager.unregisterReceiver(interruptedReceiver);
|
||||
}
|
||||
|
||||
private final BroadcastReceiver downloaderProgressReceiver = new BroadcastReceiver() {
|
||||
private void registerDownloaderReceivers() {
|
||||
if (activeDownloadUrlString != null) { // if a download is active
|
||||
String url = activeDownloadUrlString;
|
||||
localBroadcastManager.registerReceiver(startedReceiver,
|
||||
DownloaderService.getIntentFilter(url, Downloader.ACTION_STARTED));
|
||||
localBroadcastManager.registerReceiver(progressReceiver,
|
||||
DownloaderService.getIntentFilter(url, Downloader.ACTION_PROGRESS));
|
||||
localBroadcastManager.registerReceiver(completeReceiver,
|
||||
DownloaderService.getIntentFilter(url, Downloader.ACTION_COMPLETE));
|
||||
localBroadcastManager.registerReceiver(interruptedReceiver,
|
||||
DownloaderService.getIntentFilter(url, Downloader.ACTION_INTERRUPTED));
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver startedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (headerFragment != null) {
|
||||
headerFragment.startProgress();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver progressReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (headerFragment != null) {
|
||||
@ -518,6 +503,37 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts the install process one the download is complete.
|
||||
*/
|
||||
private final BroadcastReceiver completeReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
|
||||
try {
|
||||
installer.installPackage(localFile, app.packageName, intent.getDataString());
|
||||
} catch (InstallFailedException e) {
|
||||
Log.e(TAG, "Android not compatible with this Installer!", e);
|
||||
}
|
||||
cleanUpFinishedDownload();
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.hasExtra(Downloader.EXTRA_ERROR_MESSAGE)) {
|
||||
String msg = intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)
|
||||
+ " " + intent.getDataString();
|
||||
Toast.makeText(context, R.string.download_error, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
|
||||
} else { // user canceled
|
||||
Toast.makeText(context, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
cleanUpFinishedDownload();
|
||||
}
|
||||
};
|
||||
|
||||
private void onAppChanged() {
|
||||
if (!reset(app.packageName)) {
|
||||
this.finish();
|
||||
@ -543,17 +559,12 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
|
||||
@Override
|
||||
public Object onRetainCustomNonConfigurationInstance() {
|
||||
inProcessOfChangingConfiguration = true;
|
||||
return new ConfigurationChangeHelper(downloadHandler, app);
|
||||
return new ConfigurationChangeHelper(activeDownloadUrlString, app);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (downloadHandler != null && !inProcessOfChangingConfiguration) {
|
||||
downloadHandler.cancel();
|
||||
cleanUpFinishedDownload();
|
||||
}
|
||||
inProcessOfChangingConfiguration = false;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@ -565,6 +576,14 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
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));
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(packageName)) {
|
||||
newApp = AppProvider.Helper.findByPackageName(getContentResolver(), packageName);
|
||||
}
|
||||
@ -787,17 +806,11 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
}
|
||||
|
||||
// Install the version of this app denoted by 'app.curApk'.
|
||||
@Override
|
||||
public void install(final Apk apk) {
|
||||
if (isFinishing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore call if another download is running.
|
||||
if (downloadHandler != null && !downloadHandler.isComplete()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String repoaddress = getRepoAddress(apk);
|
||||
if (repoaddress == null) return;
|
||||
|
||||
@ -852,29 +865,17 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
}
|
||||
|
||||
private void startDownload(Apk apk, String repoAddress) {
|
||||
downloadHandler = new ApkDownloader(getBaseContext(), app, apk, repoAddress);
|
||||
|
||||
localBroadcastManager.registerReceiver(downloaderProgressReceiver,
|
||||
new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
|
||||
downloadHandler.setProgressListener(this);
|
||||
if (downloadHandler.download()) {
|
||||
String urlString = Utils.getApkUrl(repoAddress, apk);
|
||||
activeDownloadUrlString = urlString;
|
||||
registerDownloaderReceivers();
|
||||
headerFragment.startProgress();
|
||||
}
|
||||
DownloaderService.queue(this, apk.packageName, activeDownloadUrlString);
|
||||
}
|
||||
|
||||
private void installApk(File file) {
|
||||
try {
|
||||
installer.installPackage(file, app.packageName);
|
||||
} catch (AndroidNotCompatibleException e) {
|
||||
Log.e(TAG, "Android not compatible with this Installer!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeApk(String packageName) {
|
||||
try {
|
||||
installer.deletePackage(packageName);
|
||||
} catch (AndroidNotCompatibleException e) {
|
||||
} catch (InstallFailedException e) {
|
||||
Log.e(TAG, "Android not compatible with this Installer!", e);
|
||||
}
|
||||
}
|
||||
@ -951,42 +952,6 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(Event event) {
|
||||
if (downloadHandler == null || !downloadHandler.isEventFromThis(event)) {
|
||||
// Choose not to respond to events from previous downloaders.
|
||||
// We don't even care if we receive "cancelled" events or the like, because
|
||||
// we dealt with cancellations in the onCancel listener of the dialog,
|
||||
// rather than waiting to receive the event here. We try and be careful in
|
||||
// the download thread to make sure that we check for cancellations before
|
||||
// sending events, but it is not possible to be perfect, because the interruption
|
||||
// which triggers the download can happen after the check to see if
|
||||
Utils.debugLog(TAG, "Discarding downloader event \"" + event.type + "\" as it is from an old (probably cancelled) downloader.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean finished = false;
|
||||
switch (event.type) {
|
||||
case ApkDownloader.EVENT_ERROR:
|
||||
// this must be on the main UI thread
|
||||
Toast.makeText(this, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
|
||||
cleanUpFinishedDownload();
|
||||
finished = true;
|
||||
break;
|
||||
case ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE:
|
||||
downloadCompleteInstallApk();
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (finished) {
|
||||
if (headerFragment != null) {
|
||||
headerFragment.removeProgress();
|
||||
}
|
||||
downloadHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
// handle cases for install manager first
|
||||
@ -1001,12 +966,10 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public App getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApkListAdapter getApks() {
|
||||
return adapter;
|
||||
}
|
||||
@ -1014,7 +977,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
public static class AppDetailsSummaryFragment extends Fragment {
|
||||
|
||||
final Preferences prefs;
|
||||
private AppDetailsData data;
|
||||
private AppDetails appDetails;
|
||||
private static final int MAX_LINES = 5;
|
||||
private static boolean viewAllDescription;
|
||||
private static LinearLayout llViewMoreDescription;
|
||||
@ -1043,15 +1006,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
data = (AppDetailsData) activity;
|
||||
}
|
||||
|
||||
App getApp() {
|
||||
return data.getApp();
|
||||
}
|
||||
|
||||
ApkListAdapter getApks() {
|
||||
return data.getApks();
|
||||
appDetails = (AppDetails) activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1110,7 +1065,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
String url = null;
|
||||
App app = getApp();
|
||||
App app = appDetails.getApp();
|
||||
switch (v.getId()) {
|
||||
case R.id.website:
|
||||
url = app.webURL;
|
||||
@ -1163,7 +1118,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
};
|
||||
|
||||
private void setupView(final View view) {
|
||||
App app = getApp();
|
||||
App app = appDetails.getApp();
|
||||
// Expandable description
|
||||
final TextView description = (TextView) view.findViewById(R.id.description);
|
||||
final Spanned desc = Html.fromHtml(app.description, null, new Utils.HtmlTagHandler());
|
||||
@ -1289,8 +1244,8 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
}
|
||||
|
||||
Apk curApk = null;
|
||||
for (int i = 0; i < getApks().getCount(); i++) {
|
||||
final Apk apk = getApks().getItem(i);
|
||||
for (int i = 0; i < appDetails.getApks().getCount(); i++) {
|
||||
final Apk apk = appDetails.getApks().getItem(i);
|
||||
if (apk.vercode == app.suggestedVercode) {
|
||||
curApk = apk;
|
||||
break;
|
||||
@ -1302,7 +1257,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
final TextView permissionHeader = (TextView) view.findViewById(R.id.permissions);
|
||||
|
||||
final boolean curApkCompatible = curApk != null && curApk.compatible;
|
||||
if (!getApks().isEmpty() && (curApkCompatible || prefs.showIncompatibleVersions())) {
|
||||
if (!appDetails.getApks().isEmpty() && (curApkCompatible || prefs.showIncompatibleVersions())) {
|
||||
// build and set the string once
|
||||
buildPermissionInfo();
|
||||
permissionHeader.setOnClickListener(expanderPermissions);
|
||||
@ -1335,7 +1290,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
private void buildPermissionInfo() {
|
||||
final TextView permissionListView = (TextView) llViewMorePermissions.findViewById(R.id.permissions_list);
|
||||
|
||||
CommaSeparatedList permsList = getApks().getItem(0).permissions;
|
||||
CommaSeparatedList permsList = appDetails.getApks().getItem(0).permissions;
|
||||
if (permsList == null) {
|
||||
permissionListView.setText(R.string.no_permissions);
|
||||
} else {
|
||||
@ -1387,7 +1342,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
return;
|
||||
}
|
||||
|
||||
App app = getApp();
|
||||
App app = appDetails.getApp();
|
||||
TextView signatureView = (TextView) view.findViewById(R.id.signature);
|
||||
if (prefs.expertMode() && !TextUtils.isEmpty(app.installedSig)) {
|
||||
signatureView.setVisibility(View.VISIBLE);
|
||||
@ -1400,7 +1355,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
|
||||
public static class AppDetailsHeaderFragment extends Fragment implements View.OnClickListener {
|
||||
|
||||
private AppDetailsData data;
|
||||
private AppDetails appDetails;
|
||||
private Button btMain;
|
||||
private ProgressBar progressBar;
|
||||
private TextView progressSize;
|
||||
@ -1421,14 +1376,6 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
.build();
|
||||
}
|
||||
|
||||
private App getApp() {
|
||||
return data.getApp();
|
||||
}
|
||||
|
||||
private ApkListAdapter getApks() {
|
||||
return data.getApks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.app_details_header, container, false);
|
||||
@ -1439,11 +1386,11 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
data = (AppDetailsData) activity;
|
||||
appDetails = (AppDetails) activity;
|
||||
}
|
||||
|
||||
private void setupView(View view) {
|
||||
App app = getApp();
|
||||
App app = appDetails.getApp();
|
||||
|
||||
// Set the icon...
|
||||
ImageView iv = (ImageView) view.findViewById(R.id.icon);
|
||||
@ -1531,15 +1478,12 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
*/
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
AppDetails activity = (AppDetails) getActivity();
|
||||
if (activity == null || activity.downloadHandler == null) {
|
||||
AppDetails appDetails = (AppDetails) getActivity();
|
||||
if (appDetails == null || appDetails.activeDownloadUrlString == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
activity.downloadHandler.cancel();
|
||||
activity.cleanUpFinishedDownload();
|
||||
setProgressVisible(false);
|
||||
updateViews();
|
||||
DownloaderService.cancel(getContext(), appDetails.activeDownloadUrlString);
|
||||
}
|
||||
|
||||
public void updateViews() {
|
||||
@ -1547,21 +1491,20 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
}
|
||||
|
||||
public void updateViews(View view) {
|
||||
App app = getApp();
|
||||
App app = appDetails.getApp();
|
||||
TextView statusView = (TextView) view.findViewById(R.id.status);
|
||||
btMain.setVisibility(View.VISIBLE);
|
||||
|
||||
AppDetails activity = (AppDetails) getActivity();
|
||||
if (activity.downloadHandler != null) {
|
||||
if (appDetails.activeDownloadUrlString != null) {
|
||||
btMain.setText(R.string.downloading);
|
||||
btMain.setEnabled(false);
|
||||
} else if (!app.isInstalled() && app.suggestedVercode > 0 &&
|
||||
activity.adapter.getCount() > 0) {
|
||||
appDetails.adapter.getCount() > 0) {
|
||||
// Check count > 0 due to incompatible apps resulting in an empty list.
|
||||
// If App isn't installed
|
||||
installed = false;
|
||||
statusView.setText(R.string.details_notinstalled);
|
||||
NfcHelper.disableAndroidBeam(activity);
|
||||
NfcHelper.disableAndroidBeam(appDetails);
|
||||
// Set Install button and hide second button
|
||||
btMain.setText(R.string.menu_install);
|
||||
btMain.setOnClickListener(mOnClickListener);
|
||||
@ -1570,13 +1513,13 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
// If App is installed
|
||||
installed = true;
|
||||
statusView.setText(getString(R.string.details_installed, app.installedVersionName));
|
||||
NfcHelper.setAndroidBeam(activity, app.packageName);
|
||||
NfcHelper.setAndroidBeam(appDetails, app.packageName);
|
||||
if (app.canAndWantToUpdate()) {
|
||||
updateWanted = true;
|
||||
btMain.setText(R.string.menu_upgrade);
|
||||
} else {
|
||||
updateWanted = false;
|
||||
if (activity.packageManager.getLaunchIntentForPackage(app.packageName) != null) {
|
||||
if (appDetails.packageManager.getLaunchIntentForPackage(app.packageName) != null) {
|
||||
btMain.setText(R.string.menu_launch);
|
||||
} else {
|
||||
btMain.setText(R.string.menu_uninstall);
|
||||
@ -1594,8 +1537,8 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
author.setVisibility(View.VISIBLE);
|
||||
}
|
||||
TextView currentVersion = (TextView) view.findViewById(R.id.current_version);
|
||||
if (!getApks().isEmpty()) {
|
||||
currentVersion.setText(getApks().getItem(0).version + " (" + app.license + ")");
|
||||
if (!appDetails.getApks().isEmpty()) {
|
||||
currentVersion.setText(appDetails.getApks().getItem(0).version + " (" + app.license + ")");
|
||||
} else {
|
||||
currentVersion.setVisibility(View.GONE);
|
||||
btMain.setVisibility(View.GONE);
|
||||
@ -1605,7 +1548,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
|
||||
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
App app = getApp();
|
||||
App app = appDetails.getApp();
|
||||
AppDetails activity = (AppDetails) getActivity();
|
||||
if (updateWanted && app.suggestedVercode > 0) {
|
||||
Apk apkToInstall = ApkProvider.Helper.find(activity, app.packageName, app.suggestedVercode);
|
||||
@ -1635,8 +1578,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
|
||||
private static final String SUMMARY_TAG = "summary";
|
||||
|
||||
private AppDetailsData data;
|
||||
private AppInstallListener installListener;
|
||||
private AppDetails appDetails;
|
||||
private AppDetailsSummaryFragment summaryFragment;
|
||||
|
||||
private FrameLayout headerView;
|
||||
@ -1644,24 +1586,11 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
data = (AppDetailsData) activity;
|
||||
installListener = (AppInstallListener) activity;
|
||||
}
|
||||
|
||||
void install(final Apk apk) {
|
||||
installListener.install(apk);
|
||||
appDetails = (AppDetails) activity;
|
||||
}
|
||||
|
||||
void remove() {
|
||||
installListener.removeApk(getApp().packageName);
|
||||
}
|
||||
|
||||
App getApp() {
|
||||
return data.getApp();
|
||||
}
|
||||
|
||||
ApkListAdapter getApks() {
|
||||
return data.getApks();
|
||||
appDetails.removeApk(appDetails.getApp().packageName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1683,13 +1612,13 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
|
||||
setListAdapter(null);
|
||||
getListView().addHeaderView(headerView);
|
||||
setListAdapter(getApks());
|
||||
setListAdapter(appDetails.getApks());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
||||
App app = getApp();
|
||||
final Apk apk = getApks().getItem(position - l.getHeaderViewsCount());
|
||||
App app = appDetails.getApp();
|
||||
final Apk apk = appDetails.getApks().getItem(position - l.getHeaderViewsCount());
|
||||
if (app.installedVersionCode == apk.vercode) {
|
||||
remove();
|
||||
} else if (app.installedVersionCode > apk.vercode) {
|
||||
@ -1700,7 +1629,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog,
|
||||
int whichButton) {
|
||||
install(apk);
|
||||
appDetails.install(apk);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.no,
|
||||
@ -1713,7 +1642,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
} else {
|
||||
install(apk);
|
||||
appDetails.install(apk);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,7 +234,6 @@ public class FDroidApp extends Application {
|
||||
// been installed, but this causes problems for proprietary gapps
|
||||
// users since the introduction of verification (on pre-4.2 Android),
|
||||
// because the install intent says it's finished when it hasn't.
|
||||
Utils.deleteFiles(Utils.getApkDownloadDir(this), null, ".apk");
|
||||
if (!Preferences.get().shouldCacheApks()) {
|
||||
Utils.deleteFiles(Utils.getApkCacheDir(this), null, ".apk");
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
||||
|
||||
public static final String PREF_UPD_INTERVAL = "updateInterval";
|
||||
public static final String PREF_UPD_WIFI_ONLY = "updateOnWifiOnly";
|
||||
public static final String PREF_UPD_AUTO_DOWNLOAD = "updateAutoDownload";
|
||||
public static final String PREF_UPD_NOTIFY = "updateNotify";
|
||||
public static final String PREF_UPD_HISTORY = "updateHistoryDays";
|
||||
public static final String PREF_ROOTED = "rooted";
|
||||
@ -54,7 +55,6 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
||||
public static final String PREF_CACHE_APK = "cacheDownloaded";
|
||||
public static final String PREF_UNSTABLE_UPDATES = "unstableUpdates";
|
||||
public static final String PREF_EXPERT = "expert";
|
||||
public static final String PREF_UPD_LAST = "lastUpdateCheck";
|
||||
public static final String PREF_PRIVILEGED_INSTALLER = "privilegedInstaller";
|
||||
public static final String PREF_UNINSTALL_PRIVILEGED_APP = "uninstallPrivilegedApp";
|
||||
public static final String PREF_LOCAL_REPO_NAME = "localRepoName";
|
||||
@ -179,6 +179,18 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
||||
return preferences.getString(PREF_LOCAL_REPO_NAME, getDefaultLocalRepoName());
|
||||
}
|
||||
|
||||
public boolean isUpdateNotificationEnabled() {
|
||||
return preferences.getBoolean(PREF_UPD_NOTIFY, true);
|
||||
}
|
||||
|
||||
public boolean isAutoDownloadEnabled() {
|
||||
return preferences.getBoolean(PREF_UPD_AUTO_DOWNLOAD, false);
|
||||
}
|
||||
|
||||
public boolean isUpdateOnlyOnWifi() {
|
||||
return preferences.getBoolean(PREF_UPD_WIFI_ONLY, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* This preference's default is set dynamically based on whether Orbot is
|
||||
* installed. If Orbot is installed, default to using Tor, the user can still override
|
||||
|
@ -42,12 +42,14 @@ import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.fdroid.fdroid.compat.PreferencesCompat;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
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.net.Downloader;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -72,6 +74,8 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
public static final int STATUS_ERROR_LOCAL_SMALL = 4;
|
||||
public static final int STATUS_INFO = 5;
|
||||
|
||||
private static final String STATE_LAST_UPDATED = "lastUpdateCheck";
|
||||
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
|
||||
private static final int NOTIFY_ID_UPDATING = 0;
|
||||
@ -129,8 +133,6 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
super.onCreate();
|
||||
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
localBroadcastManager.registerReceiver(downloadProgressReceiver,
|
||||
new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
|
||||
localBroadcastManager.registerReceiver(updateStatusReceiver,
|
||||
new IntentFilter(LOCAL_ACTION_STATUS));
|
||||
|
||||
@ -192,16 +194,7 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
private final BroadcastReceiver downloadProgressReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (TextUtils.isEmpty(action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!action.equals(Downloader.LOCAL_ACTION_PROGRESS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String repoAddress = intent.getStringExtra(Downloader.EXTRA_ADDRESS);
|
||||
String repoAddress = intent.getDataString();
|
||||
int downloadedSize = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1);
|
||||
String downloadedSizeFriendly = Utils.getFriendlySize(downloadedSize);
|
||||
int totalSize = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1);
|
||||
@ -305,7 +298,7 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
Log.i(TAG, "Skipping update - disabled");
|
||||
return false;
|
||||
}
|
||||
long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0);
|
||||
long lastUpdate = prefs.getLong(STATE_LAST_UPDATED, 0);
|
||||
long elapsed = System.currentTimeMillis() - lastUpdate;
|
||||
if (elapsed < interval * 60 * 60 * 1000) {
|
||||
Log.i(TAG, "Skipping update - done " + elapsed
|
||||
@ -328,9 +321,7 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
return false;
|
||||
}
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (activeNetwork.getType() != ConnectivityManager.TYPE_WIFI
|
||||
&& prefs.getBoolean(Preferences.PREF_UPD_WIFI_ONLY, false)) {
|
||||
if (activeNetwork.getType() != ConnectivityManager.TYPE_WIFI && Preferences.get().isUpdateOnlyOnWifi()) {
|
||||
Log.i(TAG, "Skipping update - wifi not available");
|
||||
return false;
|
||||
}
|
||||
@ -353,8 +344,6 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
return;
|
||||
}
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
|
||||
|
||||
// Grab some preliminary information, then we can release the
|
||||
// database while we do all the downloading, etc...
|
||||
List<Repo> repos = RepoProvider.Helper.all(this);
|
||||
@ -381,6 +370,8 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
|
||||
sendStatus(this, STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
|
||||
RepoUpdater updater = new RepoUpdater(getBaseContext(), repo);
|
||||
localBroadcastManager.registerReceiver(downloadProgressReceiver,
|
||||
DownloaderService.getIntentFilter(updater.indexUrl, Downloader.ACTION_PROGRESS));
|
||||
updater.setProgressListener(this);
|
||||
try {
|
||||
updater.update();
|
||||
@ -395,6 +386,12 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
repoErrors.add(e.getMessage());
|
||||
Log.e(TAG, "Error updating repository " + repo.address, e);
|
||||
}
|
||||
localBroadcastManager.unregisterReceiver(downloadProgressReceiver);
|
||||
|
||||
// now that downloading the index is done, start downloading updates
|
||||
if (changes && Preferences.get().isAutoDownloadEnabled()) {
|
||||
autoDownloadUpdates(repo.address);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changes) {
|
||||
@ -402,13 +399,14 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
} else {
|
||||
notifyContentProviders();
|
||||
|
||||
if (prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, true)) {
|
||||
if (Preferences.get().isUpdateNotificationEnabled()) {
|
||||
performUpdateNotification();
|
||||
}
|
||||
}
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
|
||||
SharedPreferences.Editor e = prefs.edit();
|
||||
e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis());
|
||||
e.putLong(STATE_LAST_UPDATED, System.currentTimeMillis());
|
||||
PreferencesCompat.apply(e);
|
||||
|
||||
if (errorRepos == 0) {
|
||||
@ -484,6 +482,25 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
return inboxStyle;
|
||||
}
|
||||
|
||||
private void autoDownloadUpdates(String repoAddress) {
|
||||
Cursor cursor = getContentResolver().query(
|
||||
AppProvider.getCanUpdateUri(),
|
||||
new String[]{
|
||||
AppProvider.DataColumns.PACKAGE_NAME,
|
||||
AppProvider.DataColumns.SUGGESTED_VERSION_CODE,
|
||||
}, null, null, null);
|
||||
cursor.moveToFirst();
|
||||
for (int i = 0; i < cursor.getCount(); i++) {
|
||||
App app = new App(cursor);
|
||||
Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVercode, new String[]{
|
||||
ApkProvider.DataColumns.NAME,
|
||||
});
|
||||
String urlString = Utils.getApkUrl(repoAddress, apk);
|
||||
DownloaderService.queue(this, app.packageName, urlString);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
|
||||
private void showAppUpdatesNotification(Cursor hasUpdates) {
|
||||
Utils.debugLog(TAG, "Notifying " + hasUpdates.getCount() + " updates.");
|
||||
|
||||
|
@ -66,6 +66,7 @@ import java.util.Formatter;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.Adler32;
|
||||
|
||||
public final class Utils {
|
||||
|
||||
@ -318,7 +319,11 @@ public final class Utils {
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Utils#getApkDownloadDir(android.content.Context)} for why this is "unsafe".
|
||||
* 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
|
||||
* the APK while the install was in process, allowing malware to install things.
|
||||
* Using {@link org.fdroid.fdroid.installer.Installer#installPackage(File, String, String)}
|
||||
* is fine since that does the right thing.
|
||||
*/
|
||||
public static SanitizedFile getApkCacheDir(Context context) {
|
||||
final SanitizedFile apkCacheDir = new SanitizedFile(StorageUtils.getCacheDirectory(context, true), "apks");
|
||||
@ -328,24 +333,6 @@ public final class Utils {
|
||||
return apkCacheDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* The directory where .apk files are downloaded (and stored - if the relevant property is enabled).
|
||||
* This must be on internal storage, to prevent other apps with "write external storage" from being
|
||||
* able to change the .apk file between F-Droid requesting the Package Manger to install, and the
|
||||
* Package Manager receiving that request.
|
||||
*/
|
||||
public static File getApkDownloadDir(Context context) {
|
||||
final SanitizedFile apkCacheDir = new SanitizedFile(StorageUtils.getCacheDirectory(context, false), "temp");
|
||||
if (!apkCacheDir.exists()) {
|
||||
apkCacheDir.mkdir();
|
||||
}
|
||||
|
||||
// All parent directories of the .apk file need to be executable for the package installer
|
||||
// to be able to have permission to read our world-readable .apk files.
|
||||
FileCompat.setExecutable(apkCacheDir, true, false);
|
||||
return apkCacheDir;
|
||||
}
|
||||
|
||||
public static String calcFingerprint(String keyHexString) {
|
||||
if (TextUtils.isEmpty(keyHexString)
|
||||
|| keyHexString.matches(".*[^a-fA-F0-9].*")) {
|
||||
@ -426,6 +413,15 @@ public final class Utils {
|
||||
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> {
|
||||
private final String value;
|
||||
|
||||
|
@ -39,7 +39,7 @@ public class DefaultInstaller extends Installer {
|
||||
private final Activity mActivity;
|
||||
|
||||
public DefaultInstaller(Activity activity, PackageManager pm, InstallerCallback callback)
|
||||
throws AndroidNotCompatibleException {
|
||||
throws InstallFailedException {
|
||||
super(activity, pm, callback);
|
||||
this.mActivity = activity;
|
||||
}
|
||||
@ -48,7 +48,7 @@ public class DefaultInstaller extends Installer {
|
||||
private static final int REQUEST_CODE_DELETE = 1;
|
||||
|
||||
@Override
|
||||
protected void installPackageInternal(File apkFile) throws AndroidNotCompatibleException {
|
||||
protected void installPackageInternal(File apkFile) throws InstallFailedException {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.fromFile(apkFile),
|
||||
@ -56,12 +56,12 @@ public class DefaultInstaller extends Installer {
|
||||
try {
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
throw new AndroidNotCompatibleException(e);
|
||||
throw new InstallFailedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deletePackageInternal(String packageName) throws AndroidNotCompatibleException {
|
||||
protected void deletePackageInternal(String packageName) throws InstallFailedException {
|
||||
try {
|
||||
PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0);
|
||||
|
||||
@ -70,7 +70,7 @@ public class DefaultInstaller extends Installer {
|
||||
try {
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
throw new AndroidNotCompatibleException(e);
|
||||
throw new InstallFailedException(e);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
// already checked in super class
|
||||
|
@ -42,7 +42,7 @@ public class DefaultSdk14Installer extends Installer {
|
||||
private final Activity mActivity;
|
||||
|
||||
public DefaultSdk14Installer(Activity activity, PackageManager pm, InstallerCallback callback)
|
||||
throws AndroidNotCompatibleException {
|
||||
throws InstallFailedException {
|
||||
super(activity, pm, callback);
|
||||
this.mActivity = activity;
|
||||
}
|
||||
@ -52,7 +52,7 @@ public class DefaultSdk14Installer extends Installer {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
protected void installPackageInternal(File apkFile) throws AndroidNotCompatibleException {
|
||||
protected void installPackageInternal(File apkFile) throws InstallFailedException {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
|
||||
intent.setData(Uri.fromFile(apkFile));
|
||||
@ -68,12 +68,12 @@ public class DefaultSdk14Installer extends Installer {
|
||||
try {
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
throw new AndroidNotCompatibleException(e);
|
||||
throw new InstallFailedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deletePackageInternal(String packageName) throws AndroidNotCompatibleException {
|
||||
protected void deletePackageInternal(String packageName) throws InstallFailedException {
|
||||
try {
|
||||
PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0);
|
||||
|
||||
@ -83,7 +83,7 @@ public class DefaultSdk14Installer extends Installer {
|
||||
try {
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
throw new AndroidNotCompatibleException(e);
|
||||
throw new InstallFailedException(e);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
// already checked in super class
|
||||
|
@ -20,17 +20,29 @@
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.fdroid.fdroid.AndroidXMLDecompress;
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.Hasher;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.compat.FileCompat;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Abstract Installer class. Also provides static methods to automatically
|
||||
@ -49,11 +61,15 @@ public abstract class Installer {
|
||||
* RootInstaller or due to an incompatible Android version in case of
|
||||
* SystemPermissionInstaller
|
||||
*/
|
||||
public static class AndroidNotCompatibleException extends Exception {
|
||||
public static class InstallFailedException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -8343133906463328027L;
|
||||
|
||||
public AndroidNotCompatibleException(Throwable cause) {
|
||||
public InstallFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InstallFailedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@ -78,7 +94,7 @@ public abstract class Installer {
|
||||
}
|
||||
|
||||
Installer(Context context, PackageManager pm, InstallerCallback callback)
|
||||
throws AndroidNotCompatibleException {
|
||||
throws InstallFailedException {
|
||||
this.mContext = context;
|
||||
this.mPm = pm;
|
||||
this.mCallback = callback;
|
||||
@ -104,7 +120,7 @@ public abstract class Installer {
|
||||
|
||||
try {
|
||||
return new PrivilegedInstaller(activity, pm, callback);
|
||||
} catch (AndroidNotCompatibleException e) {
|
||||
} catch (InstallFailedException e) {
|
||||
Log.e(TAG, "Android not compatible with SystemInstaller!", e);
|
||||
}
|
||||
} else {
|
||||
@ -116,19 +132,19 @@ public abstract class Installer {
|
||||
if (android.os.Build.VERSION.SDK_INT >= 14) {
|
||||
// Default installer on Android >= 4.0
|
||||
try {
|
||||
Utils.debugLog(TAG, "try default installer for Android >= 4");
|
||||
Utils.debugLog(TAG, "try default installer for android >= 14");
|
||||
|
||||
return new DefaultSdk14Installer(activity, pm, callback);
|
||||
} catch (AndroidNotCompatibleException e) {
|
||||
} catch (InstallFailedException e) {
|
||||
Log.e(TAG, "Android not compatible with DefaultInstallerSdk14!", e);
|
||||
}
|
||||
} else {
|
||||
// Default installer on Android < 4.0
|
||||
// Default installer on Android < 4.0 (android-14)
|
||||
try {
|
||||
Utils.debugLog(TAG, "try default installer for Android < 4");
|
||||
Utils.debugLog(TAG, "try default installer for android < 14");
|
||||
|
||||
return new DefaultInstaller(activity, pm, callback);
|
||||
} catch (AndroidNotCompatibleException e) {
|
||||
} catch (InstallFailedException e) {
|
||||
Log.e(TAG, "Android not compatible with DefaultInstaller!", e);
|
||||
}
|
||||
}
|
||||
@ -137,12 +153,56 @@ public abstract class Installer {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void installPackage(File apkFile, String packageName) throws AndroidNotCompatibleException {
|
||||
// check if file exists...
|
||||
/**
|
||||
* Checks the APK file against the provided hash, returning whether it is a match.
|
||||
*/
|
||||
private static boolean verifyApkFile(File apkFile, String hash, String hashType)
|
||||
throws NoSuchAlgorithmException {
|
||||
if (!apkFile.exists()) {
|
||||
Log.e(TAG, "Couldn't find file " + apkFile + " to install.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
Hasher hasher = new Hasher(hashType, apkFile);
|
||||
if (hasher != null && hasher.match(hash)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the safe, single point of entry for submitting an APK file to be installed.
|
||||
*/
|
||||
public void installPackage(File apkFile, String packageName, String urlString)
|
||||
throws InstallFailedException {
|
||||
SanitizedFile apkToInstall = null;
|
||||
try {
|
||||
Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath());
|
||||
|
||||
/* This isn't really needed, but might as well since we have the data already */
|
||||
if (attributes.containsKey("packageName")) {
|
||||
if (!TextUtils.equals(packageName, (String) attributes.get("packageName"))) {
|
||||
throw new InstallFailedException(apkFile + " has packageName that clashes with " + packageName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!attributes.containsKey("versionCode")) {
|
||||
throw new InstallFailedException(apkFile + " is missing versionCode!");
|
||||
}
|
||||
int versionCode = (Integer) attributes.get("versionCode");
|
||||
Apk apk = ApkProvider.Helper.find(mContext, packageName, versionCode, new String[]{
|
||||
ApkProvider.DataColumns.HASH,
|
||||
ApkProvider.DataColumns.HASH_TYPE,
|
||||
});
|
||||
/* Always copy the APK to the safe location inside of the protected area
|
||||
* of the app to prevent attacks based on other apps swapping the file
|
||||
* out during the install process. Most likely, apkFile was just downloaded,
|
||||
* so it should still be in the RAM disk cache */
|
||||
apkToInstall = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk", mContext.getFilesDir()));
|
||||
FileUtils.copyFile(apkFile, apkToInstall);
|
||||
if (!verifyApkFile(apkToInstall, apk.hash, apk.hashType)) {
|
||||
FileUtils.deleteQuietly(apkFile);
|
||||
throw new InstallFailedException(apkFile + " failed to verify!");
|
||||
}
|
||||
apkFile = null; // ensure this is not used now that its copied to apkToInstall
|
||||
|
||||
// special case: F-Droid Privileged Extension
|
||||
if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
|
||||
@ -150,29 +210,37 @@ public abstract class Installer {
|
||||
// extension must be signed with the same public key as main F-Droid
|
||||
// NOTE: Disabled for debug builds to be able to use official extension from repo
|
||||
ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(mContext);
|
||||
if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(apkFile)) {
|
||||
throw new SecurityException("APK signature of extension not correct!");
|
||||
}
|
||||
|
||||
Activity activity;
|
||||
try {
|
||||
activity = (Activity) mContext;
|
||||
} catch (ClassCastException e) {
|
||||
Utils.debugLog(TAG, "F-Droid Privileged can only be updated using an activity!");
|
||||
return;
|
||||
if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(apkToInstall)) {
|
||||
throw new InstallFailedException("APK signature of extension not correct!");
|
||||
}
|
||||
|
||||
Activity activity = (Activity) mContext;
|
||||
Intent installIntent = new Intent(activity, InstallExtensionDialogActivity.class);
|
||||
installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
|
||||
installIntent.putExtra(InstallExtensionDialogActivity.EXTRA_INSTALL_APK, apkFile.getAbsolutePath());
|
||||
installIntent.putExtra(InstallExtensionDialogActivity.EXTRA_INSTALL_APK, apkToInstall.getAbsolutePath());
|
||||
activity.startActivity(installIntent);
|
||||
return;
|
||||
}
|
||||
|
||||
installPackageInternal(apkFile);
|
||||
// Need the apk to be world readable, so that the installer is able to read it.
|
||||
// Note that saving it into external storage for the purpose of letting the installer
|
||||
// have access is insecure, because apps with permission to write to the external
|
||||
// storage can overwrite the app between F-Droid asking for it to be installed and
|
||||
// the installer actually installing it.
|
||||
FileCompat.setReadable(apkToInstall, true, false);
|
||||
installPackageInternal(apkToInstall);
|
||||
|
||||
NotificationManager nm = (NotificationManager)
|
||||
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
nm.cancel(Utils.getApkUrlNotificationId(urlString));
|
||||
} catch (NumberFormatException | NoSuchAlgorithmException | IOException e) {
|
||||
throw new InstallFailedException(e);
|
||||
} catch (ClassCastException e) {
|
||||
throw new InstallFailedException("F-Droid Privileged can only be updated using an activity!");
|
||||
}
|
||||
}
|
||||
|
||||
public void deletePackage(String packageName) throws AndroidNotCompatibleException {
|
||||
public void deletePackage(String packageName) throws InstallFailedException {
|
||||
// check if package exists before proceeding...
|
||||
try {
|
||||
mPm.getPackageInfo(packageName, 0);
|
||||
@ -201,10 +269,10 @@ public abstract class Installer {
|
||||
}
|
||||
|
||||
protected abstract void installPackageInternal(File apkFile)
|
||||
throws AndroidNotCompatibleException;
|
||||
throws InstallFailedException;
|
||||
|
||||
protected abstract void deletePackageInternal(String packageName)
|
||||
throws AndroidNotCompatibleException;
|
||||
throws InstallFailedException;
|
||||
|
||||
public abstract boolean handleOnActivityResult(int requestCode, int resultCode, Intent data);
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ public class PrivilegedInstaller extends Installer {
|
||||
public static final int IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM = 3;
|
||||
|
||||
public PrivilegedInstaller(Activity activity, PackageManager pm,
|
||||
InstallerCallback callback) throws AndroidNotCompatibleException {
|
||||
InstallerCallback callback) throws InstallFailedException {
|
||||
super(activity, pm, callback);
|
||||
this.mActivity = activity;
|
||||
}
|
||||
@ -156,7 +156,7 @@ public class PrivilegedInstaller extends Installer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void installPackageInternal(File apkFile) throws AndroidNotCompatibleException {
|
||||
protected void installPackageInternal(File apkFile) throws InstallFailedException {
|
||||
Uri packageUri = Uri.fromFile(apkFile);
|
||||
int count = newPermissionCount(packageUri);
|
||||
if (count < 0) {
|
||||
@ -171,7 +171,7 @@ public class PrivilegedInstaller extends Installer {
|
||||
} else {
|
||||
try {
|
||||
doInstallPackageInternal(packageUri);
|
||||
} catch (AndroidNotCompatibleException e) {
|
||||
} catch (InstallFailedException e) {
|
||||
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
|
||||
InstallerCallback.ERROR_CODE_OTHER);
|
||||
}
|
||||
@ -194,7 +194,7 @@ public class PrivilegedInstaller extends Installer {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void doInstallPackageInternal(final Uri packageURI) throws AndroidNotCompatibleException {
|
||||
private void doInstallPackageInternal(final Uri packageURI) throws InstallFailedException {
|
||||
ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
|
||||
@ -238,7 +238,7 @@ public class PrivilegedInstaller extends Installer {
|
||||
|
||||
@Override
|
||||
protected void deletePackageInternal(final String packageName)
|
||||
throws AndroidNotCompatibleException {
|
||||
throws InstallFailedException {
|
||||
ApplicationInfo appInfo;
|
||||
try {
|
||||
appInfo = mPm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
|
||||
@ -273,7 +273,7 @@ public class PrivilegedInstaller extends Installer {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
try {
|
||||
doDeletePackageInternal(packageName);
|
||||
} catch (AndroidNotCompatibleException e) {
|
||||
} catch (InstallFailedException e) {
|
||||
mCallback.onError(InstallerCallback.OPERATION_DELETE,
|
||||
InstallerCallback.ERROR_CODE_OTHER);
|
||||
}
|
||||
@ -293,7 +293,7 @@ public class PrivilegedInstaller extends Installer {
|
||||
}
|
||||
|
||||
private void doDeletePackageInternal(final String packageName)
|
||||
throws AndroidNotCompatibleException {
|
||||
throws InstallFailedException {
|
||||
ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
|
||||
@ -344,7 +344,7 @@ public class PrivilegedInstaller extends Installer {
|
||||
final Uri packageUri = data.getData();
|
||||
try {
|
||||
doInstallPackageInternal(packageUri);
|
||||
} catch (AndroidNotCompatibleException e) {
|
||||
} catch (InstallFailedException e) {
|
||||
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
|
||||
InstallerCallback.ERROR_CODE_OTHER);
|
||||
}
|
||||
|
@ -1,264 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2010-2012 Ciaran Gultnieks <ciaran@ciarang.com>
|
||||
* Copyright (C) 2011 Henrik Tunedal <tunedal@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.fdroid.fdroid.Hasher;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.compat.FileCompat;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Downloads and verifies (against the Apk.hash) the apk file.
|
||||
* If the file has previously been downloaded, it will make use of that
|
||||
* instead, without going to the network to download a new one.
|
||||
*/
|
||||
public class ApkDownloader implements AsyncDownloader.Listener {
|
||||
|
||||
private static final String TAG = "ApkDownloader";
|
||||
|
||||
public static final String EVENT_APK_DOWNLOAD_COMPLETE = "apkDownloadComplete";
|
||||
public static final String EVENT_ERROR = "apkDownloadError";
|
||||
|
||||
public static final String ACTION_STATUS = "apkDownloadStatus";
|
||||
|
||||
private static final String EVENT_SOURCE_ID = "sourceId";
|
||||
private static long downloadIdCounter;
|
||||
|
||||
@NonNull private final App app;
|
||||
@NonNull private final Apk curApk;
|
||||
@NonNull private final Context context;
|
||||
@NonNull private final String repoAddress;
|
||||
@NonNull private final SanitizedFile localFile;
|
||||
@NonNull private final SanitizedFile potentiallyCachedFile;
|
||||
|
||||
private ProgressListener listener;
|
||||
private AsyncDownloader dlWrapper;
|
||||
private boolean isComplete;
|
||||
|
||||
private final long id = ++downloadIdCounter;
|
||||
|
||||
public void setProgressListener(ProgressListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void removeProgressListener() {
|
||||
setProgressListener(null);
|
||||
}
|
||||
|
||||
public ApkDownloader(@NonNull final Context context, @NonNull final App app, @NonNull final Apk apk, @NonNull final String repoAddress) {
|
||||
this.context = context;
|
||||
this.app = app;
|
||||
curApk = apk;
|
||||
this.repoAddress = repoAddress;
|
||||
localFile = new SanitizedFile(Utils.getApkDownloadDir(context), apk.apkName);
|
||||
potentiallyCachedFile = new SanitizedFile(Utils.getApkCacheDir(context), apk.apkName);
|
||||
}
|
||||
|
||||
/**
|
||||
* The downloaded APK. Valid only when getStatus() has returned STATUS.DONE.
|
||||
*/
|
||||
public SanitizedFile localFile() {
|
||||
return localFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* When stopping/starting downloaders multiple times (on different threads), it can
|
||||
* get weird whereby different threads are sending progress events. It is important
|
||||
* to be able to see which downloader these progress events are coming from.
|
||||
*/
|
||||
public boolean isEventFromThis(Event event) {
|
||||
return event.getData().containsKey(EVENT_SOURCE_ID) && event.getData().getLong(EVENT_SOURCE_ID) == id;
|
||||
}
|
||||
|
||||
private Hasher createHasher(File apkFile) {
|
||||
Hasher hasher;
|
||||
try {
|
||||
hasher = new Hasher(curApk.hashType, apkFile);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.e(TAG, "Error verifying hash of cached apk at " + apkFile + ". " +
|
||||
"I don't understand what the " + curApk.hashType + " hash algorithm is :(");
|
||||
hasher = null;
|
||||
}
|
||||
return hasher;
|
||||
}
|
||||
|
||||
private boolean hashMatches(@NonNull final File apkFile) {
|
||||
if (!apkFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
Hasher hasher = createHasher(apkFile);
|
||||
return hasher != null && hasher.match(curApk.hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* If an existing cached version exists, and matches the hash of the apk we
|
||||
* want to download, then we will return true. Otherwise, we return false
|
||||
* (and remove the cached file - if it exists and didn't match the correct hash).
|
||||
*/
|
||||
private boolean verifyOrDelete(@NonNull final File apkFile) {
|
||||
if (apkFile.exists()) {
|
||||
if (hashMatches(apkFile)) {
|
||||
Utils.debugLog(TAG, "Using cached apk at " + apkFile);
|
||||
return true;
|
||||
}
|
||||
Utils.debugLog(TAG, "Not using cached apk at " + apkFile + "(hash doesn't match, will delete file)");
|
||||
delete(apkFile);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void delete(@NonNull final File file) {
|
||||
if (file.exists()) {
|
||||
if (!file.delete()) {
|
||||
Log.w(TAG, "Could not delete file " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareApkFileAndSendCompleteMessage() {
|
||||
|
||||
// Need the apk to be world readable, so that the installer is able to read it.
|
||||
// Note that saving it into external storage for the purpose of letting the installer
|
||||
// have access is insecure, because apps with permission to write to the external
|
||||
// storage can overwrite the app between F-Droid asking for it to be installed and
|
||||
// the installer actually installing it.
|
||||
FileCompat.setReadable(localFile, true, false);
|
||||
|
||||
isComplete = true;
|
||||
sendMessage(EVENT_APK_DOWNLOAD_COMPLETE);
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return this.isComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the download successfully spins up a new thread to start downloading, then we return
|
||||
* true, otherwise false. This is useful, e.g. when we use a cached version, and so don't
|
||||
* want to bother with progress dialogs et al.
|
||||
*/
|
||||
public boolean download() {
|
||||
|
||||
// Can we use the cached version?
|
||||
if (verifyOrDelete(potentiallyCachedFile)) {
|
||||
delete(localFile);
|
||||
Utils.copyQuietly(potentiallyCachedFile, localFile);
|
||||
prepareApkFileAndSendCompleteMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
String remoteAddress = Utils.getApkUrl(repoAddress, curApk);
|
||||
Utils.debugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile);
|
||||
|
||||
try {
|
||||
dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, this);
|
||||
dlWrapper.download();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
onErrorDownloading();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void sendMessage(String type) {
|
||||
sendProgressEvent(new ProgressListener.Event(type));
|
||||
}
|
||||
|
||||
// TODO: Completely remove progress listener, only use broadcasts...
|
||||
private void sendProgressEvent(Event event) {
|
||||
|
||||
event.getData().putLong(EVENT_SOURCE_ID, id);
|
||||
|
||||
if (listener != null) {
|
||||
listener.onProgress(event);
|
||||
}
|
||||
|
||||
Intent intent = new Intent(ACTION_STATUS);
|
||||
intent.setData(Uri.parse(Utils.getApkUrl(repoAddress, curApk)));
|
||||
intent.putExtras(event.getData());
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onErrorDownloading() {
|
||||
delete(localFile);
|
||||
}
|
||||
|
||||
private void cacheIfRequired() {
|
||||
if (Preferences.get().shouldCacheApks()) {
|
||||
Utils.debugLog(TAG, "Copying .apk file to cache at " + potentiallyCachedFile.getAbsolutePath());
|
||||
Utils.copyQuietly(localFile, potentiallyCachedFile);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadComplete() {
|
||||
|
||||
if (!verifyOrDelete(localFile)) {
|
||||
sendProgressEvent(new Event(EVENT_ERROR));
|
||||
Toast.makeText(context, R.string.corrupt_download, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
cacheIfRequired();
|
||||
|
||||
Utils.debugLog(TAG, "Download finished: " + localFile);
|
||||
prepareApkFileAndSendCompleteMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(Event event) {
|
||||
sendProgressEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to cancel the download (if in progress) and also removes the progress
|
||||
* listener
|
||||
*/
|
||||
public void cancel() {
|
||||
if (dlWrapper != null) {
|
||||
dlWrapper.attemptCancel();
|
||||
}
|
||||
}
|
||||
|
||||
public Apk getApk() {
|
||||
return curApk;
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
|
||||
|
||||
private static final String TAG = "AsyncDownloadWrapper";
|
||||
|
||||
private static final int MSG_DOWNLOAD_COMPLETE = 2;
|
||||
private static final int MSG_ERROR = 4;
|
||||
|
||||
private final Downloader downloader;
|
||||
private DownloadThread downloadThread;
|
||||
|
||||
private final Listener listener;
|
||||
|
||||
/**
|
||||
* Normally the listener would be provided using a setListener method.
|
||||
* However for the purposes of this async downloader, it doesn't make
|
||||
* sense to have an async task without any way to notify the outside
|
||||
* world about completion. Therefore, we require the listener as a
|
||||
* parameter to the constructor.
|
||||
*/
|
||||
AsyncDownloadWrapper(Downloader downloader, Listener listener) {
|
||||
this.downloader = downloader;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void download() {
|
||||
downloadThread = new DownloadThread();
|
||||
downloadThread.start();
|
||||
}
|
||||
|
||||
public void attemptCancel() {
|
||||
if (downloader != null) {
|
||||
downloader.cancelDownload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives "messages" from the download thread, and passes them onto the
|
||||
* relevant {@link AsyncDownloader.Listener}
|
||||
*/
|
||||
public void handleMessage(Message message) {
|
||||
switch (message.arg1) {
|
||||
case MSG_DOWNLOAD_COMPLETE:
|
||||
listener.onDownloadComplete();
|
||||
break;
|
||||
case MSG_ERROR:
|
||||
listener.onErrorDownloading();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadThread extends Thread {
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
downloader.download();
|
||||
sendMessage(MSG_DOWNLOAD_COMPLETE);
|
||||
} catch (InterruptedException e) {
|
||||
// ignored
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "I/O exception in download thread", e);
|
||||
sendMessage(MSG_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(int messageType) {
|
||||
Message message = new Message();
|
||||
message.arg1 = messageType;
|
||||
AsyncDownloadWrapper.this.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
|
||||
public interface AsyncDownloader {
|
||||
|
||||
interface Listener extends ProgressListener {
|
||||
void onErrorDownloading();
|
||||
|
||||
void onDownloadComplete();
|
||||
}
|
||||
|
||||
void download();
|
||||
|
||||
void attemptCancel();
|
||||
|
||||
}
|
@ -80,7 +80,7 @@ public class BluetoothDownloader extends Downloader {
|
||||
|
||||
@Override
|
||||
public void download() throws IOException, InterruptedException {
|
||||
downloadFromStream(1024);
|
||||
downloadFromStream(1024, false);
|
||||
connection.closeQuietly();
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -10,20 +10,27 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public abstract class Downloader {
|
||||
|
||||
private static final String TAG = "Downloader";
|
||||
|
||||
public static final String LOCAL_ACTION_PROGRESS = "Downloader.PROGRESS";
|
||||
public static final String ACTION_STARTED = "org.fdroid.fdroid.net.Downloader.action.STARTED";
|
||||
public static final String ACTION_PROGRESS = "org.fdroid.fdroid.net.Downloader.action.PROGRESS";
|
||||
public static final String ACTION_INTERRUPTED = "org.fdroid.fdroid.net.Downloader.action.INTERRUPTED";
|
||||
public static final String ACTION_COMPLETE = "org.fdroid.fdroid.net.Downloader.action.COMPLETE";
|
||||
|
||||
public static final String EXTRA_ADDRESS = "extraAddress";
|
||||
public static final String EXTRA_BYTES_READ = "extraBytesRead";
|
||||
public static final String EXTRA_TOTAL_BYTES = "extraTotalBytes";
|
||||
public static final String EXTRA_DOWNLOAD_PATH = "org.fdroid.fdroid.net.Downloader.extra.DOWNLOAD_PATH";
|
||||
public static final String EXTRA_BYTES_READ = "org.fdroid.fdroid.net.Downloader.extra.BYTES_READ";
|
||||
public static final String EXTRA_TOTAL_BYTES = "org.fdroid.fdroid.net.Downloader.extra.TOTAL_BYTES";
|
||||
public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MESSAGE";
|
||||
|
||||
private volatile boolean cancelled = false;
|
||||
|
||||
private final OutputStream outputStream;
|
||||
private volatile int bytesRead;
|
||||
private volatile int totalBytes;
|
||||
private Timer timer;
|
||||
|
||||
public final File outputFile;
|
||||
|
||||
@ -39,6 +46,9 @@ public abstract class Downloader {
|
||||
void sendProgress(URL sourceUrl, int bytesRead, int totalBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* For sending download progress, should only be called in {@link #progressTask}
|
||||
*/
|
||||
private DownloaderProgressListener downloaderProgressListener;
|
||||
|
||||
protected abstract InputStream getDownloadersInputStream() throws IOException;
|
||||
@ -49,7 +59,6 @@ public abstract class Downloader {
|
||||
throws FileNotFoundException, MalformedURLException {
|
||||
this.sourceUrl = url;
|
||||
outputFile = destFile;
|
||||
outputStream = new FileOutputStream(outputFile);
|
||||
}
|
||||
|
||||
public final InputStream getInputStream() throws IOException {
|
||||
@ -89,9 +98,10 @@ public abstract class Downloader {
|
||||
|
||||
public abstract boolean isCached();
|
||||
|
||||
protected void downloadFromStream(int bufferSize) throws IOException, InterruptedException {
|
||||
protected void downloadFromStream(int bufferSize, boolean resumable) throws IOException, InterruptedException {
|
||||
Utils.debugLog(TAG, "Downloading from stream");
|
||||
InputStream input = null;
|
||||
OutputStream outputStream = new FileOutputStream(outputFile, resumable);
|
||||
try {
|
||||
input = getInputStream();
|
||||
|
||||
@ -99,7 +109,7 @@ public abstract class Downloader {
|
||||
// we were interrupted before proceeding to the download.
|
||||
throwExceptionIfInterrupted();
|
||||
|
||||
copyInputToOutputStream(input, bufferSize);
|
||||
copyInputToOutputStream(input, bufferSize, outputStream);
|
||||
} finally {
|
||||
Utils.closeQuietly(outputStream);
|
||||
Utils.closeQuietly(input);
|
||||
@ -115,11 +125,15 @@ public abstract class Downloader {
|
||||
* interrupt occured during that blocking operation. The goal is to ensure we
|
||||
* don't move onto another slow, network operation if we have cancelled the
|
||||
* download.
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
private void throwExceptionIfInterrupted() throws InterruptedException {
|
||||
if (cancelled) {
|
||||
Utils.debugLog(TAG, "Received interrupt, cancelling download");
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
}
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
@ -136,17 +150,18 @@ public abstract class Downloader {
|
||||
* keeping track of the number of bytes that have flowed through for the
|
||||
* progress counter.
|
||||
*/
|
||||
private void copyInputToOutputStream(InputStream input, int bufferSize) throws IOException, InterruptedException {
|
||||
|
||||
int bytesRead = 0;
|
||||
int totalBytes = totalDownloadSize();
|
||||
private void copyInputToOutputStream(InputStream input, int bufferSize, OutputStream output) throws IOException, InterruptedException {
|
||||
bytesRead = 0;
|
||||
totalBytes = totalDownloadSize();
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
|
||||
timer = new Timer();
|
||||
timer.scheduleAtFixedRate(progressTask, 0, 100);
|
||||
|
||||
// Getting the total download size could potentially take time, depending on how
|
||||
// it is implemented, so we may as well check this before we proceed.
|
||||
throwExceptionIfInterrupted();
|
||||
|
||||
sendProgress(bytesRead, totalBytes);
|
||||
while (true) {
|
||||
|
||||
int count;
|
||||
@ -163,21 +178,26 @@ public abstract class Downloader {
|
||||
Utils.debugLog(TAG, "Finished downloading from stream");
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += count;
|
||||
sendProgress(bytesRead, totalBytes);
|
||||
outputStream.write(buffer, 0, count);
|
||||
|
||||
output.write(buffer, 0, count);
|
||||
}
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
output.flush();
|
||||
output.close();
|
||||
}
|
||||
|
||||
private void sendProgress(int bytesRead, int totalBytes) {
|
||||
/**
|
||||
* Send progress updates on a timer to avoid flooding receivers with pointless events.
|
||||
*/
|
||||
private final TimerTask progressTask = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (downloaderProgressListener != null) {
|
||||
downloaderProgressListener.sendProgress(sourceUrl, bytesRead, totalBytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Overrides every method in {@link InputStream} and delegates to the wrapped stream.
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
@ -25,11 +25,17 @@ public class DownloaderFactory {
|
||||
throws IOException {
|
||||
File destFile = File.createTempFile("dl-", "", context.getCacheDir());
|
||||
destFile.deleteOnExit(); // this probably does nothing, but maybe...
|
||||
return create(context, new URL(urlString), destFile);
|
||||
return create(context, urlString, destFile);
|
||||
}
|
||||
|
||||
public static Downloader create(Context context, URL url, File destFile)
|
||||
public static Downloader create(Context context, Uri uri, File destFile)
|
||||
throws IOException {
|
||||
return create(context, uri.toString(), destFile);
|
||||
}
|
||||
|
||||
public static Downloader create(Context context, String urlString, File destFile)
|
||||
throws IOException {
|
||||
URL url = new URL(urlString);
|
||||
Downloader downloader = null;
|
||||
if (localBroadcastManager == null) {
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(context);
|
||||
@ -50,17 +56,6 @@ public class DownloaderFactory {
|
||||
downloader = new HttpDownloader(url, destFile, repo.username, repo.password);
|
||||
}
|
||||
}
|
||||
|
||||
downloader.setListener(new Downloader.DownloaderProgressListener() {
|
||||
@Override
|
||||
public void sendProgress(URL sourceUrl, int bytesRead, int totalBytes) {
|
||||
Intent intent = new Intent(Downloader.LOCAL_ACTION_PROGRESS);
|
||||
intent.putExtra(Downloader.EXTRA_ADDRESS, sourceUrl.toString());
|
||||
intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead);
|
||||
intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes);
|
||||
localBroadcastManager.sendBroadcast(intent);
|
||||
}
|
||||
});
|
||||
return downloader;
|
||||
}
|
||||
|
||||
@ -71,10 +66,4 @@ public class DownloaderFactory {
|
||||
private static boolean isLocalFile(URL url) {
|
||||
return "file".equalsIgnoreCase(url.getProtocol());
|
||||
}
|
||||
|
||||
public static AsyncDownloader createAsync(Context context, String urlString, File destFile, AsyncDownloader.Listener listener)
|
||||
throws IOException {
|
||||
URL url = new URL(urlString);
|
||||
return new AsyncDownloadWrapper(create(context, url, destFile), listener);
|
||||
}
|
||||
}
|
||||
|
320
app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
Normal file
320
app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
Normal file
@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
* Copyright (C) 2016 Hans-Christoph Steiner
|
||||
*
|
||||
* 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.net;
|
||||
|
||||
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.app.NotificationCompat;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* DownloaderService is a service that handles asynchronous download requests
|
||||
* (expressed as {@link Intent}s) on demand. Clients send download requests
|
||||
* through {@link android.content.Context#startService(Intent)} calls; the
|
||||
* service is started as needed, handles each Intent in turn using a worker
|
||||
* thread, and stops itself when it runs out of work.
|
||||
* <p/>
|
||||
* <p>This "work queue processor" pattern is commonly used to offload tasks
|
||||
* from an application's main thread. The DownloaderService class exists to
|
||||
* simplify this pattern and take care of the mechanics. DownloaderService
|
||||
* will receive the Intents, launch a worker thread, and stop the service as
|
||||
* appropriate.
|
||||
* <p/>
|
||||
* <p>All requests are handled on a single worker thread -- they may take as
|
||||
* long as necessary (and will not block the application's main loop), but
|
||||
* only one request will be processed at a time.
|
||||
* <p/>
|
||||
* <div class="special reference">
|
||||
* <h3>Developer Guides</h3>
|
||||
* <p>For a detailed discussion about how to create services, read the
|
||||
* <a href="{@docRoot}guide/topics/fundamentals/services.html">Services</a> developer guide.</p>
|
||||
* </div>
|
||||
*
|
||||
* @see android.os.AsyncTask
|
||||
*/
|
||||
public class DownloaderService extends Service {
|
||||
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_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;
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
|
||||
private static final HashMap<String, Integer> QUEUE_WHATS = new HashMap<String, Integer>();
|
||||
private int what;
|
||||
|
||||
private final class ServiceHandler extends Handler {
|
||||
ServiceHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
handleIntent((Intent) msg.obj);
|
||||
stopSelf(msg.arg1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.i(TAG, "onCreate");
|
||||
|
||||
HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
|
||||
thread.start();
|
||||
|
||||
serviceLooper = thread.getLooper();
|
||||
serviceHandler = new ServiceHandler(serviceLooper);
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(Intent intent, int startId) {
|
||||
super.onStart(intent, startId);
|
||||
Log.i(TAG, "onStart " + startId + " " + intent);
|
||||
String uriString = intent.getDataString();
|
||||
if (uriString == null) {
|
||||
Log.e(TAG, "Received Intent with no URI: " + intent);
|
||||
return;
|
||||
}
|
||||
if (ACTION_CANCEL.equals(intent.getAction())) {
|
||||
Log.i(TAG, "Removed " + intent);
|
||||
Integer what = QUEUE_WHATS.remove(uriString);
|
||||
if (what != null && serviceHandler.hasMessages(what)) {
|
||||
// the URL is in the queue, remove it
|
||||
serviceHandler.removeMessages(what);
|
||||
} else if (downloader != null && TextUtils.equals(uriString, downloader.sourceUrl.toString())) {
|
||||
// the URL is being downloaded, cancel it
|
||||
downloader.cancelDownload();
|
||||
} else {
|
||||
Log.e(TAG, "CANCEL called on something not queued or running: " + startId + " " + intent);
|
||||
}
|
||||
} else if (ACTION_QUEUE.equals(intent.getAction())) {
|
||||
if (Preferences.get().isUpdateNotificationEnabled()) {
|
||||
createNotification(intent.getDataString());
|
||||
}
|
||||
Log.i(TAG, "Queued " + intent);
|
||||
Message msg = serviceHandler.obtainMessage();
|
||||
msg.arg1 = startId;
|
||||
msg.obj = intent;
|
||||
msg.what = what++;
|
||||
serviceHandler.sendMessage(msg);
|
||||
Log.i(TAG, "QUEUE_WHATS.put(" + uriString + ", " + msg.what);
|
||||
QUEUE_WHATS.put(uriString, msg.what);
|
||||
} else {
|
||||
Log.e(TAG, "Received Intent with unknown action: " + intent);
|
||||
}
|
||||
}
|
||||
|
||||
private void createNotification(String urlString) {
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(this)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(getString(R.string.downloading))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentText(urlString);
|
||||
startForeground(NOTIFY_DOWNLOADING, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
onStart(intent, startId);
|
||||
Log.i(TAG, "onStartCommand " + intent);
|
||||
return START_REDELIVER_INTENT; // if killed before completion, retry Intent
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.i(TAG, "onDestroy");
|
||||
stopForeground(true);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is invoked on the worker thread with a request to process.
|
||||
* Only one Intent is processed at a time, but the processing happens on a
|
||||
* worker thread that runs independently from other application logic.
|
||||
* So, if this code takes a long time, it will hold up other requests to
|
||||
* the same DownloaderService, but it will not hold up anything else.
|
||||
* When all requests have been handled, the DownloaderService stops itself,
|
||||
* so you should not ever call {@link #stopSelf}.
|
||||
* <p/>
|
||||
* Downloads are put into subdirectories based on hostname/port of each repo
|
||||
* to prevent files with the same names from conflicting. Each repo enforces
|
||||
* unique APK file names on the server side.
|
||||
*
|
||||
* @param intent The {@link Intent} passed via {@link
|
||||
* android.content.Context#startService(Intent)}.
|
||||
*/
|
||||
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());
|
||||
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile);
|
||||
try {
|
||||
downloader = DownloaderFactory.create(this, uri, localFile);
|
||||
downloader.setListener(new Downloader.DownloaderProgressListener() {
|
||||
@Override
|
||||
public void sendProgress(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);
|
||||
}
|
||||
});
|
||||
downloader.download();
|
||||
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile);
|
||||
DownloadCompleteService.notify(this, intent.getStringExtra(EXTRA_PACKAGE_NAME),
|
||||
intent.getDataString());
|
||||
} catch (InterruptedException e) {
|
||||
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile,
|
||||
e.getLocalizedMessage());
|
||||
} finally {
|
||||
if (downloader != null) {
|
||||
downloader.close();
|
||||
}
|
||||
}
|
||||
downloader = null;
|
||||
}
|
||||
|
||||
private void sendBroadcast(Uri uri, String action, File file) {
|
||||
sendBroadcast(uri, action, file, null);
|
||||
}
|
||||
|
||||
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 (!TextUtils.isEmpty(errorMessage)) {
|
||||
intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, errorMessage);
|
||||
}
|
||||
localBroadcastManager.sendBroadcast(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a URL to the download queue.
|
||||
* <p/>
|
||||
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
|
||||
*
|
||||
* @param context
|
||||
* @param packageName The packageName of the app being downloaded
|
||||
* @param urlString The URL to add to the download queue
|
||||
* @see #cancel(Context, String)
|
||||
*/
|
||||
public static void queue(Context context, String packageName, String urlString) {
|
||||
Log.i(TAG, "queue " + urlString);
|
||||
Intent intent = new Intent(context, DownloaderService.class);
|
||||
intent.setAction(ACTION_QUEUE);
|
||||
intent.setData(Uri.parse(urlString));
|
||||
if (!TextUtils.isEmpty(EXTRA_PACKAGE_NAME)) {
|
||||
intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
|
||||
}
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a URL to the download queue, even if it is currently downloading.
|
||||
* <p/>
|
||||
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
|
||||
*
|
||||
* @param context
|
||||
* @param urlString The URL to remove from the download queue
|
||||
* @see #queue(Context, String, String)
|
||||
*/
|
||||
public static void cancel(Context context, String urlString) {
|
||||
Log.i(TAG, "cancel " + urlString);
|
||||
Intent intent = new Intent(context, DownloaderService.class);
|
||||
intent.setAction(ACTION_CANCEL);
|
||||
intent.setData(Uri.parse(urlString));
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL is waiting in the queue for downloading or if actively
|
||||
* being downloaded. This is useful for checking whether to re-register
|
||||
* {@link android.content.BroadcastReceiver}s in
|
||||
* {@link android.app.Activity#onResume()}
|
||||
*/
|
||||
public static boolean isQueuedOrActive(String urlString) {
|
||||
if (TextUtils.isEmpty(urlString)) {
|
||||
return false;
|
||||
}
|
||||
Integer what = QUEUE_WHATS.get(urlString);
|
||||
return (what != null && serviceHandler.hasMessages(what))
|
||||
|| (downloader != null && TextUtils.equals(urlString, downloader.sourceUrl.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a prepared {@link IntentFilter} for use for matching this service's action events.
|
||||
*
|
||||
* @param urlString The full file URL to match.
|
||||
* @param action {@link Downloader#ACTION_STARTED}, {@link Downloader#ACTION_PROGRESS},
|
||||
* {@link Downloader#ACTION_INTERRUPTED}, or {@link Downloader#ACTION_COMPLETE},
|
||||
* @return
|
||||
*/
|
||||
public static IntentFilter getIntentFilter(String urlString, String action) {
|
||||
Uri uri = Uri.parse(urlString);
|
||||
IntentFilter intentFilter = new IntentFilter(action);
|
||||
intentFilter.addDataScheme(uri.getScheme());
|
||||
intentFilter.addDataAuthority(uri.getHost(), String.valueOf(uri.getPort()));
|
||||
intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
|
||||
return intentFilter;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package org.fdroid.fdroid.net;
|
||||
|
||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.spongycastle.util.encoders.Base64;
|
||||
@ -65,7 +66,7 @@ public class HttpDownloader extends Downloader {
|
||||
*/
|
||||
@Override
|
||||
protected InputStream getDownloadersInputStream() throws IOException {
|
||||
setupConnection();
|
||||
setupConnection(false);
|
||||
return new BufferedInputStream(connection.getInputStream());
|
||||
}
|
||||
|
||||
@ -79,8 +80,25 @@ public class HttpDownloader extends Downloader {
|
||||
*/
|
||||
@Override
|
||||
public void download() throws IOException, InterruptedException {
|
||||
setupConnection();
|
||||
doDownload();
|
||||
boolean resumable = false;
|
||||
long fileLength = outputFile.length();
|
||||
|
||||
// get the file size from the server
|
||||
HttpURLConnection tmpConn = getConnection();
|
||||
int contentLength = -1;
|
||||
if (tmpConn.getResponseCode() == 200) {
|
||||
contentLength = tmpConn.getContentLength();
|
||||
}
|
||||
tmpConn.disconnect();
|
||||
if (fileLength > contentLength) {
|
||||
FileUtils.deleteQuietly(outputFile);
|
||||
} else if (fileLength == contentLength && outputFile.isFile()) {
|
||||
return; // already have it!
|
||||
} else if (fileLength > 0) {
|
||||
resumable = true;
|
||||
}
|
||||
setupConnection(resumable);
|
||||
doDownload(resumable);
|
||||
}
|
||||
|
||||
private boolean isSwapUrl() {
|
||||
@ -90,10 +108,8 @@ public class HttpDownloader extends Downloader {
|
||||
&& FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are
|
||||
}
|
||||
|
||||
protected void setupConnection() throws IOException {
|
||||
if (connection != null) {
|
||||
return;
|
||||
}
|
||||
private HttpURLConnection getConnection() throws IOException {
|
||||
HttpURLConnection connection;
|
||||
if (isSwapUrl()) {
|
||||
// swap never works with a proxy, its unrouted IP on the same subnet
|
||||
connection = (HttpURLConnection) sourceUrl.openConnection();
|
||||
@ -113,9 +129,25 @@ public class HttpDownloader extends Downloader {
|
||||
String authString = username + ":" + password;
|
||||
connection.setRequestProperty("Authorization", "Basic " + Base64.toBase64String(authString.getBytes()));
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
protected void doDownload() throws IOException, InterruptedException {
|
||||
/**
|
||||
* @return Whether the connection is resumable or not
|
||||
*/
|
||||
protected void setupConnection(boolean resumable) throws IOException {
|
||||
if (connection != null) {
|
||||
return;
|
||||
}
|
||||
connection = getConnection();
|
||||
|
||||
if (resumable) {
|
||||
// partial file exists, resume the download
|
||||
connection.setRequestProperty("Range", "bytes=" + outputFile.length() + "-");
|
||||
}
|
||||
}
|
||||
|
||||
protected void doDownload(boolean resumable) throws IOException, InterruptedException {
|
||||
if (wantToCheckCache()) {
|
||||
setupCacheCheck();
|
||||
Utils.debugLog(TAG, "Checking cached status of " + sourceUrl);
|
||||
@ -125,8 +157,8 @@ public class HttpDownloader extends Downloader {
|
||||
if (isCached()) {
|
||||
Utils.debugLog(TAG, sourceUrl + " is cached, so not downloading (HTTP " + statusCode + ")");
|
||||
} else {
|
||||
Utils.debugLog(TAG, "Downloading from " + sourceUrl);
|
||||
downloadFromStream(4096);
|
||||
Utils.debugLog(TAG, "doDownload for " + sourceUrl + " " + resumable);
|
||||
downloadFromStream(8192, resumable);
|
||||
updateCacheCheck();
|
||||
}
|
||||
}
|
||||
@ -166,6 +198,8 @@ public class HttpDownloader extends Downloader {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ public class LocalFileDownloader extends Downloader {
|
||||
|
||||
@Override
|
||||
public void download() throws IOException, InterruptedException {
|
||||
downloadFromStream(1024 * 50);
|
||||
downloadFromStream(1024 * 50, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -36,6 +36,7 @@ import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
@ -49,8 +50,8 @@ import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.localrepo.SwapService;
|
||||
import org.fdroid.fdroid.net.ApkDownloader;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
@ -231,6 +232,7 @@ public class SwapAppsView extends ListView implements
|
||||
|
||||
private class ViewHolder {
|
||||
|
||||
private final LocalBroadcastManager localBroadcastManager;
|
||||
private App app;
|
||||
|
||||
@Nullable
|
||||
@ -247,13 +249,6 @@ public class SwapAppsView extends ListView implements
|
||||
private final BroadcastReceiver downloadProgressReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Apk apk = getApkToInstall();
|
||||
String broadcastUrl = intent.getStringExtra(Downloader.EXTRA_ADDRESS);
|
||||
|
||||
if (apk != null && apk.repoAddress != null && !TextUtils.equals(Utils.getApkUrl(apk.repoAddress, apk), broadcastUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int read = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
|
||||
int total = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
|
||||
if (total > 0) {
|
||||
@ -267,21 +262,25 @@ public class SwapAppsView extends ListView implements
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver apkDownloadReceiver = new BroadcastReceiver() {
|
||||
private final BroadcastReceiver appListViewResetReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Apk apk = getApkToInstall();
|
||||
|
||||
// Note: This can also be done by using the build in IntentFilter.matchData()
|
||||
// functionality, matching against the Intent.getData() of the incoming intent.
|
||||
// I've chosen to do this way, because otherwise we need to query the database
|
||||
// once for each ViewHolder in order to get the repository address for the
|
||||
// apkToInstall. This way, we can wait until we receive an incoming intent (if
|
||||
// at all) and then lazily load the apk to install.
|
||||
String broadcastUrl = intent.getDataString();
|
||||
if (TextUtils.equals(Utils.getApkUrl(apk.repoAddress, apk), broadcastUrl)) {
|
||||
resetView();
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.hasExtra(Downloader.EXTRA_ERROR_MESSAGE)) {
|
||||
String msg = intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)
|
||||
+ " " + intent.getDataString();
|
||||
Toast.makeText(context, R.string.download_error, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
|
||||
} else { // user canceled
|
||||
Toast.makeText(context, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
resetView();
|
||||
}
|
||||
};
|
||||
|
||||
@ -299,11 +298,19 @@ public class SwapAppsView extends ListView implements
|
||||
|
||||
ViewHolder() {
|
||||
// TODO: Unregister receivers correctly...
|
||||
IntentFilter apkFilter = new IntentFilter(ApkDownloader.ACTION_STATUS);
|
||||
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(apkDownloadReceiver, apkFilter);
|
||||
|
||||
IntentFilter progressFilter = new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS);
|
||||
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(downloadProgressReceiver, progressFilter);
|
||||
Apk apk = getApkToInstall();
|
||||
String url = Utils.getApkUrl(apk.repoAddress, apk);
|
||||
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(getActivity());
|
||||
localBroadcastManager.registerReceiver(appListViewResetReceiver,
|
||||
DownloaderService.getIntentFilter(url, Downloader.ACTION_STARTED));
|
||||
localBroadcastManager.registerReceiver(downloadProgressReceiver,
|
||||
DownloaderService.getIntentFilter(url, Downloader.ACTION_PROGRESS));
|
||||
localBroadcastManager.registerReceiver(appListViewResetReceiver,
|
||||
DownloaderService.getIntentFilter(url, Downloader.ACTION_COMPLETE));
|
||||
localBroadcastManager.registerReceiver(interruptedReceiver,
|
||||
DownloaderService.getIntentFilter(url, Downloader.ACTION_INTERRUPTED));
|
||||
}
|
||||
|
||||
public void setApp(@NonNull App app) {
|
||||
|
@ -2,6 +2,7 @@ package org.fdroid.fdroid.views.swap;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
@ -34,7 +35,6 @@ import com.google.zxing.integration.android.IntentResult;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.NfcHelper;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
@ -45,7 +45,8 @@ import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.localrepo.LocalRepoManager;
|
||||
import org.fdroid.fdroid.localrepo.SwapService;
|
||||
import org.fdroid.fdroid.localrepo.peers.Peer;
|
||||
import org.fdroid.fdroid.net.ApkDownloader;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
@ -116,6 +117,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
private boolean hasPreparedLocalRepo;
|
||||
private PrepareSwapRepo updateSwappableAppsTask;
|
||||
private NewRepoConfig confirmSwapConfig;
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
private BroadcastReceiver downloadCompleteReceiver;
|
||||
|
||||
@NonNull
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@ -181,6 +184,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
|
||||
container = (ViewGroup) findViewById(R.id.fragment_container);
|
||||
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
|
||||
new SwapDebug().logStatus();
|
||||
}
|
||||
|
||||
@ -777,22 +782,21 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
public void install(@NonNull final App app) {
|
||||
final Apk apkToInstall = ApkProvider.Helper.find(this, app.packageName, app.suggestedVercode);
|
||||
final ApkDownloader downloader = new ApkDownloader(this, app, apkToInstall, apkToInstall.repoAddress);
|
||||
downloader.setProgressListener(new ProgressListener() {
|
||||
final Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVercode);
|
||||
String urlString = Utils.getApkUrl(apk.repoAddress, apk);
|
||||
downloadCompleteReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onProgress(Event event) {
|
||||
switch (event.type) {
|
||||
case ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE:
|
||||
handleDownloadComplete(downloader.localFile(), app.packageName);
|
||||
break;
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String path = intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH);
|
||||
handleDownloadComplete(new File(path), app.packageName, intent.getDataString());
|
||||
}
|
||||
}
|
||||
});
|
||||
downloader.download();
|
||||
};
|
||||
localBroadcastManager.registerReceiver(downloadCompleteReceiver,
|
||||
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE));
|
||||
DownloaderService.queue(this, app.packageName, urlString);
|
||||
}
|
||||
|
||||
private void handleDownloadComplete(File apkFile, String packageName) {
|
||||
private void handleDownloadComplete(File apkFile, String packageName, String urlString) {
|
||||
|
||||
try {
|
||||
Installer.getActivityInstaller(this, new Installer.InstallerCallback() {
|
||||
@ -807,8 +811,9 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
public void onError(int operation, int errorCode) {
|
||||
// TODO: Boo!
|
||||
}
|
||||
}).installPackage(apkFile, packageName);
|
||||
} catch (Installer.AndroidNotCompatibleException e) {
|
||||
}).installPackage(apkFile, packageName, urlString);
|
||||
localBroadcastManager.unregisterReceiver(downloadCompleteReceiver);
|
||||
} catch (Installer.InstallFailedException e) {
|
||||
// TODO: Handle exception properly
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,8 @@
|
||||
<string name="update_interval_zero">No automatic app list updates</string>
|
||||
<string name="automatic_scan_wifi">Only on Wi-Fi</string>
|
||||
<string name="automatic_scan_wifi_on">Update app lists automatically only on Wi-Fi</string>
|
||||
<string name="update_auto_download">Automatically download updates</string>
|
||||
<string name="update_auto_download_summary">Download the update files in the background</string>
|
||||
<string name="notify">Update notifications</string>
|
||||
<string name="notify_on">Show a notification when updates are available</string>
|
||||
<string name="update_history">Update history</string>
|
||||
@ -339,6 +341,8 @@
|
||||
<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="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?
|
||||
It will get access to:</string>
|
||||
<string name="install_confirm_no_perms">Do you want to install this application?
|
||||
|
@ -9,6 +9,10 @@
|
||||
<CheckBoxPreference android:title="@string/automatic_scan_wifi"
|
||||
android:defaultValue="false"
|
||||
android:key="updateOnWifiOnly" />
|
||||
<CheckBoxPreference android:title="@string/update_auto_download"
|
||||
android:summary="@string/update_auto_download_summary"
|
||||
android:defaultValue="false"
|
||||
android:key="updateAutoDownload" />
|
||||
<CheckBoxPreference android:title="@string/notify"
|
||||
android:defaultValue="true"
|
||||
android:key="updateNotify" />
|
||||
|
BIN
app/src/test/assets/urzip.apk
Normal file
BIN
app/src/test/assets/urzip.apk
Normal file
Binary file not shown.
@ -0,0 +1,51 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AndroidXMLDecompressTest {
|
||||
|
||||
String[] testDirNames = {
|
||||
System.getProperty("user.dir") + "/src/test/assets",
|
||||
System.getProperty("user.dir") + "/build/outputs/apk",
|
||||
System.getenv("HOME") + "/fdroid/repo",
|
||||
};
|
||||
|
||||
FilenameFilter apkFilter = new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String filename) {
|
||||
return filename.endsWith(".apk");
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testParseVersionCode() throws IOException {
|
||||
for (File f : getFilesToTest()) {
|
||||
System.out.println("\n" + f);
|
||||
Map<String, Object> map = AndroidXMLDecompress.getManifestHeaderAttributes(f.getAbsolutePath());
|
||||
for (String key : map.keySet()) {
|
||||
System.out.println(key + "=\"" + map.get(key) + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<File> getFilesToTest() {
|
||||
ArrayList<File> apkFiles = new ArrayList<File>(5);
|
||||
for (String dirName : testDirNames) {
|
||||
System.out.println("looking in " + dirName);
|
||||
File dir = new File(dirName);
|
||||
File[] files = dir.listFiles(apkFilter);
|
||||
if (files != null) {
|
||||
apkFiles.addAll(Arrays.asList(files));
|
||||
}
|
||||
}
|
||||
return apkFiles;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user