diff --git a/res/values/strings.xml b/res/values/strings.xml index b4069ef33..95184871c 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 @@ -128,4 +129,10 @@ 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 + Root + Show apps that require root privileges diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 4a5c09be7..eb1a87d96 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -33,6 +33,14 @@ 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 3e9598f6d..ccc6aa4bf 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -122,6 +122,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; } } @@ -152,6 +158,8 @@ public class AppDetails extends ListActivity { private String appid; private PackageManager mPm; private ProgressDialog pd; + private DB.Apk.CompatibilityChecker compatChecker; + private volatile boolean cancelDownload; private Context mctx = this; @@ -192,6 +200,7 @@ public class AppDetails extends ListActivity { pref_cacheDownloaded = prefs.getBoolean("cacheDownloaded", false); pref_expert = prefs.getBoolean("expert", false); viewResetRequired = true; + compatChecker = DB.Apk.CompatibilityChecker.getChecker(this); } @@ -218,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... @@ -287,59 +296,13 @@ public class AppDetails extends ListActivity { @Override protected void onListItemClick(ListView l, View v, int position, long id) { - // 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); - 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; - } - } - p.setMessage(getString(R.string.isinst) + " " + installed); - - 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 @@ -347,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( @@ -391,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; @@ -435,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(); } @@ -445,9 +409,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() { @@ -515,6 +494,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(); @@ -526,6 +509,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)) { @@ -599,6 +588,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 { diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index ae8fe8c21..c8f48228b 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -20,6 +20,8 @@ package org.fdroid.fdroid; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Vector; @@ -27,13 +29,16 @@ 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; import android.database.sqlite.SQLiteDatabase; 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 { @@ -70,6 +75,7 @@ public class DB { donateURL = null; webURL = ""; antiFeatures = null; + requirements = null; hasUpdates = false; updated = false; apks = new Vector(); @@ -90,9 +96,13 @@ public class DB { public String marketVersion; public int marketVercode; - // Comma-separated list 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 String antiFeatures; + 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 @@ -109,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; } @@ -158,6 +171,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 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. @@ -181,6 +197,58 @@ 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 abstract boolean isCompatible(Apk apk); + + 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; + } + } + + private static class BasicChecker extends CompatibilityChecker { + public boolean isCompatible(Apk apk) { + return (apk.minSdkVersion <= SDK_INT); + } + } + + private static class EclairChecker extends CompatibilityChecker { + + private HashSet features; + + public EclairChecker(Context ctx) { + 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 (!features.contains(feat)) return false; + } + } + return true; + } + } } // The TABLE_REPO table stores the details of the repositories in use. @@ -236,7 +304,15 @@ 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" }, + + // Version 11... + { "alter table " + TABLE_APP + " add requirements string" }}; private class DBHelper extends SQLiteOpenHelper { @@ -275,6 +351,7 @@ public class DB { private PackageManager mPm; private Context mContext; + private Apk.CompatibilityChecker compatChecker; public DB(Context ctx) { @@ -295,6 +372,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() { @@ -326,8 +404,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 @@ -340,6 +419,8 @@ 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); + boolean pref_rooted = prefs.getBoolean("rooted", true); Vector result = new Vector(); Cursor c = null; @@ -360,12 +441,11 @@ public class DB { while (!c.isAfterLast()) { App app = new App(); - app.antiFeatures = c - .getString(c.getColumnIndex("antiFeatures")); + app.antiFeatures = DB.CommaSeparatedList.make(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) @@ -378,6 +458,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")); @@ -406,6 +495,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; @@ -421,12 +511,28 @@ public class DB { .getString(c2.getColumnIndex("apkName")); apk.apkSource = c2.getString(c2 .getColumnIndex("apkSource")); + apk.minSdkVersion = c2.getInt(c2 + .getColumnIndex("minSdkVersion")); + 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. + 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(); @@ -458,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) @@ -497,6 +603,35 @@ public class DB { } } + public static class CommaSeparatedList implements Iterable { + private String value; + + 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; // Called before a repo update starts. @@ -637,7 +772,8 @@ 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", 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 }); @@ -664,6 +800,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", 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/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) { diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index 3443b812e..a081ca343 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.CommaSeparatedList.make(str); + } else if (curel.equals("features")) { + curapk.features = DB.CommaSeparatedList.make(str); } } else if (curapp != null && str != null) { if (curel.equals("id")) { @@ -151,7 +161,9 @@ public class RepoXMLHandler extends DefaultHandler { curapp.marketVercode = 0; } } else if (curel.equals("antifeatures")) { - curapp.antiFeatures = str; + curapp.antiFeatures = DB.CommaSeparatedList.make(str); + } else if (curel.equals("requirements")) { + curapp.requirements = DB.CommaSeparatedList.make(str); } }