Refactoring Apk references into content provider.

Removed DB.Apk in favour of stand-alone Apk class.

Conflicts:
	src/org/fdroid/fdroid/DB.java
This commit is contained in:
Peter Serwylo 2014-01-31 03:15:48 +11:00
parent 2a8c570a00
commit b3773a1561
14 changed files with 729 additions and 319 deletions

View File

@ -36,10 +36,15 @@
android:supportsRtl="false" >
<provider
android:authorities="org.fdroid.fdroid.data"
android:authorities="org.fdroid.fdroid.data.RepoProvider"
android:name="org.fdroid.fdroid.data.RepoProvider"
android:exported="false"/>
<provider
android:authorities="org.fdroid.fdroid.data.ApkProvider"
android:name="org.fdroid.fdroid.data.ApkProvider"
android:exported="false"/>
<activity
android:name=".FDroid"
android:configChanges="keyboardHidden|orientation|screenSize" >

3
TODO Normal file
View File

@ -0,0 +1,3 @@
incompatible_reasons needs to be implemented correctly for the data.Apk class (rather than DB.Apk).
Currently, it is set during a CompatabilityChecker call to isCompatible(), which means we don't really
know whether the field has been set or not.

View File

@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.xml.sax.XMLReader;
@ -74,13 +75,9 @@ import org.fdroid.fdroid.compat.ActionBarCompat;
import org.fdroid.fdroid.compat.MenuManager;
import org.fdroid.fdroid.DB.CommaSeparatedList;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.utils.StorageUtils;
import android.os.Environment;
public class AppDetails extends ListActivity {
@ -99,13 +96,13 @@ public class AppDetails extends ListActivity {
private class ApkListAdapter extends BaseAdapter {
private List<DB.Apk> items;
private List<Apk> items;
private LayoutInflater mInflater;
public ApkListAdapter(Context context, List<DB.Apk> items) {
this.items = new ArrayList<DB.Apk>();
public ApkListAdapter(Context context, List<Apk> items) {
this.items = new ArrayList<Apk>();
if (items != null) {
for (DB.Apk apk : items) {
for (Apk apk : items) {
this.addItem(apk);
}
}
@ -113,13 +110,13 @@ public class AppDetails extends ListActivity {
Context.LAYOUT_INFLATER_SERVICE);
}
public void addItem(DB.Apk apk) {
public void addItem(Apk apk) {
if (apk.compatible || pref_incompatibleVersions) {
items.add(apk);
}
}
public List<DB.Apk> getItems() {
public List<Apk> getItems() {
return items;
}
@ -142,7 +139,7 @@ public class AppDetails extends ListActivity {
public View getView(int position, View convertView, ViewGroup parent) {
java.text.DateFormat df = DateFormat.getDateFormat(mctx);
DB.Apk apk = items.get(position);
Apk apk = items.get(position);
ViewHolder holder;
if (convertView == null) {
@ -983,7 +980,7 @@ public class AppDetails extends ListActivity {
private boolean updating;
private String id;
public DownloadHandler(DB.Apk apk, String repoaddress, File destdir) {
public DownloadHandler(Apk apk, String repoaddress, File destdir) {
id = apk.id;
download = new Downloader(apk, repoaddress, destdir);
download.start();

View File

@ -21,16 +21,17 @@ package org.fdroid.fdroid;
import java.io.File;
import java.security.MessageDigest;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@ -55,8 +56,7 @@ import android.util.Log;
import org.fdroid.fdroid.compat.Compatibility;
import org.fdroid.fdroid.compat.ContextCompat;
import org.fdroid.fdroid.compat.SupportedArchitectures;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.*;
public class DB {
@ -270,155 +270,6 @@ public class DB {
}
// The TABLE_APK table stores details of all the application versions we
// know about. Each relates directly back to an entry in TABLE_APP.
// This information is retrieved from the repositories.
public static final String TABLE_APK = "fdroid_apk";
public static class Apk {
public Apk() {
updated = false;
detail_size = 0;
added = null;
repo = 0;
detail_hash = null;
detail_hashType = null;
detail_permissions = null;
compatible = false;
}
public String id;
public String version;
public int vercode;
public int detail_size; // Size in bytes - 0 means we don't know!
public long repo; // ID of the repo it comes from
public String detail_hash;
public String detail_hashType;
public int minSdkVersion; // 0 if unknown
public Date added;
public CommaSeparatedList detail_permissions; // null if empty or
// unknown
public CommaSeparatedList features; // null if empty or unknown
public CommaSeparatedList nativecode; // null if empty or unknown
public CommaSeparatedList incompatible_reasons; // null if empty or
// unknown
// ID (md5 sum of public key) of signature. Might be null, in the
// transition to this field existing.
public String sig;
// True if compatible with the device.
public boolean compatible;
public String apkName;
// If not null, this is the name of the source tarball for the
// application. Null indicates that it's a developer's binary
// build - otherwise it's built from source.
public String srcname;
// Used internally for tracking during repo updates.
public boolean updated;
// Call isCompatible(apk) on an instance of this class to
// check if an APK is compatible with the user's device.
private static class CompatibilityChecker extends Compatibility {
private Set<String> features;
private Set<String> cpuAbis;
private String cpuAbisDesc;
private boolean ignoreTouchscreen;
public CompatibilityChecker(Context ctx) {
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(ctx);
ignoreTouchscreen = prefs
.getBoolean(Preferences.PREF_IGN_TOUCH, false);
PackageManager pm = ctx.getPackageManager();
StringBuilder logMsg = new StringBuilder();
logMsg.append("Available device features:");
features = new HashSet<String>();
if (pm != null) {
for (FeatureInfo fi : pm.getSystemAvailableFeatures()) {
features.add(fi.name);
logMsg.append('\n');
logMsg.append(fi.name);
}
}
cpuAbis = SupportedArchitectures.getAbis();
StringBuilder builder = new StringBuilder();
boolean first = true;
for (String abi : cpuAbis) {
if (first) first = false;
else builder.append(", ");
builder.append(abi);
}
cpuAbisDesc = builder.toString();
builder = null;
Log.d("FDroid", logMsg.toString());
}
private boolean compatibleApi(CommaSeparatedList nativecode) {
if (nativecode == null) return true;
for (String abi : nativecode) {
if (cpuAbis.contains(abi)) {
return true;
}
}
return false;
}
public boolean isCompatible(Apk apk) {
if (!hasApi(apk.minSdkVersion)) {
apk.incompatible_reasons = CommaSeparatedList.make(String.valueOf(apk.minSdkVersion));
return false;
}
if (apk.features != null) {
for (String feat : apk.features) {
if (ignoreTouchscreen
&& feat.equals("android.hardware.touchscreen")) {
// Don't check it!
} else if (!features.contains(feat)) {
apk.incompatible_reasons = CommaSeparatedList.make(feat);
Log.d("FDroid", apk.id + " vercode " + apk.vercode
+ " is incompatible based on lack of "
+ feat);
return false;
}
}
}
if (!compatibleApi(apk.nativecode)) {
apk.incompatible_reasons = apk.nativecode;
Log.d("FDroid", apk.id + " vercode " + apk.vercode
+ " only supports " + CommaSeparatedList.str(apk.nativecode)
+ " while your architectures are " + cpuAbisDesc);
return false;
}
return true;
}
}
}
public int countAppsForRepo(long id) {
String[] selection = { "COUNT(distinct id)" };
String[] selectionArgs = { Long.toString(id) };
Cursor result = db.query(
TABLE_APK, selection, "repo = ?", selectionArgs, "repo", null, null);
if (result.getCount() > 0) {
result.moveToFirst();
return result.getInt(0);
} else {
return 0;
}
}
public static String calcFingerprint(String keyHexString) {
if (TextUtils.isEmpty(keyHexString))
return null;
@ -569,33 +420,21 @@ public class DB {
}
}
private static final String[] POPULATE_APK_COLS = new String[] { "hash", "hashType", "size", "permissions" };
private static final String[] POPULATE_APK_COLS = new String[] {
ApkProvider.DataColumns.HASH,
ApkProvider.DataColumns.HASH_TYPE,
ApkProvider.DataColumns.SIZE,
ApkProvider.DataColumns.PERMISSIONS
};
private void populateApkDetails(Apk apk, long repo) {
if (repo == 0 || repo == apk.repo) {
Cursor cursor = null;
try {
cursor = db.query(
TABLE_APK,
POPULATE_APK_COLS,
"id = ? and vercode = ?",
new String[] { apk.id,
Integer.toString(apk.vercode) }, null,
null, null, null);
cursor.moveToFirst();
apk.detail_hash = cursor.getString(0);
apk.detail_hashType = cursor.getString(1);
apk.detail_size = cursor.getInt(2);
apk.detail_permissions = CommaSeparatedList.make(cursor
.getString(3));
} catch (Exception e) {
Log.d("FDroid", "Error populating apk details for " + apk.id + " (version " + apk.version + ")");
Log.d("FDroid", e.getMessage());
} finally {
if (cursor != null) {
cursor.close();
}
}
Apk loadedApk = ApkProvider.Helper.find(
mContext, apk.id, apk.vercode, POPULATE_APK_COLS);
apk.detail_hash = loadedApk.detail_hash;
apk.detail_hashType = loadedApk.detail_hashType;
apk.detail_size = loadedApk.detail_size;
apk.detail_permissions = loadedApk.detail_permissions;
} else {
Log.d("FDroid", "Not setting details for apk '" + apk.id + "' (version " + apk.version +") because it belongs to a different repo.");
}
@ -695,18 +534,6 @@ public class DB {
Log.d("FDroid", "Read app data from database " + " (took "
+ (System.currentTimeMillis() - startTime) + " ms)");
String query = "SELECT apk.id, apk.version, apk.vercode, apk.sig,"
+ " apk.srcname, apk.apkName, apk.minSdkVersion, "
+ " apk.added, apk.features, apk.nativecode, "
+ " apk.compatible, apk.repo, repo.version, repo.address "
+ " FROM " + TABLE_APK + " as apk "
+ " LEFT JOIN " + DBHelper.TABLE_REPO + " as repo "
+ " ON repo._id = apk.repo "
+ " ORDER BY apk.vercode DESC";
c = db.rawQuery(query, null);
c.moveToFirst();
DisplayMetrics metrics = mContext.getResources()
.getDisplayMetrics();
String iconsDir = null;
@ -726,43 +553,21 @@ public class DB {
metrics = null;
Log.d("FDroid", "Density-specific icons dir is " + iconsDir);
while (!c.isAfterLast()) {
String id = c.getString(0);
App app = apps.get(id);
List<Apk> apks = ApkProvider.Helper.all(mContext);
for (Apk apk : apks) {
App app = apps.get(apk.id);
if (app == null) {
c.moveToNext();
continue;
}
boolean compatible = c.getInt(10) == 1;
int repoid = c.getInt(11);
Apk apk = new Apk();
apk.id = id;
apk.version = c.getString(1);
apk.vercode = c.getInt(2);
apk.sig = c.getString(3);
apk.srcname = c.getString(4);
apk.apkName = c.getString(5);
apk.minSdkVersion = c.getInt(6);
String sApkAdded = c.getString(7);
apk.added = (sApkAdded == null || sApkAdded.length() == 0) ? null
: DATE_FORMAT.parse(sApkAdded);
apk.features = CommaSeparatedList.make(c.getString(8));
apk.nativecode = CommaSeparatedList.make(c.getString(9));
apk.compatible = compatible;
apk.repo = repoid;
app.apks.add(apk);
if (app.iconUrl == null && app.icon != null) {
int repoVersion = c.getInt(12);
String repoAddress = c.getString(13);
if (repoVersion >= 11) {
app.iconUrl = repoAddress + iconsDir + app.icon;
if (apk.repoVersion >= 11) {
app.iconUrl = apk.repoAddress + iconsDir + app.icon;
} else {
app.iconUrl = repoAddress + "/icons/" + app.icon;
app.iconUrl = apk.repoAddress + "/icons/" + app.icon;
}
}
c.moveToNext();
}
c.close();
} catch (Exception e) {
Log.e("FDroid",
@ -948,8 +753,8 @@ public class DB {
// in the repos.
Log.d("FDroid", "AppUpdate: " + app.name
+ " is no longer in any repository - removing");
db.delete(TABLE_APP, "id = ?", new String[] { app.id });
db.delete(TABLE_APK, "id = ?", new String[] { app.id });
db.delete(TABLE_APP, "id = ?", new String[]{app.id});
ApkProvider.Helper.deleteApksByApp(mContext, app);
} else {
for (Apk apk : app.apks) {
if (!apk.updated) {
@ -958,8 +763,7 @@ public class DB {
Log.d("FDroid", "AppUpdate: Package " + apk.id + "/"
+ apk.version
+ " is no longer in any repository - removing");
db.delete(TABLE_APK, "id = ? and version = ?",
new String[] { app.id, apk.version });
ApkProvider.Helper.delete(mContext, app.id, apk.vercode);
}
}
}
@ -1014,9 +818,13 @@ public class DB {
boolean afound = false;
for (Apk apk : app.apks) {
if (apk.vercode == upapk.vercode) {
// Log.d("FDroid", "AppUpdate: " + apk.version
// + " is a known version.");
updateApkIfDifferent(apk, upapk);
ApkProvider.Helper.update(
mContext,
upapk,
apk.id,
apk.vercode);
apk.updated = true;
afound = true;
break;
@ -1024,7 +832,7 @@ public class DB {
}
if (!afound) {
// A new version of this application.
updateApkIfDifferent(null, upapk);
ApkProvider.Helper.insert(mContext, upapk);
upapk.updated = true;
app.apks.add(upapk);
}
@ -1036,7 +844,7 @@ public class DB {
// It's a brand new application...
updateApp(null, upapp);
for (Apk upapk : upapp.apks) {
updateApkIfDifferent(null, upapk);
ApkProvider.Helper.insert(mContext, upapk);
upapk.updated = true;
}
upapp.updated = true;
@ -1095,41 +903,6 @@ public class DB {
}
}
// Update apk details in the database, if different to the
// previous ones.
// 'oldapk' - previous details - i.e. what's in the database.
// If null, this apk is not in the database at all and
// should be added.
// 'upapk' - updated details
private void updateApkIfDifferent(Apk oldapk, Apk upapk) {
ContentValues values = new ContentValues();
values.put("id", upapk.id);
values.put("version", upapk.version);
values.put("vercode", upapk.vercode);
values.put("repo", upapk.repo);
values.put("hash", upapk.detail_hash);
values.put("hashType", upapk.detail_hashType);
values.put("sig", upapk.sig);
values.put("srcname", upapk.srcname);
values.put("size", upapk.detail_size);
values.put("apkName", upapk.apkName);
values.put("minSdkVersion", upapk.minSdkVersion);
values.put("added",
upapk.added == null ? "" : DATE_FORMAT.format(upapk.added));
values.put("permissions",
CommaSeparatedList.str(upapk.detail_permissions));
values.put("features", CommaSeparatedList.str(upapk.features));
values.put("nativecode", CommaSeparatedList.str(upapk.nativecode));
values.put("compatible", upapk.compatible ? 1 : 0);
if (oldapk != null) {
db.update(TABLE_APK, values,
"id = ? and vercode = ?",
new String[] { oldapk.id, Integer.toString(oldapk.vercode) });
} else {
db.insert(TABLE_APK, null, values);
}
}
public void setIgnoreUpdates(String appid, boolean All, int This) {
db.execSQL("update " + TABLE_APP + " set"
+ " ignoreAllUpdates=" + (All ? '1' : '0')
@ -1141,7 +914,7 @@ public class DB {
db.beginTransaction();
try {
db.delete(TABLE_APK, "repo = ?", new String[] { Long.toString(repo.getId()) });
ApkProvider.Helper.deleteApksByRepo(mContext, repo);
List<App> apps = getApps(false);
for (App app : apps) {
if (app.apks.isEmpty()) {

View File

@ -27,10 +27,11 @@ import java.io.OutputStream;
import java.net.URL;
import android.util.Log;
import org.fdroid.fdroid.data.Apk;
public class Downloader extends Thread {
private DB.Apk curapk;
private Apk curapk;
private String repoaddress;
private String filename;
private File destdir;
@ -38,11 +39,11 @@ public class Downloader extends Thread {
public static enum Status {
STARTING, RUNNING, ERROR, DONE, CANCELLED
};
}
public static enum Error {
CORRUPT, UNKNOWN
};
}
private Status status = Status.STARTING;
private Error error;
@ -52,7 +53,7 @@ public class Downloader extends Thread {
// Constructor - creates a Downloader to download the given Apk,
// which must have its detail populated.
Downloader(DB.Apk apk, String repoaddress, File destdir) {
Downloader(Apk apk, String repoaddress, File destdir) {
curapk = apk;
this.repoaddress = repoaddress;
this.destdir = destdir;
@ -91,7 +92,7 @@ public class Downloader extends Thread {
}
// The APK being downloaded
public synchronized DB.Apk getApk() {
public synchronized Apk getApk() {
return curapk;
}

View File

@ -20,6 +20,7 @@
package org.fdroid.fdroid;
import android.os.Bundle;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.updater.RepoUpdater;
import org.xml.sax.Attributes;
@ -40,7 +41,7 @@ public class RepoXMLHandler extends DefaultHandler {
private List<DB.App> appsList;
private DB.App curapp = null;
private DB.Apk curapk = null;
private Apk curapk = null;
private StringBuilder curchars = new StringBuilder();
// After processing the XML, these will be -1 if the index didn't specify
@ -280,7 +281,7 @@ public class RepoXMLHandler extends DefaultHandler {
totalAppCount, progressData));
} else if (localName.equals("package") && curapp != null && curapk == null) {
curapk = new DB.Apk();
curapk = new Apk();
curapk.id = curapp.id;
curapk.repo = repo.getId();
hashType = null;

View File

@ -49,6 +49,7 @@ import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.updater.RepoUpdater;
@ -351,7 +352,7 @@ public class UpdateService extends IntentService implements ProgressListener {
for (long keep : keeprepos) {
for (DB.App app : apps) {
boolean keepapp = false;
for (DB.Apk apk : app.apks) {
for (Apk apk : app.apks) {
if (apk.repo == keep) {
keepapp = true;
break;
@ -371,7 +372,7 @@ public class UpdateService extends IntentService implements ProgressListener {
}
app_k.updated = true;
db.populateDetails(app_k, keep);
for (DB.Apk apk : app.apks)
for (Apk apk : app.apks)
if (apk.repo == keep)
apk.updated = true;
}

View File

@ -0,0 +1,216 @@
package org.fdroid.fdroid.data;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.preference.PreferenceManager;
import android.util.Log;
import org.fdroid.fdroid.DB;
import org.fdroid.fdroid.compat.Compatibility;
import org.fdroid.fdroid.compat.SupportedArchitectures;
import java.util.Date;
import java.util.Set;
import java.util.HashSet;
public class Apk {
public String id;
public String version;
public int vercode;
public int detail_size; // Size in bytes - 0 means we don't know!
public long repo; // ID of the repo it comes from
public String detail_hash;
public String detail_hashType;
public int minSdkVersion; // 0 if unknown
public Date added;
public DB.CommaSeparatedList detail_permissions; // null if empty or
// unknown
public DB.CommaSeparatedList features; // null if empty or unknown
public DB.CommaSeparatedList nativecode; // null if empty or unknown
// ID (md5 sum of public key) of signature. Might be null, in the
// transition to this field existing.
public String sig;
// True if compatible with the device.
public boolean compatible;
public String apkName;
// If not null, this is the name of the source tarball for the
// application. Null indicates that it's a developer's binary
// build - otherwise it's built from source.
public String srcname;
// Used internally for tracking during repo updates.
public boolean updated;
public int repoVersion;
public String repoAddress;
public DB.CommaSeparatedList incompatible_reasons;
public Apk() {
updated = false;
detail_size = 0;
added = null;
repo = 0;
detail_hash = null;
detail_hashType = null;
detail_permissions = null;
compatible = false;
}
public Apk(Cursor cursor) {
for(int i = 0; i < cursor.getColumnCount(); i ++ ) {
String column = cursor.getColumnName(i);
if (column.equals(ApkProvider.DataColumns.HASH)) {
detail_hash = cursor.getString(i);
} else if (column.equals(ApkProvider.DataColumns.HASH_TYPE)) {
detail_hashType = cursor.getString(i);
} else if (column.equals(ApkProvider.DataColumns.ADDED_DATE)) {
added = ValueObject.toDate(cursor.getString(i));
} else if (column.equals(ApkProvider.DataColumns.FEATURES)) {
features = DB.CommaSeparatedList.make(cursor.getString(i));
} else if (column.equals(ApkProvider.DataColumns.APK_ID)) {
id = cursor.getString(i);
} else if (column.equals(ApkProvider.DataColumns.IS_COMPATIBLE)) {
compatible = cursor.getInt(i) == 1;
} else if (column.equals(ApkProvider.DataColumns.MIN_SDK_VERSION)) {
minSdkVersion = cursor.getInt(i);
} else if (column.equals(ApkProvider.DataColumns.NAME)) {
apkName = cursor.getString(i);
} else if (column.equals(ApkProvider.DataColumns.PERMISSIONS)) {
detail_permissions = DB.CommaSeparatedList.make(cursor.getString(i));
} else if (column.equals(ApkProvider.DataColumns.NATIVE_CODE)) {
nativecode = DB.CommaSeparatedList.make(cursor.getString(i));
} else if (column.equals(ApkProvider.DataColumns.REPO_ID)) {
repo = cursor.getInt(i);
} else if (column.equals(ApkProvider.DataColumns.SIGNATURE)) {
sig = cursor.getString(i);
} else if (column.equals(ApkProvider.DataColumns.SIZE)) {
detail_size = cursor.getInt(i);
} else if (column.equals(ApkProvider.DataColumns.SOURCE_NAME)) {
srcname = cursor.getString(i);
} else if (column.equals(ApkProvider.DataColumns.VERSION)) {
version = cursor.getString(i);
} else if (column.equals(ApkProvider.DataColumns.VERSION_CODE)) {
vercode = cursor.getInt(i);
} else if (column.equals(ApkProvider.DataColumns.REPO_VERSION)) {
repoVersion = cursor.getInt(i);
} else if (column.equals(ApkProvider.DataColumns.REPO_ADDRESS)) {
repoAddress = cursor.getString(i);
}
}
}
public ContentValues toContentValues() {
ContentValues values = new ContentValues();
values.put(ApkProvider.DataColumns.APK_ID, id);
values.put(ApkProvider.DataColumns.VERSION, version);
values.put(ApkProvider.DataColumns.VERSION_CODE, vercode);
values.put(ApkProvider.DataColumns.REPO_ID, repo);
values.put(ApkProvider.DataColumns.HASH, detail_hash);
values.put(ApkProvider.DataColumns.HASH_TYPE, detail_hashType);
values.put(ApkProvider.DataColumns.SIGNATURE, sig);
values.put(ApkProvider.DataColumns.SOURCE_NAME, srcname);
values.put(ApkProvider.DataColumns.SIZE, detail_size);
values.put(ApkProvider.DataColumns.NAME, apkName);
values.put(ApkProvider.DataColumns.MIN_SDK_VERSION, minSdkVersion);
values.put(ApkProvider.DataColumns.ADDED_DATE,
added == null ? "" : DB.DATE_FORMAT.format(added));
values.put(ApkProvider.DataColumns.PERMISSIONS,
DB.CommaSeparatedList.str(detail_permissions));
values.put(ApkProvider.DataColumns.FEATURES, DB.CommaSeparatedList.str(features));
values.put(ApkProvider.DataColumns.NATIVE_CODE, DB.CommaSeparatedList.str(nativecode));
values.put(ApkProvider.DataColumns.IS_COMPATIBLE, compatible ? 1 : 0);
return values;
}
// Call isCompatible(apk) on an instance of this class to
// check if an APK is compatible with the user's device.
public static class CompatibilityChecker extends Compatibility {
private Set<String> features;
private Set<String> cpuAbis;
private String cpuAbisDesc;
private boolean ignoreTouchscreen;
public CompatibilityChecker(Context ctx) {
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(ctx);
ignoreTouchscreen = prefs
.getBoolean("ignoreTouchscreen", false);
PackageManager pm = ctx.getPackageManager();
StringBuilder logMsg = new StringBuilder();
logMsg.append("Available device features:");
features = new HashSet<String>();
if (pm != null) {
for (FeatureInfo fi : pm.getSystemAvailableFeatures()) {
features.add(fi.name);
logMsg.append('\n');
logMsg.append(fi.name);
}
}
cpuAbis = SupportedArchitectures.getAbis();
StringBuilder builder = new StringBuilder();
boolean first = true;
for (String abi : cpuAbis) {
if (first) first = false;
else builder.append(", ");
builder.append(abi);
}
cpuAbisDesc = builder.toString();
builder = null;
Log.d("FDroid", logMsg.toString());
}
private boolean compatibleApi(DB.CommaSeparatedList nativecode) {
if (nativecode == null) return true;
for (String abi : nativecode) {
if (cpuAbis.contains(abi)) {
return true;
}
}
return false;
}
public boolean isCompatible(Apk apk) {
if (!hasApi(apk.minSdkVersion)) {
apk.incompatible_reasons = DB.CommaSeparatedList.make(String.valueOf(apk.minSdkVersion));
return false;
}
if (apk.features != null) {
for (String feat : apk.features) {
if (ignoreTouchscreen
&& feat.equals("android.hardware.touchscreen")) {
// Don't check it!
} else if (!features.contains(feat)) {
apk.incompatible_reasons = DB.CommaSeparatedList.make(feat);
Log.d("FDroid", apk.id + " vercode " + apk.vercode
+ " is incompatible based on lack of "
+ feat);
return false;
}
}
}
if (!compatibleApi(apk.nativecode)) {
apk.incompatible_reasons = apk.nativecode;
Log.d("FDroid", apk.id + " vercode " + apk.vercode
+ " only supports " + DB.CommaSeparatedList.str(apk.nativecode)
+ " while your architectures are " + cpuAbisDesc);
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,370 @@
package org.fdroid.fdroid.data;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.util.Log;
import org.fdroid.fdroid.DB;
import java.util.*;
public class ApkProvider extends FDroidProvider {
public static final class Helper {
private Helper() {}
public static void update(Context context, Apk apk,
String id, int versionCode) {
ContentResolver resolver = context.getContentResolver();
Uri uri = getContentUri(id, versionCode);
resolver.update(uri, apk.toContentValues(), null, null);
}
public static void update(Context context, Apk apk) {
ContentResolver resolver = context.getContentResolver();
Uri uri = getContentUri(apk.id, apk.vercode);
resolver.update(uri, apk.toContentValues(), null, null);
}
/**
* This doesn't do anything other than call "insert" on the content
* resolver, but I thought I'd put it here in the interests of having
* each of the CRUD methods available in the helper class.
*/
public static void insert(Context context, ContentValues values) {
ContentResolver resolver = context.getContentResolver();
resolver.insert(getContentUri(), values);
}
public static void insert(Context context, Apk apk) {
insert(context, apk.toContentValues());
}
public static List<Apk> all(Context context) {
return all(context, DataColumns.ALL);
}
public static List<Apk> all(Context context, String[] projection) {
ContentResolver resolver = context.getContentResolver();
Uri uri = ApkProvider.getContentUri();
Cursor cursor = resolver.query(uri, projection, null, null, null);
return cursorToList(cursor);
}
private static List<Apk> cursorToList(Cursor cursor) {
List<Apk> apks = new ArrayList<Apk>();
if (cursor != null) {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
apks.add(new Apk(cursor));
cursor.moveToNext();
}
cursor.close();
}
return apks;
}
public static void deleteApksByRepo(Context context, Repo repo) {
ContentResolver resolver = context.getContentResolver();
Uri uri = getContentUri();
String[] args = { Long.toString(repo.getId()) };
String selection = DataColumns.REPO_ID + " = ?";
resolver.delete(uri, selection + " = ?", args);
}
public static void deleteApksByApp(Context context, DB.App app) {
ContentResolver resolver = context.getContentResolver();
Uri uri = getContentUri();
String[] args = { app.id };
String selection = DataColumns.APK_ID + " = ?";
resolver.delete(uri, selection, args);
}
public static Apk find(Context context, String id, int versionCode) {
return find(context, id, versionCode, DataColumns.ALL);
}
public static Apk find(Context context, String id, int versionCode, String[] projection) {
ContentResolver resolver = context.getContentResolver();
Uri uri = getContentUri(id, versionCode);
Cursor cursor = resolver.query(uri, projection, null, null, null);
if (cursor != null && cursor.getCount() > 0) {
return new Apk(cursor);
} else {
return null;
}
}
public static void delete(Context context, String id, int versionCode) {
ContentResolver resolver = context.getContentResolver();
Uri uri = getContentUri(id, versionCode);
resolver.delete(uri, null, null);
}
}
public interface DataColumns extends BaseColumns {
public static String APK_ID = "id";
public static String VERSION = "version";
public static String REPO_ID = "repo";
public static String HASH = "hash";
public static String VERSION_CODE = "vercode";
public static String NAME = "apkName";
public static String SIZE = "size";
public static String SIGNATURE = "sig";
public static String SOURCE_NAME = "srcname";
public static String MIN_SDK_VERSION = "minSdkVersion";
public static String PERMISSIONS = "permissions";
public static String FEATURES = "features";
public static String NATIVE_CODE = "nativecode";
public static String HASH_TYPE = "hashType";
public static String ADDED_DATE = "added";
public static String IS_COMPATIBLE = "compatible";
public static String REPO_VERSION = "repoVersion";
public static String REPO_ADDRESS = "repoAddress";
public static String[] ALL = {
_ID, APK_ID, VERSION, REPO_ID, HASH, VERSION_CODE, NAME, SIZE,
SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, PERMISSIONS, FEATURES,
NATIVE_CODE, HASH_TYPE, ADDED_DATE, IS_COMPATIBLE,
REPO_VERSION, REPO_ADDRESS
};
}
private static final String PROVIDER_NAME = "ApkProvider";
private static final UriMatcher matcher = new UriMatcher(-1);
public static Map<String,String> REPO_FIELDS = new HashMap<String,String>();
static {
REPO_FIELDS.put(DataColumns.REPO_VERSION, RepoProvider.DataColumns.VERSION);
REPO_FIELDS.put(DataColumns.REPO_ADDRESS, RepoProvider.DataColumns.ADDRESS);
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST);
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "/*/#", CODE_SINGLE);
}
public static Uri getContentUri() {
return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME);
}
public static Uri getContentUri(String id, int versionCode) {
return getContentUri()
.buildUpon()
.appendPath(Integer.toString(versionCode))
.appendPath(id)
.build();
}
@Override
protected String getTableName() {
return DBHelper.TABLE_APK;
}
@Override
protected String getProviderName() {
return PROVIDER_NAME;
}
protected UriMatcher getMatcher() {
return matcher;
}
private static class QueryBuilder {
private StringBuilder fields = new StringBuilder();
private StringBuilder tables = new StringBuilder(DBHelper.TABLE_APK + " AS apk");
private String selection = null;
private String orderBy = null;
private boolean repoTableRequired = false;
public void addField(String field) {
if (REPO_FIELDS.containsKey(field)) {
addRepoField(REPO_FIELDS.get(field), field);
} else if (field.equals(DataColumns._ID)) {
appendField("rowid", "apk", "_id");
} else if (field.startsWith("COUNT")) {
appendField(field);
} else {
appendField(field, "apk");
}
}
public void addRepoField(String field, String alias) {
if (!repoTableRequired) {
repoTableRequired = true;
tables.append(" LEFT JOIN ");
tables.append(DBHelper.TABLE_REPO);
tables.append(" AS repo ON (apk.repo = repo._id) ");
}
appendField(field, "repo", alias);
}
private void appendField(String field) {
appendField(field, null, null);
}
private void appendField(String field, String tableAlias) {
appendField(field, tableAlias, null);
}
private void appendField(String field, String tableAlias,
String fieldAlias) {
if (fields.length() != 0) {
fields.append(',');
}
if (tableAlias != null) {
fields.append(tableAlias).append('.');
}
fields.append(field);
if (fieldAlias != null) {
fields.append(" AS ").append(fieldAlias);
}
}
public void addSelection(String selection) {
this.selection = selection;
}
public void addOrderBy(String orderBy) {
this.orderBy = orderBy;
}
public String toString() {
StringBuilder suffix = new StringBuilder();
if (selection != null) {
suffix.append(" WHERE ").append(selection);
}
if (orderBy != null) {
suffix.append(" ORDER BY ").append(orderBy);
}
return "SELECT " + fields + " FROM " + tables + suffix;
}
}
private String appendPrimaryKeyToSelection(String selection) {
return (selection == null ? "" : selection + " AND ") + " id = ? and vercode = ?";
}
private String[] appendPrimaryKeyToArgs(Uri uri, String[] selectionArgs) {
List<String> args = new ArrayList<String>(selectionArgs.length + 2);
for (String arg : args) {
args.add(arg);
}
args.addAll(uri.getPathSegments());
return (String[])args.toArray();
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
switch (matcher.match(uri)) {
case CODE_LIST:
break;
case CODE_SINGLE:
selection = appendPrimaryKeyToSelection(selection);
selectionArgs = appendPrimaryKeyToArgs(uri, selectionArgs);
break;
default:
Log.e("FDroid", "Invalid URI for apk content provider: " + uri);
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
}
QueryBuilder query = new QueryBuilder();
for (String field : projection) {
query.addField(field);
}
query.addSelection(selection);
query.addOrderBy(sortOrder);
Cursor cursor = read().rawQuery(query.toString(), selectionArgs);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
private static void removeRepoFields(ContentValues values) {
for (Map.Entry<String,String> repoField : REPO_FIELDS.entrySet()) {
String field = repoField.getKey();
if (values.containsKey(field)) {
Log.i("FDroid", "Cannot insert/update '" + field + "' field " +
"on apk table, as it belongs to the repo table. " +
"This field will be ignored.");
values.remove(field);
}
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
removeRepoFields(values);
long id = write().insertOrThrow(getTableName(), null, values);
getContext().getContentResolver().notifyChange(uri, null);
return getContentUri(
values.getAsString(DataColumns.APK_ID),
values.getAsInteger(DataColumns.VERSION_CODE));
}
@Override
public int delete(Uri uri, String where, String[] whereArgs) {
switch (matcher.match(uri)) {
case CODE_LIST:
// Don't support deleting of multiple apks yet.
return 0;
case CODE_SINGLE:
where = appendPrimaryKeyToSelection(where);
whereArgs = appendPrimaryKeyToArgs(uri, whereArgs);
break;
default:
Log.e("FDroid", "Invalid URI for apk content provider: " + uri);
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
}
int rowsAffected = write().delete(getTableName(), where, whereArgs);
getContext().getContentResolver().notifyChange(uri, null);
return rowsAffected;
}
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
switch (matcher.match(uri)) {
case CODE_LIST:
return 0;
case CODE_SINGLE:
where = appendPrimaryKeyToSelection(where);
whereArgs = appendPrimaryKeyToArgs(uri, whereArgs);
break;
}
removeRepoFields(values);
int numRows = write().update(getTableName(), values, where, whereArgs);
getContext().getContentResolver().notifyChange(uri, null);
return numRows;
}
}

View File

@ -18,6 +18,11 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String TABLE_REPO = "fdroid_repo";
// The TABLE_APK table stores details of all the application versions we
// know about. Each relates directly back to an entry in TABLE_APP.
// This information is retrieved from the repositories.
public static final String TABLE_APK = "fdroid_apk";
private static final String CREATE_TABLE_REPO = "create table "
+ TABLE_REPO + " (_id integer primary key, "
+ "address text not null, "
@ -27,15 +32,26 @@ public class DBHelper extends SQLiteOpenHelper {
+ "version integer not null default 0, "
+ "lastetag text, lastUpdated string);";
private static final String CREATE_TABLE_APK = "create table " + DB.TABLE_APK
+ " ( " + "id text not null, " + "version text not null, "
+ "repo integer not null, " + "hash text not null, "
+ "vercode int not null," + "apkName text not null, "
+ "size int not null," + "sig string," + "srcname string,"
+ "minSdkVersion integer," + "permissions string,"
+ "features string," + "nativecode string,"
+ "hashType string," + "added string,"
+ "compatible int not null," + "primary key(id,vercode));";
private static final String CREATE_TABLE_APK =
"CREATE TABLE " + TABLE_APK + " ( "
+ "id text not null, "
+ "version text not null, "
+ "repo integer not null, "
+ "hash text not null, "
+ "vercode int not null,"
+ "apkName text not null, "
+ "size int not null, "
+ "sig string, "
+ "srcname string, "
+ "minSdkVersion integer, "
+ "permissions string, "
+ "features string, "
+ "nativecode string, "
+ "hashType string, "
+ "added string, "
+ "compatible int not null, "
+ "primary key(id, vercode)"
+ ");";
private static final String CREATE_TABLE_APP = "create table " + DB.TABLE_APP
+ " ( " + "id text not null, " + "name text not null, "
@ -308,7 +324,7 @@ public class DBHelper extends SQLiteOpenHelper {
context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit()
.putBoolean("triedEmptyUpdate", false).commit();
db.execSQL("drop table " + DB.TABLE_APP);
db.execSQL("drop table " + DB.TABLE_APK);
db.execSQL("drop table " + TABLE_APK);
db.execSQL("update " + TABLE_REPO + " set lastetag = NULL");
createAppApk(db);
}
@ -317,8 +333,8 @@ public class DBHelper extends SQLiteOpenHelper {
db.execSQL(CREATE_TABLE_APP);
db.execSQL("create index app_id on " + DB.TABLE_APP + " (id);");
db.execSQL(CREATE_TABLE_APK);
db.execSQL("create index apk_vercode on " + DB.TABLE_APK + " (vercode);");
db.execSQL("create index apk_id on " + DB.TABLE_APK + " (id);");
db.execSQL("create index apk_vercode on " + TABLE_APK + " (vercode);");
db.execSQL("create index apk_id on " + TABLE_APK + " (id);");
}
private static boolean columnExists(SQLiteDatabase db,

View File

@ -10,7 +10,7 @@ import java.net.URL;
import java.text.ParseException;
import java.util.Date;
public class Repo {
public class Repo extends ValueObject{
private long id;
@ -46,14 +46,7 @@ public class Repo {
} else if (column.equals(RepoProvider.DataColumns.IN_USE)) {
inuse = cursor.getInt(i) == 1;
} else if (column.equals(RepoProvider.DataColumns.LAST_UPDATED)) {
String dateString = cursor.getString(i);
if (dateString != null) {
try {
lastUpdated = DB.DATE_FORMAT.parse(dateString);
} catch (ParseException e) {
Log.e("FDroid", "Error parsing date " + dateString);
}
}
lastUpdated = toDate(cursor.getString(i));
} else if (column.equals(RepoProvider.DataColumns.MAX_AGE)) {
maxage = cursor.getInt(i);
} else if (column.equals(RepoProvider.DataColumns.VERSION)) {
@ -78,13 +71,6 @@ public class Repo {
return address;
}
public int getNumberOfApps() {
DB db = DB.getDB();
int count = db.countAppsForRepo(id);
DB.releaseDB();
return count;
}
public boolean isSigned() {
return this.pubkey != null && this.pubkey.length() > 0;
}

View File

@ -76,6 +76,7 @@ public class RepoProvider extends FDroidProvider {
repos.add(new Repo(cursor));
cursor.moveToNext();
}
cursor.close();
}
return repos;
}
@ -163,6 +164,20 @@ public class RepoProvider extends FDroidProvider {
}
}
public static int countAppsForRepo(ContentResolver resolver,
long repoId) {
String[] projection = { "COUNT(distinct id)" };
String selection = "repo = ?";
String[] args = { Long.toString(repoId) };
Uri apkUri = ApkProvider.getContentUri();
Cursor result = resolver.query(apkUri, projection, selection, args, null);
if (result != null && result.getCount() > 0) {
result.moveToFirst();
return result.getInt(0);
} else {
return 0;
}
}
}
public interface DataColumns extends BaseColumns {
@ -189,12 +204,12 @@ public class RepoProvider extends FDroidProvider {
private static final UriMatcher matcher = new UriMatcher(-1);
static {
matcher.addURI(AUTHORITY, PROVIDER_NAME, CODE_LIST);
matcher.addURI(AUTHORITY, PROVIDER_NAME + "/#", CODE_SINGLE);
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST);
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE);
}
public static Uri getContentUri() {
return Uri.parse("content://" + AUTHORITY + "/" + PROVIDER_NAME);
return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME);
}
public static Uri getContentUri(long repoId) {
@ -226,8 +241,8 @@ public class RepoProvider extends FDroidProvider {
break;
case CODE_SINGLE:
selection = ( selection == null ? "" : selection ) +
"_ID = " + uri.getLastPathSegment();
selection = ( selection == null ? "" : selection + " AND " ) +
DataColumns._ID + " = " + uri.getLastPathSegment();
break;
default:
@ -287,7 +302,7 @@ public class RepoProvider extends FDroidProvider {
return 0;
case CODE_SINGLE:
where = ( where == null ? "" : where ) +
where = ( where == null ? "" : where + " AND " ) +
"_ID = " + uri.getLastPathSegment();
break;

View File

@ -0,0 +1,23 @@
package org.fdroid.fdroid.data;
import android.util.Log;
import org.fdroid.fdroid.DB;
import java.text.ParseException;
import java.util.Date;
abstract class ValueObject {
static Date toDate(String string) {
Date date = null;
if (string != null) {
try {
date = DB.DATE_FORMAT.parse(string);
} catch (ParseException e) {
Log.e("FDroid", "Error parsing date " + string);
}
}
return date;
}
}

View File

@ -148,7 +148,10 @@ public class RepoDetailsFragment extends Fragment {
TextView lastUpdated = (TextView)repoView.findViewById(R.id.text_last_update);
name.setText(repo.getName());
numApps.setText(Integer.toString(repo.getNumberOfApps()));
int appCount = RepoProvider.Helper.countAppsForRepo(
getActivity().getContentResolver(), repo.getId());
numApps.setText(Integer.toString(appCount));
setupDescription(repoView, repo);
setupRepoFingerprint(repoView, repo);