From b9dad4bce6c76249dc55ca4da7f91ff3c98ec5fd Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 10 Nov 2016 11:02:32 +0100 Subject: [PATCH 1/7] handle dirs and I/O errors when parsing ACTION_PACKAGE_ADDED Intents InstalledAppProviderService tries to keep a running log of what is actually installed on the device. It seems that ApplicationInfo.sourceDir and related things sometimes returns a dir rather than an APK. So try to find an APK in that folder. closes #801 --- .../data/InstalledAppProviderService.java | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java index 74aeb18c4..34616230a 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -11,11 +11,13 @@ import android.net.Uri; import android.os.Process; import android.support.annotation.Nullable; +import org.acra.ACRA; import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Schema.InstalledAppTable; import java.io.File; +import java.io.FilenameFilter; import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Map; @@ -154,24 +156,49 @@ public class InstalledAppProviderService extends IntentService { @Override protected void onHandleIntent(Intent intent) { Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); - if (intent != null) { - String packageName = intent.getData().getSchemeSpecificPart(); - final String action = intent.getAction(); - if (ACTION_INSERT.equals(action)) { - PackageInfo packageInfo = getPackageInfo(intent, packageName); - if (packageInfo != null) { - File apk = new File(packageInfo.applicationInfo.publicSourceDir); - if (apk.exists() && apk.canRead()) { + if (intent == null) { + return; + } + + String packageName = intent.getData().getSchemeSpecificPart(); + final String action = intent.getAction(); + if (ACTION_INSERT.equals(action)) { + PackageInfo packageInfo = getPackageInfo(intent, packageName); + if (packageInfo != null) { + File apk = new File(packageInfo.applicationInfo.publicSourceDir); + if (apk.isDirectory()) { + FilenameFilter filter = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".apk"); + } + }; + File[] files = apk.listFiles(filter); + if (files == null) { + String msg = packageName + " sourceDir has no APKs: " + + apk.getAbsolutePath(); + Utils.debugLog(TAG, msg); + ACRA.getErrorReporter().handleException(new IllegalArgumentException(msg), false); + return; + } + apk = files[0]; + } + if (apk.exists() && apk.canRead()) { + try { String hashType = "sha256"; String hash = Utils.getBinaryHash(apk, hashType); insertAppIntoDb(this, packageInfo, hashType, hash); + } catch (IllegalArgumentException e) { + Utils.debugLog(TAG, e.getMessage()); + ACRA.getErrorReporter().handleException(e, false); + return; } } - } else if (ACTION_DELETE.equals(action)) { - deleteAppFromDb(this, packageName); } - notifyEvents.onNext(null); + } else if (ACTION_DELETE.equals(action)) { + deleteAppFromDb(this, packageName); } + notifyEvents.onNext(null); } /** From 096b7132c4c79f12dc55addf72fb30e7a8960030 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 10 Nov 2016 12:55:35 +0100 Subject: [PATCH 2/7] prevent AppDetailsHeaderFragment crash in startProgress() The real solution would involve figuring out where to handle this in the right spot in the lifecycle. Since AppDetails is being totally replaced, this is just to stop the crashing. closes #802 --- app/src/main/java/org/fdroid/fdroid/AppDetails.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index 524b35a09..1def0e6ed 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -1525,8 +1525,10 @@ public class AppDetails extends AppCompatActivity { public void startProgress(boolean allowCancel) { cancelButton.setVisibility(allowCancel ? View.VISIBLE : View.GONE); - showIndeterminateProgress(getString(R.string.download_pending)); - updateViews(); + if (isAdded()) { + showIndeterminateProgress(getString(R.string.download_pending)); + updateViews(); + } } private void showIndeterminateProgress(String message) { From ab1e869ebe49c13b182873868607db36dc971b8f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 10 Nov 2016 13:30:06 +0100 Subject: [PATCH 3/7] use HEAD request when just checking the file size This code will be changed again when implementing the client-side etag check #562 closes #777 --- app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java index 04bf202fa..3780bfd53 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java @@ -83,6 +83,7 @@ public class HttpDownloader extends Downloader { // get the file size from the server HttpURLConnection tmpConn = getConnection(); + tmpConn.setRequestMethod("HEAD"); int contentLength = -1; if (tmpConn.getResponseCode() == 200) { contentLength = tmpConn.getContentLength(); From 6545a26e316edf5db9f6c73e296fe6094534b392 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 10 Nov 2016 16:54:27 +0100 Subject: [PATCH 4/7] set HTTP User Agent to "F-Droid" First, this is more honest than just using the default since it is saying what the actual software is. Second, it protects identity, since the default User Agent on Android can have a lot of info in it, for example: "Dalvik/2.1.0 (Linux; U; Android 5.1; XT1039 Build/LPBS23.13-17.3-1)" --- app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java index 3780bfd53..d792ed2b0 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java @@ -3,6 +3,7 @@ package org.fdroid.fdroid.net; import com.nostra13.universalimageloader.core.download.BaseImageDownloader; import org.apache.commons.io.FileUtils; +import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Utils; import org.spongycastle.util.encoders.Base64; @@ -116,6 +117,8 @@ public class HttpDownloader extends Downloader { connection = NetCipher.getHttpURLConnection(sourceUrl); } + connection.setRequestProperty("User-Agent", "F-Droid " + BuildConfig.VERSION_NAME); + if (username != null && password != null) { // add authorization header from username / password if set String authString = username + ":" + password; From e14cb9d16ab364dde96b017e1d0656b81e4df014 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 10 Nov 2016 14:28:35 +0100 Subject: [PATCH 5/7] treat ethernet as WiFi when checking updates Its really easy to use USB Ethernet devices with ChromeOS and some Android devices like Android TV. ChromeOS now supports Android apps. Since really the goal is to avoid metered networks, and ethernet is very rarely metered, this fits in with the user expectations around the preference. And if it doesn't, there are very few people using Ethernet with F-Droid right now, so whatever harm does happen will affect an extremely limited number of people. --- .../main/java/org/fdroid/fdroid/UpdateService.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 143f67d19..ff8c810f9 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -294,11 +294,14 @@ public class UpdateService extends IntentService { return false; } - if (activeNetwork.getType() != ConnectivityManager.TYPE_WIFI && Preferences.get().isUpdateOnlyOnWifi()) { - Log.i(TAG, "Skipping update - wifi not available"); - return false; + int networkType = activeNetwork.getType(); + switch (networkType) { + case ConnectivityManager.TYPE_ETHERNET: + case ConnectivityManager.TYPE_WIFI: + return activeNetwork.isConnectedOrConnecting(); + default: + return Preferences.get().isUpdateOnlyOnWifi(); } - return activeNetwork.isConnectedOrConnecting(); } @Override From 7e7ec966ee33958c26d80324047f313f5d5e6cdc Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 10 Nov 2016 16:00:11 +0100 Subject: [PATCH 6/7] improved internet state handling for updates, including metered This introduces three network states: 1. completely disconnected 2. connected only via metered networks 3. connected via unlimited networks This allows the update process to use bandwidth better, especially when the user has enabled the "Only on WiFi" setting. It also helps prevent silly, cryptic error messages in the update process is triggered when there isn't internet available. I tested this with: * 4G only, but not set up for internet * 4G only, with internet * 4G + WiFi * WiFi only airplane mode * no internet at all, full airplane mode closes #793 closes #774 --- .../java/org/fdroid/fdroid/Preferences.java | 2 +- .../java/org/fdroid/fdroid/UpdateService.java | 63 +++++++++++++++---- app/src/main/res/values/strings.xml | 3 +- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index 7a5b91bed..cfe402d47 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -231,7 +231,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh return preferences.getBoolean(PREF_AUTO_DOWNLOAD_INSTALL_UPDATES, false); } - public boolean isUpdateOnlyOnWifi() { + public boolean isUpdateOnlyOnUnmeteredNetworks() { return preferences.getBoolean(PREF_UPD_WIFI_ONLY, false); } diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index ff8c810f9..6586bae3f 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -31,6 +31,8 @@ import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.os.Process; import android.os.SystemClock; import android.preference.PreferenceManager; @@ -79,6 +81,12 @@ public class UpdateService extends IntentService { private static final int NOTIFY_ID_UPDATING = 0; private static final int NOTIFY_ID_UPDATES_AVAILABLE = 1; + private static final int FLAG_NET_UNAVAILABLE = 0; + private static final int FLAG_NET_METERED = 1; + private static final int FLAG_NET_NO_LIMIT = 2; + + private static Handler toastHandler; + private NotificationManager notificationManager; private NotificationCompat.Builder notificationBuilder; @@ -279,31 +287,52 @@ public class UpdateService extends IntentService { return false; } - return isNetworkAvailableForUpdate(this); + return true; } /** - * If we are to update the repos only on wifi, make sure that connection is active + * Gets the state of internet availability, whether there is no connection at all, + * whether the connection has no usage limit (like most WiFi), or whether this is + * a metered connection like most cellular plans or hotspot WiFi connections. */ - private static boolean isNetworkAvailableForUpdate(Context context) { + private static int getNetworkState(Context context) { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - // this could be cellular or wifi NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); - if (activeNetwork == null) { - return false; + if (activeNetwork == null || !activeNetwork.isConnected()) { + return FLAG_NET_UNAVAILABLE; } int networkType = activeNetwork.getType(); switch (networkType) { case ConnectivityManager.TYPE_ETHERNET: case ConnectivityManager.TYPE_WIFI: - return activeNetwork.isConnectedOrConnecting(); + if (Build.VERSION.SDK_INT >= 16 && cm.isActiveNetworkMetered()) { + return FLAG_NET_METERED; + } else { + return FLAG_NET_NO_LIMIT; + } default: - return Preferences.get().isUpdateOnlyOnWifi(); + return FLAG_NET_METERED; } } + /** + * In order to send a {@link Toast} from a {@link IntentService}, we have to do these tricks. + */ + private void sendNoInternetToast() { + if (toastHandler == null) { + toastHandler = new Handler(Looper.getMainLooper()); + } + toastHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), + R.string.warning_no_internet, Toast.LENGTH_SHORT).show(); + } + }); + } + @Override protected void onHandleIntent(Intent intent) { Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); @@ -318,9 +347,21 @@ public class UpdateService extends IntentService { try { // See if it's time to actually do anything yet... - if (manualUpdate) { - Utils.debugLog(TAG, "Unscheduled (manually requested) update"); - } else if (!verifyIsTimeForScheduledRun()) { + int netState = getNetworkState(this); + if (netState == FLAG_NET_UNAVAILABLE) { + Utils.debugLog(TAG, "No internet, cannot update"); + if (manualUpdate) { + sendNoInternetToast(); + } + return; + } + + if (manualUpdate || (netState == FLAG_NET_NO_LIMIT)) { + // triggered by the user, or by connecting to WiFi, etc. + Utils.debugLog(TAG, "manually requested update or on unlimited internet"); + } else if (Preferences.get().isUpdateOnlyOnUnmeteredNetworks() + || !verifyIsTimeForScheduledRun()) { + Utils.debugLog(TAG, "don't run update, we're on metered internet"); return; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d23295a97..fdb4542a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,7 +21,7 @@ Automatic update interval No automatic app list updates Only on Wi-Fi - Update app lists automatically only on Wi-Fi + Only update automatically on unmetered networks like Wi-Fi Automatically download updates Download the update files in the background Automatically install updates @@ -201,6 +201,7 @@ All repositories are up to date All other repos didn\'t create errors. Error during update: %s + Cannot update, are you connected to the internet? No permissions are used. Permissions You don\'t have any available app that can handle %s. From b799fe84945a31de6c0ec35747622c6d714ef62a Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 15 Nov 2016 21:36:05 +0100 Subject: [PATCH 7/7] follow "Only on WiFi" preference strictly This is a tested version of pserwlyo's suggestion in !415 https://gitlab.com/fdroid/fdroidclient/merge_requests/415#note_18469122 --- .../main/java/org/fdroid/fdroid/UpdateService.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 6586bae3f..fbf780caa 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -356,12 +356,11 @@ public class UpdateService extends IntentService { return; } - if (manualUpdate || (netState == FLAG_NET_NO_LIMIT)) { - // triggered by the user, or by connecting to WiFi, etc. - Utils.debugLog(TAG, "manually requested update or on unlimited internet"); - } else if (Preferences.get().isUpdateOnlyOnUnmeteredNetworks() - || !verifyIsTimeForScheduledRun()) { - Utils.debugLog(TAG, "don't run update, we're on metered internet"); + if (manualUpdate) { + Utils.debugLog(TAG, "manually requested update"); + } else if (!verifyIsTimeForScheduledRun() + || (netState == FLAG_NET_METERED && Preferences.get().isUpdateOnlyOnUnmeteredNetworks())) { + Utils.debugLog(TAG, "don't run update"); return; }