From 08816e4bc82799577fb732269aa93255f8a0873b Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Thu, 3 Mar 2011 23:02:39 +0100 Subject: [PATCH 01/10] Make use of sdkver, permissions and features --- src/org/fdroid/fdroid/AppDetails.java | 28 +++++++++++- src/org/fdroid/fdroid/DB.java | 53 +++++++++++++++++++---- src/org/fdroid/fdroid/RepoXMLHandler.java | 12 ++++- 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index e09f06cf8..c766e93d3 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -312,7 +312,33 @@ public class AppDetails extends ListActivity { installed = app.installedVersion; } } - p.setMessage(getString(R.string.isinst) + " " + installed); + + StringBuilder msg = new StringBuilder(); + msg.append(getString(R.string.isinst)); + msg.append(" "); + msg.append(installed); + if (caninstall && curapk.minSdkVersion > 0) { + msg.append("\nMinimum API level: "); + msg.append(curapk.minSdkVersion); + } + if (caninstall && curapk.permissions != null) { + msg.append("\nPermissions required:"); + for (String perm : curapk.permissions) { + msg.append("\n "); + msg.append(perm); + } + } + if (caninstall && curapk.features != null) { + msg.append("\nFeatures required:"); + for (String feat : curapk.features) { + msg.append("\n "); + if (feat.matches("android\\.(hard|soft)ware\\..*")) + msg.append(feat.substring(17)); + else + msg.append(feat); + } + } + p.setMessage(msg.toString()); if (caninstall) { p.setButton(getString(R.string.install), diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index ae8fe8c21..6efcee5a0 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -90,9 +90,9 @@ public class DB { public String marketVersion; public int marketVercode; - // Comma-separated list of anti-features (as defined in the metadata + // Array of anti-features (as defined in the metadata // documentation) or null if there aren't any. - public String antiFeatures; + public String[] antiFeatures; // True if there are new versions (apks) that the user hasn't // explicitly ignored. (We're currently not using the database @@ -158,6 +158,9 @@ public class DB { public int size; // Size in bytes - 0 means we don't know! public String server; public String hash; + public int minSdkVersion; // 0 if unknown + public String[] permissions; // null if empty or unknown + public String[] features; // null if empty or unknown // ID (md5 sum of public key) of signature. Might be null, in the // transition to this field existing. @@ -236,7 +239,12 @@ public class DB { { "alter table " + TABLE_APP + " add donateURL string" }, // Version 9... - { "alter table " + TABLE_APK + " add srcname string" } }; + { "alter table " + TABLE_APK + " add srcname string" }, + + // Version 10... + { "alter table " + TABLE_APK + " add minSdkVersion integer", + "alter table " + TABLE_APK + " add permissions string", + "alter table " + TABLE_APK + " add features string" }}; private class DBHelper extends SQLiteOpenHelper { @@ -360,12 +368,11 @@ public class DB { while (!c.isAfterLast()) { App app = new App(); - app.antiFeatures = c - .getString(c.getColumnIndex("antiFeatures")); + app.antiFeatures = decodeList(c + .getString(c.getColumnIndex("antiFeatures"))); boolean include = true; - if (app.antiFeatures != null && app.antiFeatures.length() > 0) { - String[] afs = app.antiFeatures.split(","); - for (String af : afs) { + if (app.antiFeatures != null) { + for (String af : app.antiFeatures) { if (af.equals("Ads") && !pref_antiAds) include = false; else if (af.equals("Tracking") && !pref_antiTracking) @@ -421,6 +428,12 @@ public class DB { .getString(c2.getColumnIndex("apkName")); apk.apkSource = c2.getString(c2 .getColumnIndex("apkSource")); + apk.minSdkVersion = c2.getInt(c2 + .getColumnIndex("minSdkVersion")); + apk.permissions = decodeList(c2.getString(c2 + .getColumnIndex("permissions"))); + apk.features = decodeList(c2.getString(c2 + .getColumnIndex("features"))); app.apks.add(apk); c2.moveToNext(); } @@ -497,6 +510,25 @@ public class DB { } } + // Join the elements of a String array with commas. An empty array + // or a null value both result in null as the return value. + public static String encodeList(String[] array) { + if (array == null || array.length == 0) + return null; + StringBuilder sb = new StringBuilder(); + for (String e : array) { + sb.append(e); + sb.append(","); + } + return sb.substring(0, sb.length() - 1); + } + + public static String[] decodeList(String string) { + if (string == null || string.length() == 0) + return null; + return string.split(","); + } + private Vector updateApps = null; // Called before a repo update starts. @@ -637,7 +669,7 @@ public class DB { values.put("installedVerCode", upapp.installedVerCode); values.put("marketVersion", upapp.marketVersion); values.put("marketVercode", upapp.marketVercode); - values.put("antiFeatures", upapp.antiFeatures); + values.put("antiFeatures", encodeList(upapp.antiFeatures)); values.put("hasUpdates", upapp.hasUpdates ? 1 : 0); if (oldapp != null) { db.update(TABLE_APP, values, "id = ?", new String[] { oldapp.id }); @@ -664,6 +696,9 @@ public class DB { values.put("size", upapk.size); values.put("apkName", upapk.apkName); values.put("apkSource", upapk.apkSource); + values.put("minSdkVersion", upapk.minSdkVersion); + values.put("permissions", encodeList(upapk.permissions)); + values.put("features", encodeList(upapk.features)); if (oldapk != null) { db.update(TABLE_APK, values, "id = ? and version =?", new String[] { oldapk.id, oldapk.version }); diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index 3443b812e..033fb2992 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -119,6 +119,16 @@ public class RepoXMLHandler extends DefaultHandler { curapk.apkName = str; } else if (curel.equals("apksource")) { curapk.apkSource = str; + } else if (curel.equals("sdkver")) { + try { + curapk.minSdkVersion = Integer.parseInt(str); + } catch (NumberFormatException ex) { + curapk.minSdkVersion = 0; + } + } else if (curel.equals("permissions")) { + curapk.permissions = DB.decodeList(str); + } else if (curel.equals("features")) { + curapk.features = DB.decodeList(str); } } else if (curapp != null && str != null) { if (curel.equals("id")) { @@ -151,7 +161,7 @@ public class RepoXMLHandler extends DefaultHandler { curapp.marketVercode = 0; } } else if (curel.equals("antifeatures")) { - curapp.antiFeatures = str; + curapp.antiFeatures = DB.decodeList(str); } } From 111a57fc47461fe71750cf0cb0e3af67d079ec58 Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Fri, 4 Mar 2011 18:08:52 +0100 Subject: [PATCH 02/10] Disable incompatible APKs --- src/org/fdroid/fdroid/AppDetails.java | 8 ++++++ src/org/fdroid/fdroid/DB.java | 40 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index c766e93d3..4496489aa 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -70,8 +70,10 @@ public class AppDetails extends ListActivity { private class ApkListAdapter extends BaseAdapter { private List items = new ArrayList(); + private DB.Apk.CompatibilityChecker compatChecker; public ApkListAdapter(Context context) { + compatChecker = DB.Apk.CompatibilityChecker.getChecker(context); } public void addItem(DB.Apk apk) { @@ -122,6 +124,12 @@ public class AppDetails extends ListActivity { } else { buildtype.setText("bin"); } + if (!compatChecker.isCompatible(apk)) { + View[] views = { v, version, status, size, buildtype }; + for (View view : views) { + view.setEnabled(false); + } + } return v; } } diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 6efcee5a0..48e41873a 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -32,6 +32,7 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; @@ -184,6 +185,45 @@ public class DB { String path = apkName.replace(" ", "%20"); return server + "/" + path; } + + // Call isCompatible(apk) on an instance of this class to + // check if an APK is compatible with the user's device. + public static abstract class CompatibilityChecker { + // Because Build.VERSION.SDK_INT requires API level 5 + protected final static int SDK_INT + = Integer.parseInt(Build.VERSION.SDK); + public static CompatibilityChecker getChecker(Context ctx) { + CompatibilityChecker checker; + if (SDK_INT >= 5) + checker = new EclairChecker(ctx); + else + checker = new BasicChecker(); + Log.d("FDroid", "Compatibility checker for API level " + + SDK_INT + ": " + checker.getClass().getName()); + return checker; + } + public abstract boolean isCompatible(Apk apk); + } + private static class BasicChecker extends CompatibilityChecker { + public boolean isCompatible(Apk apk) { + return (apk.minSdkVersion <= SDK_INT); + } + } + private static class EclairChecker extends CompatibilityChecker { + private PackageManager pm; + public EclairChecker(Context ctx) { + pm = ctx.getPackageManager(); + } + public boolean isCompatible(Apk apk) { + if (apk.minSdkVersion > SDK_INT) + return false; + for (String feat : apk.features) { + if (!pm.hasSystemFeature(feat)) + return false; + } + return true; + } + } } // The TABLE_REPO table stores the details of the repositories in use. From 0513bb2de778ae87a1b95f4dd2857d175a953d24 Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Fri, 4 Mar 2011 23:46:24 +0100 Subject: [PATCH 03/10] Filter incompatible applications --- res/values/strings.xml | 4 ++++ res/xml/preferences.xml | 5 +++++ src/org/fdroid/fdroid/AppDetails.java | 11 ++++++---- src/org/fdroid/fdroid/DB.java | 29 +++++++++++++++++++++------ src/org/fdroid/fdroid/FDroid.java | 5 ++++- 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index b4069ef33..ac9310003 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -128,4 +128,8 @@ Database sync mode Set the value of SQLite\'s "synchronous" flag + + Application compatibility + Incompatible apps + Show apps written for newer Android versions or different hardware diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 4a5c09be7..0a86e4bc3 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -33,6 +33,11 @@ android:defaultValue="false" android:summary="@string/antinonfreenetlong" android:key="antiNonFreeNet" /> + + + diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 4496489aa..ecb6b6dee 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -70,10 +70,8 @@ public class AppDetails extends ListActivity { private class ApkListAdapter extends BaseAdapter { private List items = new ArrayList(); - private DB.Apk.CompatibilityChecker compatChecker; public ApkListAdapter(Context context) { - compatChecker = DB.Apk.CompatibilityChecker.getChecker(context); } public void addItem(DB.Apk apk) { @@ -160,6 +158,7 @@ public class AppDetails extends ListActivity { private String appid; private PackageManager mPm; private ProgressDialog pd; + private DB.Apk.CompatibilityChecker compatChecker; private Context mctx = this; @@ -200,6 +199,7 @@ public class AppDetails extends ListActivity { pref_cacheDownloaded = prefs.getBoolean("cacheDownloaded", true); pref_expert = prefs.getBoolean("expert", false); viewResetRequired = true; + compatChecker = DB.Apk.CompatibilityChecker.getChecker(this); } @@ -295,11 +295,14 @@ public class AppDetails extends ListActivity { @Override protected void onListItemClick(ListView l, View v, int position, long id) { + curapk = app.apks.get(position); + + // Ignore attempt to install incompatible APK + if (!compatChecker.isCompatible(curapk)) return; + // Create alert dialog... final AlertDialog p = new AlertDialog.Builder(this).create(); - curapk = app.apks.get(position); - // Set the title and icon... String icon_path = DB.getIconsPath() + app.icon; File test_icon = new File(icon_path); diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 48e41873a..1d0d2d83f 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -217,9 +217,11 @@ public class DB { public boolean isCompatible(Apk apk) { if (apk.minSdkVersion > SDK_INT) return false; - for (String feat : apk.features) { - if (!pm.hasSystemFeature(feat)) - return false; + if (apk.features != null) { + for (String feat : apk.features) { + if (!pm.hasSystemFeature(feat)) + return false; + } } return true; } @@ -323,6 +325,7 @@ public class DB { private PackageManager mPm; private Context mContext; + private Apk.CompatibilityChecker compatChecker; public DB(Context ctx) { @@ -343,6 +346,7 @@ public class DB { sync_mode = null; if (sync_mode != null) Log.d("FDroid", "Database synchronization mode: " + sync_mode); + compatChecker = Apk.CompatibilityChecker.getChecker(ctx); } public void close() { @@ -374,8 +378,9 @@ public class DB { return count; } - // Return a list of apps matching the given criteria. Filtering is also - // done based on the user's current anti-features preferences. + // Return a list of apps matching the given criteria. Filtering is + // also done based on compatibility and anti-features according to + // the user's current preferences. // 'appid' - specific app id to retrieve, or null // 'filter' - search text to filter on, or null // 'update' - update installed version information from device, rather than @@ -388,6 +393,7 @@ public class DB { boolean pref_antiTracking = prefs.getBoolean("antiTracking", false); boolean pref_antiNonFreeAdd = prefs.getBoolean("antiNonFreeAdd", false); boolean pref_antiNonFreeNet = prefs.getBoolean("antiNonFreeNet", false); + boolean pref_showIncompat = prefs.getBoolean("showIncompatible", false); Vector result = new Vector(); Cursor c = null; @@ -453,6 +459,7 @@ public class DB { + " where id = ? order by vercode desc", new String[] { app.id }); c2.moveToFirst(); + boolean compatible = pref_showIncompat; while (!c2.isAfterLast()) { Apk apk = new Apk(); apk.id = app.id; @@ -475,11 +482,21 @@ public class DB { apk.features = decodeList(c2.getString(c2 .getColumnIndex("features"))); app.apks.add(apk); + if (!compatible && compatChecker.isCompatible(apk)) { + // At least one compatible APK. + compatible = true; + } c2.moveToNext(); } c2.close(); - result.add(app); + if (compatible) { + result.add(app); + } + else { + Log.d("FDroid", "Excluding incompatible application: " + + app.id); + } } c.moveToNext(); diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index 3385ce371..1f04f183e 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -309,6 +309,7 @@ public class FDroid extends TabActivity implements OnItemClickListener { apps_av.clear(); apps_up.clear(); + long startTime = System.currentTimeMillis(); Vector apps = db.getApps(null, null, update); if (apps.isEmpty()) { // Don't attempt this more than once - we may have invalid @@ -322,7 +323,9 @@ public class FDroid extends TabActivity implements OnItemClickListener { triedEmptyUpdate = true; return; } - Log.d("FDroid", "Updating lists - " + apps.size() + " apps in total"); + Log.d("FDroid", "Updating lists - " + apps.size() + " apps in total" + + " (update took " + (System.currentTimeMillis() - startTime) + + " ms)"); for (DB.App app : apps) { if (app.installedVersion == null) { From 1d025c1e7da044486a67c90be9e3cd24e7041159 Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Sun, 6 Mar 2011 00:12:51 +0100 Subject: [PATCH 04/10] Keep lists as unsplit strings There is no point in splitting the strings ahead of time since 1) not all strings need to be split and 2) we currently deserialize them just as often as we iterate over them. --- src/org/fdroid/fdroid/DB.java | 66 +++++++++++++---------- src/org/fdroid/fdroid/RepoXMLHandler.java | 6 +-- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 1d0d2d83f..8751dbff9 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -20,6 +20,7 @@ package org.fdroid.fdroid; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Vector; @@ -35,6 +36,7 @@ import android.database.sqlite.SQLiteOpenHelper; import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; +import android.text.TextUtils.SimpleStringSplitter; public class DB { @@ -93,7 +95,7 @@ public class DB { // Array of anti-features (as defined in the metadata // documentation) or null if there aren't any. - public String[] antiFeatures; + public CommaSeparatedList antiFeatures; // True if there are new versions (apks) that the user hasn't // explicitly ignored. (We're currently not using the database @@ -160,8 +162,8 @@ public class DB { public String server; public String hash; public int minSdkVersion; // 0 if unknown - public String[] permissions; // null if empty or unknown - public String[] features; // null if empty or unknown + public CommaSeparatedList permissions; // null if empty or unknown + public CommaSeparatedList features; // null if empty or unknown // ID (md5 sum of public key) of signature. Might be null, in the // transition to this field existing. @@ -414,7 +416,7 @@ public class DB { while (!c.isAfterLast()) { App app = new App(); - app.antiFeatures = decodeList(c + app.antiFeatures = DB.CommaSeparatedList.make(c .getString(c.getColumnIndex("antiFeatures"))); boolean include = true; if (app.antiFeatures != null) { @@ -477,10 +479,10 @@ public class DB { .getColumnIndex("apkSource")); apk.minSdkVersion = c2.getInt(c2 .getColumnIndex("minSdkVersion")); - apk.permissions = decodeList(c2.getString(c2 - .getColumnIndex("permissions"))); - apk.features = decodeList(c2.getString(c2 - .getColumnIndex("features"))); + apk.permissions = CommaSeparatedList.make(c2 + .getString(c2.getColumnIndex("permissions"))); + apk.features = CommaSeparatedList.make(c2 + .getString(c2.getColumnIndex("features"))); app.apks.add(apk); if (!compatible && compatChecker.isCompatible(apk)) { // At least one compatible APK. @@ -567,23 +569,33 @@ public class DB { } } - // Join the elements of a String array with commas. An empty array - // or a null value both result in null as the return value. - public static String encodeList(String[] array) { - if (array == null || array.length == 0) - return null; - StringBuilder sb = new StringBuilder(); - for (String e : array) { - sb.append(e); - sb.append(","); - } - return sb.substring(0, sb.length() - 1); - } + public static class CommaSeparatedList implements Iterable { + private String value; - public static String[] decodeList(String string) { - if (string == null || string.length() == 0) - return null; - return string.split(","); + private CommaSeparatedList(String list) { + value = list; + } + + public static CommaSeparatedList make(String list) { + if (list == null || list.length() == 0) + return null; + else + return new CommaSeparatedList(list); + } + + public static String str(CommaSeparatedList instance) { + return (instance == null ? null : instance.toString()); + } + + public String toString() { + return value; + } + + public Iterator iterator() { + SimpleStringSplitter splitter = new SimpleStringSplitter(','); + splitter.setString(value); + return splitter.iterator(); + } } private Vector updateApps = null; @@ -726,7 +738,7 @@ public class DB { values.put("installedVerCode", upapp.installedVerCode); values.put("marketVersion", upapp.marketVersion); values.put("marketVercode", upapp.marketVercode); - values.put("antiFeatures", encodeList(upapp.antiFeatures)); + values.put("antiFeatures", CommaSeparatedList.str(upapp.antiFeatures)); values.put("hasUpdates", upapp.hasUpdates ? 1 : 0); if (oldapp != null) { db.update(TABLE_APP, values, "id = ?", new String[] { oldapp.id }); @@ -754,8 +766,8 @@ public class DB { values.put("apkName", upapk.apkName); values.put("apkSource", upapk.apkSource); values.put("minSdkVersion", upapk.minSdkVersion); - values.put("permissions", encodeList(upapk.permissions)); - values.put("features", encodeList(upapk.features)); + values.put("permissions", CommaSeparatedList.str(upapk.permissions)); + values.put("features", CommaSeparatedList.str(upapk.features)); if (oldapk != null) { db.update(TABLE_APK, values, "id = ? and version =?", new String[] { oldapk.id, oldapk.version }); diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index 033fb2992..fe51df8df 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -126,9 +126,9 @@ public class RepoXMLHandler extends DefaultHandler { curapk.minSdkVersion = 0; } } else if (curel.equals("permissions")) { - curapk.permissions = DB.decodeList(str); + curapk.permissions = DB.CommaSeparatedList.make(str); } else if (curel.equals("features")) { - curapk.features = DB.decodeList(str); + curapk.features = DB.CommaSeparatedList.make(str); } } else if (curapp != null && str != null) { if (curel.equals("id")) { @@ -161,7 +161,7 @@ public class RepoXMLHandler extends DefaultHandler { curapp.marketVercode = 0; } } else if (curel.equals("antifeatures")) { - curapp.antiFeatures = DB.decodeList(str); + curapp.antiFeatures = DB.CommaSeparatedList.make(str); } } From 06253a94cfb94f667130c3d1ab6a68792fd47850 Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Sun, 6 Mar 2011 20:50:40 +0100 Subject: [PATCH 05/10] Small optimization in EclairChecker --- src/org/fdroid/fdroid/DB.java | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 8751dbff9..36f174bb6 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -20,6 +20,7 @@ package org.fdroid.fdroid; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -28,6 +29,7 @@ import java.util.Vector; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.FeatureInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.Cursor; @@ -191,9 +193,13 @@ public class DB { // Call isCompatible(apk) on an instance of this class to // check if an APK is compatible with the user's device. public static abstract class CompatibilityChecker { + // Because Build.VERSION.SDK_INT requires API level 5 protected final static int SDK_INT = Integer.parseInt(Build.VERSION.SDK); + + public abstract boolean isCompatible(Apk apk); + public static CompatibilityChecker getChecker(Context ctx) { CompatibilityChecker checker; if (SDK_INT >= 5) @@ -204,25 +210,32 @@ public class DB { + SDK_INT + ": " + checker.getClass().getName()); return checker; } - public abstract boolean isCompatible(Apk apk); } + private static class BasicChecker extends CompatibilityChecker { public boolean isCompatible(Apk apk) { return (apk.minSdkVersion <= SDK_INT); } } + private static class EclairChecker extends CompatibilityChecker { - private PackageManager pm; + + private HashSet features; + public EclairChecker(Context ctx) { - pm = ctx.getPackageManager(); + PackageManager pm = ctx.getPackageManager(); + features = new HashSet(); + for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { + features.add(fi.name); + } } + public boolean isCompatible(Apk apk) { if (apk.minSdkVersion > SDK_INT) return false; if (apk.features != null) { for (String feat : apk.features) { - if (!pm.hasSystemFeature(feat)) - return false; + if (!features.contains(feat)) return false; } } return true; From 87ca5ce189a961aa900166563755095ad213da6c Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Tue, 8 Mar 2011 00:06:34 +0100 Subject: [PATCH 06/10] Oops, fix compiler warning --- src/org/fdroid/fdroid/DB.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 36f174bb6..27adb18c3 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -220,11 +220,11 @@ public class DB { private static class EclairChecker extends CompatibilityChecker { - private HashSet features; + private HashSet features; public EclairChecker(Context ctx) { PackageManager pm = ctx.getPackageManager(); - features = new HashSet(); + features = new HashSet(); for (FeatureInfo fi : pm.getSystemAvailableFeatures()) { features.add(fi.name); } From 62ad5e2ed1c36ca929f6f80ddf0b8a5aef90aea5 Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Tue, 8 Mar 2011 00:33:17 +0100 Subject: [PATCH 07/10] Add cancel button to download dialog --- res/values/strings.xml | 1 + src/org/fdroid/fdroid/AppDetails.java | 34 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/res/values/strings.xml b/res/values/strings.xml index ac9310003..36e4720d0 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -107,6 +107,7 @@ Not installed (%d available) Installed Downloaded file is corrupt + Download cancelled diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index ecb6b6dee..5e8e1e19c 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -159,6 +159,7 @@ public class AppDetails extends ListActivity { private PackageManager mPm; private ProgressDialog pd; private DB.Apk.CompatibilityChecker compatChecker; + private volatile boolean cancelDownload; private Context mctx = this; @@ -482,9 +483,24 @@ public class AppDetails extends ListActivity { return; } + cancelDownload = false; + pd = new ProgressDialog(this); pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); pd.setMessage(getString(R.string.download_server)); + pd.setCancelable(true); + pd.setOnCancelListener( + new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + cancelDownload = true; + } + }); + pd.setButton(getString(R.string.cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); pd.show(); new Thread() { @@ -552,6 +568,10 @@ public class AppDetails extends ListActivity { int totalRead = 0; int bytesRead = getit.read(data, 0, 1024); while (bytesRead != -1) { + if (cancelDownload) { + Log.d("FDroid", "Download cancelled!"); + break; + } bout.write(data, 0, bytesRead); totalRead += bytesRead; msg = new Message(); @@ -563,6 +583,12 @@ public class AppDetails extends ListActivity { getit.close(); saveit.close(); f = new File(localfile); + if (cancelDownload) { + f.delete(); + msg = download_cancelled_handler.obtainMessage(); + msg.sendToTarget(); + return; + } Md5Handler hash = new Md5Handler(); String calcedhash = hash.md5Calc(f); if (curapk.hash.equalsIgnoreCase(calcedhash)) { @@ -636,6 +662,14 @@ public class AppDetails extends ListActivity { } }; + private Handler download_cancelled_handler = new Handler() { + @Override + public void handleMessage(Message msg) { + Toast.makeText(mctx, getString(R.string.download_cancelled), + Toast.LENGTH_SHORT).show(); + } + }; + private void removeApk(String id) { PackageInfo pkginfo; try { From 7ae24e2917f83b3539df7a6ac2ebf035f71dd173 Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Tue, 8 Mar 2011 00:47:57 +0100 Subject: [PATCH 08/10] Remove useless confirmation dialog The dialog shown when clicking an APK provides the same information and options as the system dialog that follows it. --- src/org/fdroid/fdroid/AppDetails.java | 88 +++------------------------ 1 file changed, 7 insertions(+), 81 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 5e8e1e19c..3fdbe07f9 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -297,87 +297,12 @@ public class AppDetails extends ListActivity { @Override protected void onListItemClick(ListView l, View v, int position, long id) { curapk = app.apks.get(position); - - // Ignore attempt to install incompatible APK - if (!compatChecker.isCompatible(curapk)) return; - - // Create alert dialog... - final AlertDialog p = new AlertDialog.Builder(this).create(); - - // Set the title and icon... - String icon_path = DB.getIconsPath() + app.icon; - File test_icon = new File(icon_path); - if (test_icon.exists()) { - p.setIcon(new BitmapDrawable(icon_path)); - } else { - p.setIcon(android.R.drawable.sym_def_app_icon); + if (app.installedVersion != null + && app.installedVersion.equals(curapk.version)) { + removeApk(app.id); + } else if (compatChecker.isCompatible(curapk)) { + install(); } - p.setTitle(app.name + " " + curapk.version); - - boolean caninstall = true; - String installed = getString(R.string.no); - if (app.installedVersion != null) { - if (app.installedVersion.equals(curapk.version)) { - installed = getString(R.string.yes); - caninstall = false; - } else { - installed = app.installedVersion; - } - } - - StringBuilder msg = new StringBuilder(); - msg.append(getString(R.string.isinst)); - msg.append(" "); - msg.append(installed); - if (caninstall && curapk.minSdkVersion > 0) { - msg.append("\nMinimum API level: "); - msg.append(curapk.minSdkVersion); - } - if (caninstall && curapk.permissions != null) { - msg.append("\nPermissions required:"); - for (String perm : curapk.permissions) { - msg.append("\n "); - msg.append(perm); - } - } - if (caninstall && curapk.features != null) { - msg.append("\nFeatures required:"); - for (String feat : curapk.features) { - msg.append("\n "); - if (feat.matches("android\\.(hard|soft)ware\\..*")) - msg.append(feat.substring(17)); - else - msg.append(feat); - } - } - p.setMessage(msg.toString()); - - if (caninstall) { - p.setButton(getString(R.string.install), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - p.dismiss(); - install(); - } - }); - } else { - p.setButton(getString(R.string.uninstall), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - p.dismiss(); - removeApk(app.id); - } - }); - } - - p.setButton2(getString(R.string.cancel), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - return; - } - }); - - p.show(); } @Override @@ -473,7 +398,8 @@ public class AppDetails extends ListActivity { && !curapk.sig.equals(mInstalledSigID)) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.SignatureMismatch).setPositiveButton( - "Ok", new DialogInterface.OnClickListener() { + getString(R.string.ok), + new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } From cea9278ef2645bbd495cc92b45193473ab84d71e Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Tue, 8 Mar 2011 17:14:50 +0100 Subject: [PATCH 09/10] Optionally filter apps that require root --- res/values/strings.xml | 2 ++ res/xml/preferences.xml | 3 +++ src/org/fdroid/fdroid/DB.java | 23 +++++++++++++++++++++-- src/org/fdroid/fdroid/RepoXMLHandler.java | 2 ++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 36e4720d0..95184871c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -133,4 +133,6 @@ Application compatibility Incompatible apps Show apps written for newer Android versions or different hardware + Root + Show apps that require root privileges diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 0a86e4bc3..eb1a87d96 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -37,6 +37,9 @@ + (); @@ -95,10 +96,14 @@ public class DB { public String marketVersion; public int marketVercode; - // Array of anti-features (as defined in the metadata + // List of anti-features (as defined in the metadata // documentation) or null if there aren't any. public CommaSeparatedList antiFeatures; + // List of special requirements (such as root privileges) or + // null if there aren't any. + public CommaSeparatedList requirements; + // True if there are new versions (apks) that the user hasn't // explicitly ignored. (We're currently not using the database // field for this - we make the decision on the fly in getApps(). @@ -301,7 +306,10 @@ public class DB { // Version 10... { "alter table " + TABLE_APK + " add minSdkVersion integer", "alter table " + TABLE_APK + " add permissions string", - "alter table " + TABLE_APK + " add features string" }}; + "alter table " + TABLE_APK + " add features string" }, + + // Version 11... + { "alter table " + TABLE_APP + " add requirements string" }}; private class DBHelper extends SQLiteOpenHelper { @@ -409,6 +417,7 @@ public class DB { boolean pref_antiNonFreeAdd = prefs.getBoolean("antiNonFreeAdd", false); boolean pref_antiNonFreeNet = prefs.getBoolean("antiNonFreeNet", false); boolean pref_showIncompat = prefs.getBoolean("showIncompatible", false); + boolean pref_rooted = prefs.getBoolean("rooted", true); Vector result = new Vector(); Cursor c = null; @@ -446,6 +455,15 @@ public class DB { include = false; } } + app.requirements = DB.CommaSeparatedList.make(c + .getString(c.getColumnIndex("requirements"))); + if (app.requirements != null) { + for (String r : app.requirements) { + if (r.equals("root") && !pref_rooted) { + include = false; + } + } + } if (include) { app.id = c.getString(c.getColumnIndex("id")); @@ -752,6 +770,7 @@ public class DB { values.put("marketVersion", upapp.marketVersion); values.put("marketVercode", upapp.marketVercode); values.put("antiFeatures", CommaSeparatedList.str(upapp.antiFeatures)); + values.put("requirements", CommaSeparatedList.str(upapp.requirements)); values.put("hasUpdates", upapp.hasUpdates ? 1 : 0); if (oldapp != null) { db.update(TABLE_APP, values, "id = ?", new String[] { oldapp.id }); diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index fe51df8df..a081ca343 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -162,6 +162,8 @@ public class RepoXMLHandler extends DefaultHandler { } } else if (curel.equals("antifeatures")) { curapp.antiFeatures = DB.CommaSeparatedList.make(str); + } else if (curel.equals("requirements")) { + curapp.requirements = DB.CommaSeparatedList.make(str); } } From 3502896e653fb6c5cdbbf608a644c809b8f2f071 Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Tue, 8 Mar 2011 19:54:57 +0100 Subject: [PATCH 10/10] Check compatibility when determining current version --- src/org/fdroid/fdroid/AppDetails.java | 6 +++--- src/org/fdroid/fdroid/DB.java | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 3fdbe07f9..fe09a6500 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -227,7 +227,7 @@ public class AppDetails extends ListActivity { Log.d("FDroid", "Getting application details for " + appid); app = db.getApps(appid, null, true).get(0); - DB.Apk curver = app.getCurrentVersion(); + DB.Apk curver = app.getCurrentVersion(compatChecker); app_currentvercode = curver == null ? 0 : curver.vercode; // Get the signature of the installed package... @@ -310,7 +310,7 @@ public class AppDetails extends ListActivity { super.onCreateOptionsMenu(menu); menu.clear(); - DB.Apk curver = app.getCurrentVersion(); + DB.Apk curver = app.getCurrentVersion(compatChecker); if (app.installedVersion != null && curver != null && !app.installedVersion.equals(curver.version)) { menu.add(Menu.NONE, INSTALL, 0, R.string.menu_update).setIcon( @@ -354,7 +354,7 @@ public class AppDetails extends ListActivity { case INSTALL: // Note that this handles updating as well as installing. - curapk = app.getCurrentVersion(); + curapk = app.getCurrentVersion(compatChecker); if (curapk != null) install(); return true; diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index b21485b2f..c8f48228b 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -119,22 +119,25 @@ public class DB { // This should be the 'current' version, as in the most recent stable // one, that most users would want by default. It might not be the // most recent, if for example there are betas etc. - public Apk getCurrentVersion() { + // To skip compatibility checks, pass null as the checker. + public Apk getCurrentVersion(DB.Apk.CompatibilityChecker checker) { // Try and return the version that's in Google's market first... if (marketVersion != null && marketVercode > 0) { for (Apk apk : apks) { - if (apk.vercode == marketVercode) + if (apk.vercode == marketVercode + && (checker == null || checker.isCompatible(apk))) return apk; } } // If we don't know the market version, or we don't have it, we - // return the most recent version we have... + // return the most recent compatible version we have... int latestcode = -1; Apk latestapk = null; for (Apk apk : apks) { - if (apk.vercode > latestcode) { + if (apk.vercode > latestcode + && (checker == null || checker.isCompatible(apk))) { latestapk = apk; latestcode = apk.vercode; } @@ -561,7 +564,7 @@ public class DB { // installed version is not the 'current' one AND the installed // version is older than the current one. for (App app : result) { - Apk curver = app.getCurrentVersion(); + Apk curver = app.getCurrentVersion(compatChecker); if (curver != null && app.installedVersion != null && !app.installedVersion.equals(curver.version)) { if (app.installedVerCode < curver.vercode)