From b3773a156121cfce94aa6db769c7c7ff5fb2913d Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 31 Jan 2014 03:15:48 +1100 Subject: [PATCH] Refactoring Apk references into content provider. Removed DB.Apk in favour of stand-alone Apk class. Conflicts: src/org/fdroid/fdroid/DB.java --- AndroidManifest.xml | 7 +- TODO | 3 + src/org/fdroid/fdroid/AppDetails.java | 21 +- src/org/fdroid/fdroid/DB.java | 297 ++------------ src/org/fdroid/fdroid/Downloader.java | 11 +- src/org/fdroid/fdroid/RepoXMLHandler.java | 5 +- src/org/fdroid/fdroid/UpdateService.java | 5 +- src/org/fdroid/fdroid/data/Apk.java | 216 ++++++++++ src/org/fdroid/fdroid/data/ApkProvider.java | 370 ++++++++++++++++++ src/org/fdroid/fdroid/data/DBHelper.java | 40 +- src/org/fdroid/fdroid/data/Repo.java | 18 +- src/org/fdroid/fdroid/data/RepoProvider.java | 27 +- src/org/fdroid/fdroid/data/ValueObject.java | 23 ++ .../views/fragments/RepoDetailsFragment.java | 5 +- 14 files changed, 729 insertions(+), 319 deletions(-) create mode 100644 TODO create mode 100644 src/org/fdroid/fdroid/data/Apk.java create mode 100644 src/org/fdroid/fdroid/data/ApkProvider.java create mode 100644 src/org/fdroid/fdroid/data/ValueObject.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c8b373eb7..248240f6c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -36,10 +36,15 @@ android:supportsRtl="false" > + + diff --git a/TODO b/TODO new file mode 100644 index 000000000..63f46645f --- /dev/null +++ b/TODO @@ -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. diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index c01eccdb5..d39486d8a 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -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 items; + private List items; private LayoutInflater mInflater; - public ApkListAdapter(Context context, List items) { - this.items = new ArrayList(); + public ApkListAdapter(Context context, List items) { + this.items = new ArrayList(); 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 getItems() { + public List 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(); diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index e94374bab..b101cad14 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -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 features; - private Set 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(); - 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 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 apps = getApps(false); for (App app : apps) { if (app.apks.isEmpty()) { diff --git a/src/org/fdroid/fdroid/Downloader.java b/src/org/fdroid/fdroid/Downloader.java index e0cb96ad6..ebd45271c 100644 --- a/src/org/fdroid/fdroid/Downloader.java +++ b/src/org/fdroid/fdroid/Downloader.java @@ -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; } diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index 59a109b7a..a22c98c36 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -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 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; diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index d161cc5ae..7d4063a88 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -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; } diff --git a/src/org/fdroid/fdroid/data/Apk.java b/src/org/fdroid/fdroid/data/Apk.java new file mode 100644 index 000000000..cf0c0a0f7 --- /dev/null +++ b/src/org/fdroid/fdroid/data/Apk.java @@ -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 features; + private Set 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(); + 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; + } + } +} diff --git a/src/org/fdroid/fdroid/data/ApkProvider.java b/src/org/fdroid/fdroid/data/ApkProvider.java new file mode 100644 index 000000000..b1fc75f30 --- /dev/null +++ b/src/org/fdroid/fdroid/data/ApkProvider.java @@ -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 all(Context context) { + return all(context, DataColumns.ALL); + } + + public static List 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 cursorToList(Cursor cursor) { + List apks = new ArrayList(); + 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 REPO_FIELDS = new HashMap(); + + 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 args = new ArrayList(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 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; + + } + +} diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 26c1b3582..58807514c 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -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, diff --git a/src/org/fdroid/fdroid/data/Repo.java b/src/org/fdroid/fdroid/data/Repo.java index ca01bde09..051c634ff 100644 --- a/src/org/fdroid/fdroid/data/Repo.java +++ b/src/org/fdroid/fdroid/data/Repo.java @@ -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; } diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java index 3f955fd37..6466af81f 100644 --- a/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -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; diff --git a/src/org/fdroid/fdroid/data/ValueObject.java b/src/org/fdroid/fdroid/data/ValueObject.java new file mode 100644 index 000000000..fb936cc77 --- /dev/null +++ b/src/org/fdroid/fdroid/data/ValueObject.java @@ -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; + } + +} diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 1e23b034e..d86807c0d 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -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);