diff --git a/app/src/main/java/org/fdroid/fdroid/CompatibilityChecker.java b/app/src/main/java/org/fdroid/fdroid/CompatibilityChecker.java index 0c007a38d..1cd3db664 100644 --- a/app/src/main/java/org/fdroid/fdroid/CompatibilityChecker.java +++ b/app/src/main/java/org/fdroid/fdroid/CompatibilityChecker.java @@ -7,8 +7,6 @@ import android.content.pm.PackageManager; import android.os.Build; import android.preference.PreferenceManager; import android.support.annotation.Nullable; -import android.text.TextUtils; - import org.fdroid.fdroid.compat.SupportedArchitectures; import org.fdroid.fdroid.data.Apk; @@ -22,12 +20,11 @@ import java.util.Set; // find reasons why an apk may be incompatible with the user's device. public class CompatibilityChecker { - private static final String TAG = "Compatibility"; + public static final String TAG = "Compatibility"; private final Context context; private final Set features; private final String[] cpuAbis; - private final String cpuAbisDesc; private final boolean forceTouchApps; public CompatibilityChecker(Context ctx) { @@ -43,13 +40,6 @@ public class CompatibilityChecker { if (pm != null) { final FeatureInfo[] featureArray = pm.getSystemAvailableFeatures(); if (featureArray != null) { - if (BuildConfig.DEBUG) { - StringBuilder logMsg = new StringBuilder("Available device features:"); - for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { - logMsg.append('\n').append(fi.name); - } - Utils.debugLog(TAG, logMsg.toString()); - } for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { features.add(fi.name); } @@ -57,18 +47,6 @@ public class CompatibilityChecker { } cpuAbis = SupportedArchitectures.getAbis(); - - StringBuilder builder = new StringBuilder(); - boolean first = true; - for (final String abi : cpuAbis) { - if (first) { - first = false; - } else { - builder.append(", "); - } - builder.append(abi); - } - cpuAbisDesc = builder.toString(); } private boolean compatibleApi(@Nullable String[] nativecode) { @@ -107,16 +85,11 @@ public class CompatibilityChecker { } if (!features.contains(feat)) { Collections.addAll(incompatibleReasons, feat.split(",")); - Utils.debugLog(TAG, apk.packageName + " vercode " + apk.versionCode - + " is incompatible based on lack of " + feat); } } } if (!compatibleApi(apk.nativecode)) { Collections.addAll(incompatibleReasons, apk.nativecode); - Utils.debugLog(TAG, apk.packageName + " vercode " + apk.versionCode - + " only supports " + TextUtils.join(", ", apk.nativecode) - + " while your architectures are " + cpuAbisDesc); } return incompatibleReasons; diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index fc16184b8..c68f6737c 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -36,6 +36,7 @@ import android.graphics.Bitmap; import android.os.Build; import android.os.Environment; import android.os.StrictMode; +import android.support.v4.util.LongSparseArray; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -113,7 +114,9 @@ public class FDroidApp extends Application { public static volatile int networkState = ConnectivityMonitorService.FLAG_NET_UNAVAILABLE; - private static volatile String lastWorkingMirror = null; + public static final SubnetUtils.SubnetInfo UNSET_SUBNET_INFO = new SubnetUtils("0.0.0.0/32").getInfo(); + + private static volatile LongSparseArray lastWorkingMirrorArray = new LongSparseArray<>(1); private static volatile int numTries = Integer.MAX_VALUE; private static volatile int timeout = 10000; @@ -230,7 +233,7 @@ public class FDroidApp extends Application { public static void initWifiSettings() { port = 8888; ipAddressString = null; - subnetInfo = new SubnetUtils("0.0.0.0/32").getInfo(); + subnetInfo = UNSET_SUBNET_INFO; ssid = ""; bssid = ""; repo = new Repo(); @@ -242,6 +245,7 @@ public class FDroidApp extends Application { public static String getMirror(String urlString, Repo repo2) throws IOException { if (repo2.hasMirrors()) { + String lastWorkingMirror = lastWorkingMirrorArray.get(repo2.getId()); if (lastWorkingMirror == null) { lastWorkingMirror = repo2.address; } @@ -264,7 +268,7 @@ public class FDroidApp extends Application { String newUrl = urlString.replace(lastWorkingMirror, mirror); Utils.debugLog(TAG, "Trying mirror " + mirror + " after " + lastWorkingMirror + " failed," + " timeout=" + timeout / 1000 + "s"); - lastWorkingMirror = mirror; + lastWorkingMirrorArray.put(repo2.getId(), mirror); numTries--; return newUrl; } else { @@ -278,7 +282,9 @@ public class FDroidApp extends Application { public static void resetMirrorVars() { // Reset last working mirror, numtries, and timeout - lastWorkingMirror = null; + for (int i = 0; i < lastWorkingMirrorArray.size(); i++) { + lastWorkingMirrorArray.removeAt(i); + } numTries = Integer.MAX_VALUE; timeout = 10000; } diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java index 37a6a07d6..5da337996 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -18,6 +18,7 @@ import org.fdroid.fdroid.data.Schema.InstalledAppTable; import org.fdroid.fdroid.data.Schema.InstalledAppTable.Cols; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; public class InstalledAppProvider extends FDroidProvider { @@ -81,6 +82,20 @@ public class InstalledAppProvider extends FDroidProvider { private static final UriMatcher MATCHER = new UriMatcher(-1); + /** + * Built-in apps that are signed by the various Android ROM keys. + * + * @see Certificates and private keys + */ + private static final String[] SYSTEM_PACKAGES = { + "android", // platform key + "com.android.email", // test/release key + "com.android.contacts", // shared key + "com.android.providers.downloads", // media key + }; + + private static String[] systemSignatures; + static { MATCHER.addURI(getAuthority(), null, CODE_LIST); MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", CODE_SEARCH); @@ -117,6 +132,36 @@ public class InstalledAppProvider extends FDroidProvider { return packageName; // all else fails, return packageName } + /** + * Add SQL selection statement to exclude {@link InstalledApp}s that were + * signed by the platform/shared/media/testkey keys. + * + * @see Certificates and private keys + */ + private QuerySelection selectNotSystemSignature(QuerySelection selection) { + if (systemSignatures == null) { + Log.i(TAG, "selectNotSystemSignature: systemSignature == null, querying for it"); + HashSet signatures = new HashSet<>(); + for (String packageName : SYSTEM_PACKAGES) { + Cursor cursor = query(InstalledAppProvider.getAppUri(packageName), new String[]{Cols.SIGNATURE}, + null, null, null); + if (cursor != null) { + if (cursor.moveToFirst()) { + signatures.add(cursor.getString(cursor.getColumnIndex(Cols.SIGNATURE))); + } + cursor.close(); + } + } + systemSignatures = signatures.toArray(new String[signatures.size()]); + } + + Log.i(TAG, "excluding InstalledApps signed by system signatures"); + for (String systemSignature : systemSignatures) { + selection = selection.add("NOT " + Cols.SIGNATURE + " IN (?)", new String[]{systemSignature}); + } + return selection; + } + @Override protected String getTableName() { return InstalledAppTable.NAME; @@ -185,6 +230,7 @@ public class InstalledAppProvider extends FDroidProvider { QuerySelection selection = new QuerySelection(customSelection, selectionArgs); switch (MATCHER.match(uri)) { case CODE_LIST: + selection = selectNotSystemSignature(selection); break; case CODE_SINGLE: 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 1b531d205..2df49e74d 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -23,6 +23,8 @@ import rx.subjects.PublishSubject; import java.io.File; import java.io.FilenameFilter; import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -146,7 +148,10 @@ public class InstalledAppProviderService extends IntentService { * Make sure that {@link InstalledAppProvider}, our database of installed apps, * is in sync with what the {@link PackageManager} tells us is installed. Once * completed, the relevant {@link android.content.ContentProvider}s will be - * notified of any changes to installed statuses. + * notified of any changes to installed statuses. The packages are processed + * in alphabetically order so that "{@code android}" is processed first. That + * is always present and signed by the system key, so it is the source of the + * system key for comparing all packages. *

