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);
}
}