Merge branch 'compatcheck'

This commit is contained in:
Henrik Tunedal 2011-03-09 00:22:31 +01:00
commit ce3e4bd34f
6 changed files with 240 additions and 74 deletions

View File

@ -107,6 +107,7 @@
<string name="details_notinstalled">Not installed (%d available)</string>
<string name="inst">Installed</string>
<string name="corrupt_download">Downloaded file is corrupt</string>
<string name="download_cancelled">Download cancelled</string>
@ -128,4 +129,10 @@
<string name="db_sync_mode">Database sync mode</string>
<string name="db_sync_mode_long">Set the value of SQLite\'s "synchronous" flag</string>
<string name="appcompatibility">Application compatibility</string>
<string name="showincompat">Incompatible apps</string>
<string name="showincompat_long">Show apps written for newer Android versions or different hardware</string>
<string name="rooted">Root</string>
<string name="rooted_long">Show apps that require root privileges</string>
</resources>

View File

@ -33,6 +33,14 @@
android:defaultValue="false" android:summary="@string/antinonfreenetlong"
android:key="antiNonFreeNet" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/appcompatibility">
<CheckBoxPreference android:title="@string/showincompat"
android:defaultValue="false" android:summary="@string/showincompat_long"
android:key="showIncompatible" />
<CheckBoxPreference android:title="@string/rooted"
android:defaultValue="true" android:summary="@string/rooted_long"
android:key="rooted" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/maintenance">
<Preference android:title="@string/reset" android:summary="@string/clear_all_cached_data"
android:key="reset" />

View File

@ -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 {

View File

@ -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<Apk>();
@ -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<String> features;
public EclairChecker(Context ctx) {
PackageManager pm = ctx.getPackageManager();
features = new HashSet<String>();
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<App> result = new Vector<App>();
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<String> {
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<String> iterator() {
SimpleStringSplitter splitter = new SimpleStringSplitter(',');
splitter.setString(value);
return splitter.iterator();
}
}
private Vector<App> 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 });

View File

@ -309,6 +309,7 @@ public class FDroid extends TabActivity implements OnItemClickListener {
apps_av.clear();
apps_up.clear();
long startTime = System.currentTimeMillis();
Vector<DB.App> 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) {

View File

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