* The installed app cache could get out of sync, e.g. if F-Droid crashed/ or * ran out of battery half way through responding to {@link Intent#ACTION_PACKAGE_ADDED}. @@ -169,6 +174,12 @@ public class InstalledAppProviderService extends IntentService { List packageInfoList = context.getPackageManager() .getInstalledPackages(PackageManager.GET_SIGNATURES); + Collections.sort(packageInfoList, new Comparator() { + @Override + public int compare(PackageInfo o1, PackageInfo o2) { + return o1.packageName.compareTo(o2.packageName); + } + }); for (PackageInfo packageInfo : packageInfoList) { if (cachedInfo.containsKey(packageInfo.packageName)) { if (packageInfo.lastUpdateTime < 1262300400000L // 2010-01-01 00:00 @@ -314,6 +325,11 @@ public class InstalledAppProviderService extends IntentService { context.getContentResolver().delete(uri, null, null); } + /** + * Get the fingerprint used to represent an APK signing key in F-Droid. + * This is a custom fingerprint algorithm that was kind of accidentally + * created, but is still in use. + */ private static String getPackageSig(PackageInfo info) { if (info == null || info.signatures == null || info.signatures.length < 1) { return ""; diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index b7e509179..3a16ea723 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -481,6 +481,8 @@ public class SwapService extends Service { Utils.debugLog(TAG, "Creating swap service."); startForeground(NOTIFICATION, createNotification()); + deleteAllSwapRepos(); + CacheSwapAppsService.startCaching(this); swapPreferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); @@ -537,11 +539,11 @@ public class SwapService extends Service { Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange); - if (!SwapService.wasBluetoothEnabledBeforeSwap()) { + if (bluetoothAdapter != null && !wasBluetoothEnabledBeforeSwap()) { bluetoothAdapter.disable(); } - if (!SwapService.wasWifiEnabledBeforeSwap()) { + if (wifiManager != null && !wasWifiEnabledBeforeSwap()) { wifiManager.setWifiEnabled(false); } @@ -553,6 +555,8 @@ public class SwapService extends Service { } stopForeground(true); + deleteAllSwapRepos(); + super.onDestroy(); } @@ -568,7 +572,26 @@ public class SwapService extends Service { .build(); } + /** + * For now, swap repos are only trusted as long as swapping is active. They + * should have a long lived trust based on the signing key, but that requires + * that the repos are stored in the database by fingerprint, not by URL address. + * + * @see TOFU in swap + * @see + * signing key fingerprint should be sole ID for repos in the database + */ + private void deleteAllSwapRepos() { + for (Repo repo : RepoProvider.Helper.all(this)) { + if (repo.isSwap) { + Utils.debugLog(TAG, "Removing stale swap repo: " + repo.address + " - " + repo.fingerprint); + RepoProvider.Helper.remove(this, repo.getId()); + } + } + } + private void initTimer() { + // TODO replace by Android scheduler if (timer != null) { Utils.debugLog(TAG, "Cancelling existing timeout timer so timeout can be reset."); timer.cancel(); diff --git a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java index fcd62ad6c..c4280c3cc 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java @@ -41,6 +41,10 @@ import java.util.Locale; * the current state because it means that something about the wifi has * changed. Having the {@code Thread} also makes it easy to kill work * that is in progress. + *

+ * Some devices send multiple copies of given events, like a Moto G often + * sends three {@code CONNECTED} events. So they have to be debounced to + * keep the {@link #BROADCAST} useful. */ @SuppressWarnings("LineLength") public class WifiStateChangeService extends IntentService { @@ -50,6 +54,7 @@ public class WifiStateChangeService extends IntentService { private WifiManager wifiManager; private static WifiInfoThread wifiInfoThread; + private static int previousWifiState = Integer.MIN_VALUE; public WifiStateChangeService() { super("WifiStateChangeService"); @@ -70,16 +75,17 @@ public class WifiStateChangeService extends IntentService { Utils.debugLog(TAG, "received null Intent, ignoring"); return; } - Utils.debugLog(TAG, "WiFi change service started, clearing info about wifi state until we have figured it out again."); + Utils.debugLog(TAG, "WiFi change service started."); NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE); int wifiState = wifiManager.getWifiState(); if (ni == null || ni.isConnected()) { Utils.debugLog(TAG, "ni == " + ni + " wifiState == " + printWifiState(wifiState)); - if (wifiState == WifiManager.WIFI_STATE_ENABLED - || wifiState == WifiManager.WIFI_STATE_DISABLING // might be switching to hotspot - || wifiState == WifiManager.WIFI_STATE_DISABLED // might be hotspot - || wifiState == WifiManager.WIFI_STATE_UNKNOWN) { // might be hotspot + if (previousWifiState != wifiState && + (wifiState == WifiManager.WIFI_STATE_ENABLED + || wifiState == WifiManager.WIFI_STATE_DISABLING // might be switching to hotspot + || wifiState == WifiManager.WIFI_STATE_DISABLED // might be hotspot + || wifiState == WifiManager.WIFI_STATE_UNKNOWN)) { // might be hotspot if (wifiInfoThread != null) { wifiInfoThread.interrupt(); } @@ -121,6 +127,10 @@ public class WifiStateChangeService extends IntentService { } } } + if (FDroidApp.ipAddressString == null + || FDroidApp.subnetInfo == FDroidApp.UNSET_SUBNET_INFO) { + setIpInfoFromNetworkInterface(); + } } else if (wifiState == WifiManager.WIFI_STATE_DISABLED || wifiState == WifiManager.WIFI_STATE_DISABLING) { // try once to see if its a hotspot @@ -210,6 +220,16 @@ public class WifiStateChangeService extends IntentService { } } + /** + * Search for known Wi-Fi, Hotspot, and local network interfaces and get + * the IP Address info from it. This is necessary because network + * interfaces in Hotspot/AP mode do not show up in the regular + * {@link WifiManager} queries, and also on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP Android 5.0} and newer, + * {@link WifiManager#getDhcpInfo()} returns an invalid netmask. + * + * @see netmask of WifiManager.getDhcpInfo() is always zero on Android 5.0 + */ private void setIpInfoFromNetworkInterface() { try { Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); @@ -273,7 +293,10 @@ public class WifiStateChangeService extends IntentService { return "WIFI_STATE_ENABLED"; case WifiManager.WIFI_STATE_UNKNOWN: return "WIFI_STATE_UNKNOWN"; + case Integer.MIN_VALUE: + return "previous value unset"; + default: + return "~not mapped~"; } - return null; } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 6098a58cd..5d93b8e72 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -425,7 +425,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { }) .create().show(); } else { - showSelectApps(); + showWifiQr(); } }