diff --git a/.gitignore b/.gitignore index 4b35a9282..4f54aeced 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ /local.properties -.classpath +/.classpath /bin/ /gen/ -/build -/.gradle +/build/ +/.gradle/ /build.xml *~ -.idea -*.iml +/.idea/ +/*.iml out diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 72bc34282..9ffd57e71 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3,7 +3,7 @@ package="org.fdroid.fdroid" android:installLocation="auto" android:versionCode="560" - android:versionName="@string/version_name" > + android:versionName="0.56-test" > { + + public App() { + name = "Unknown"; + summary = "Unknown application"; + icon = null; + id = "unknown"; + license = "Unknown"; + detail_trackerURL = null; + detail_sourceURL = null; + detail_donateURL = null; + detail_bitcoinAddr = null; + detail_litecoinAddr = null; + detail_dogecoinAddr = null; + detail_webURL = null; + categories = null; + antiFeatures = null; + requirements = null; + hasUpdates = false; + toUpdate = false; + updated = false; + added = null; + lastUpdated = null; + apks = new ArrayList(); + detail_Populated = false; + compatible = false; + ignoreAllUpdates = false; + ignoreThisUpdate = 0; + filtered = false; + iconUrl = null; + } + + // True when all the detail fields are populated, False otherwise. + public boolean detail_Populated; + + // True if compatible with the device (i.e. if at least one apk is) + public boolean compatible; + + public String id; + public String name; + public String summary; + public String icon; + + // Null when !detail_Populated + public String detail_description; + + public String license; + + // Null when !detail_Populated + public String detail_webURL; + + // Null when !detail_Populated + public String detail_trackerURL; + + // Null when !detail_Populated + public String detail_sourceURL; + + // Donate link, or null + // Null when !detail_Populated + public String detail_donateURL; + + // Bitcoin donate address, or null + // Null when !detail_Populated + public String detail_bitcoinAddr; + + // Litecoin donate address, or null + // Null when !detail_Populated + public String detail_litecoinAddr; + + // Dogecoin donate address, or null + // Null when !detail_Populated + public String detail_dogecoinAddr; + + // Flattr donate ID, or null + // Null when !detail_Populated + public String detail_flattrID; + + public String curVersion; + public int curVercode; + public Apk curApk; + public Date added; + public Date lastUpdated; + + // Installed version (or null), version code and whether it was + // installed by the user or bundled with the system. These are valid + // only when getApps() has been called with getinstalledinfo=true. + public String installedVersion; + public int installedVerCode; + public boolean userInstalled; + + // List of categories (as defined in the metadata + // documentation) or null if there aren't any. + public CommaSeparatedList categories; + + // List of anti-features (as defined in the metadata + // documentation) or null if there aren't any. + public CommaSeparatedList antiFeatures; + + // List of special requirements (such as root privileges) or + // null if there aren't any. + public CommaSeparatedList requirements; + + // Whether the app is filtered or not based on AntiFeatures and root + // permission (set in the Settings page) + public boolean filtered; + + // True if there are new versions (apks) available, regardless of + // any filtering + public boolean hasUpdates; + + // True if there are new versions (apks) available and the user wants + // to be notified about them + public boolean toUpdate; + + // True if all updates for this app are to be ignored + public boolean ignoreAllUpdates; + + // True if the current update for this app is to be ignored + public int ignoreThisUpdate; + + // Used internally for tracking during repo updates. + public boolean updated; + + // List of apks. + public List apks; + + public String iconUrl; + + // Get the current version - this will be one of the Apks from 'apks'. + // Can return null if there are no available versions. + // 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() { + + // Try and return the real current version first. It will find the + // closest version smaller than the curVercode, being the same + // vercode if it exists. + if (curVercode > 0) { + int latestcode = -1; + Apk latestapk = null; + for (Apk apk : apks) { + if ((!this.compatible || apk.compatible) + && apk.vercode <= curVercode + && apk.vercode > latestcode) { + latestapk = apk; + latestcode = apk.vercode; + } + } + return latestapk; + } + + // If the current version was not set we return the most recent apk. + if (curVercode == -1) { + int latestcode = -1; + Apk latestapk = null; + for (Apk apk : apks) { + if ((!this.compatible || apk.compatible) + && apk.vercode > latestcode) { + latestapk = apk; + latestcode = apk.vercode; + } + } + return latestapk; + } + + return null; + } + + @Override + public int compareTo(App arg0) { + return name.compareToIgnoreCase(arg0.name); + } + + } + + // 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 int 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 + + // 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 HashSet features; + private List cpuAbis; + private boolean ignoreTouchscreen; + + //@SuppressLint("NewApi") + 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 = new ArrayList(2); + cpuAbis.add(android.os.Build.CPU_ABI); + if (hasApi(8)) { + cpuAbis.add(android.os.Build.CPU_ABI2); + } + + 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)) + 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)) { + Log.d("FDroid", apk.id + " vercode " + apk.vercode + + " is incompatible based on lack of " + + feat); + return false; + } + } + } + if (!compatibleApi(apk.nativecode)) { + Log.d("FDroid", apk.id + " vercode " + apk.vercode + + " only supports " + CommaSeparatedList.str(apk.nativecode) + + " while your architecture is " + cpuAbis.get(0)); + return false; + } + return true; + } + } + } + + // The TABLE_REPO table stores the details of the repositories in use. +<<<<<<< HEAD + public static final String TABLE_REPO = "fdroid_repo"; +======= + private static final String TABLE_REPO = "fdroid_repo"; + private static final String CREATE_TABLE_REPO = "create table " + + TABLE_REPO + " (id integer primary key, address text not null, " + + "name text, description text, inuse integer not null, " + + "priority integer not null, pubkey text, fingerprint text, " + + "maxage integer not null default 0, " + + "version integer not null default 0, " + + "lastetag text);"; +>>>>>>> master + + public static class Repo { + public int id; + public String address; + public String name; + public String description; + public int version; // index version, i.e. what fdroidserver built it - 0 if not specified + public boolean inuse; + public int priority; + public String pubkey; // null for an unsigned repo + public String fingerprint; // always null for an unsigned repo + public int maxage; // maximum age of index that will be accepted - 0 for any + public String lastetag; // last etag we updated from, null forces update + public Date lastUpdated; + + /** + * If we haven't run an update for this repo yet, then the name + * will be unknown, in which case we will just take a guess at an + * appropriate name based on the url (e.g. "fdroid.org/archive") + */ + public String getName() { + if (name == null) { + String tempName = null; + try { + URL url = new URL(address); + tempName = url.getHost() + url.getPath(); + } catch (MalformedURLException e) { + tempName = address; + } + return tempName; + } else { + return name; + } + } + + public String toString() { + return address; + } + + public int getNumberOfApps() { + DB db = DB.getDB(); + int count = db.countAppsForRepo(id); + DB.releaseDB(); + return count; + } + + /** + * @param application In order invalidate the list of apps, we require + * a reference to the top level application. + */ + public void enable(FDroidApp application) { + try { + DB db = DB.getDB(); + List toEnable = new ArrayList(1); + toEnable.add(this); + db.enableRepos(toEnable); + } finally { + DB.releaseDB(); + } + application.invalidateAllApps(); + } + + /** + * @param application See DB.Repo.enable(application) + */ + public void disable(FDroidApp application) { + disableRemove(application, false); + } + + /** + * @param application See DB.Repo.enable(application) + */ + public void remove(FDroidApp application) { + disableRemove(application, true); + } + + /** + * @param application See DB.Repo.enable(application) + */ + private void disableRemove(FDroidApp application, boolean removeAfterDisabling) { + try { + DB db = DB.getDB(); + List toDisable = new ArrayList(1); + toDisable.add(this); + db.doDisableRepos(toDisable, removeAfterDisabling); + } finally { + DB.releaseDB(); + } + application.invalidateAllApps(); + } + + public boolean isSigned() { + return this.pubkey != null && this.pubkey.length() > 0; + } + + public boolean hasBeenUpdated() { + return this.lastetag != null; + } + } + + private int countAppsForRepo(int id) { + String[] selection = { "COUNT(distinct id)" }; + String[] selectionArgs = { Integer.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 pubkey) { + String ret = null; + if (pubkey == null) + return null; + try { + // keytool -list -v gives you the SHA-256 fingerprint + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(Hasher.unhex(pubkey)); + byte[] fingerprint = digest.digest(); + Formatter formatter = new Formatter(new StringBuilder()); + for (int i = 1; i < fingerprint.length; i++) { + formatter.format("%02X", fingerprint[i]); + } + ret = formatter.toString(); + formatter.close(); + } catch (Exception e) { + Log.w("FDroid", "Unable to get certificate fingerprint.\n" + + Log.getStackTraceString(e)); + } + return ret; + } + + /** + * Get the local storage (cache) path. This will also create it if + * it doesn't exist. It can return null if it's currently unavailable. + */ + public static File getDataPath(Context ctx) { + return ContextCompat.create(ctx).getExternalCacheDir(); + } + + private Context mContext; + private Apk.CompatibilityChecker compatChecker = null; + + // The date format used for storing dates (e.g. lastupdated, added) in the + // database. + private SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + private DB(Context ctx) { + + mContext = ctx; + DBHelper h = new DBHelper(ctx); + db = h.getWritableDatabase(); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(mContext); + String sync_mode = prefs.getString("dbSyncMode", null); + if ("off".equals(sync_mode)) + setSynchronizationMode(SYNC_OFF); + else if ("normal".equals(sync_mode)) + setSynchronizationMode(SYNC_NORMAL); + else if ("full".equals(sync_mode)) + setSynchronizationMode(SYNC_FULL); + else + sync_mode = null; + if (sync_mode != null) + Log.d("FDroid", "Database synchronization mode: " + sync_mode); + } + + public void close() { + db.close(); + db = null; + } + + // Delete the database, which should cause it to be re-created next time + // it's used. + public static void delete(Context ctx) { + try { + ctx.deleteDatabase(DBHelper.DATABASE_NAME); + // Also try and delete the old one, from versions 0.13 and earlier. + ctx.deleteDatabase("fdroid_db"); + } catch (Exception ex) { + Log.e("FDroid", + "Exception in DB.delete:\n" + Log.getStackTraceString(ex)); + } + } + + public List getCategories() { + List result = new ArrayList(); + Cursor c = null; + try { + c = db.query(true, TABLE_APP, new String[] { "categories" }, + null, null, null, null, null, null); + c.moveToFirst(); + while (!c.isAfterLast()) { + CommaSeparatedList categories = CommaSeparatedList + .make(c.getString(0)); + if (categories != null) { + for (String category : categories) { + if (!result.contains(category)) { + result.add(category); + } + } + } + c.moveToNext(); + } + } catch (Exception e) { + Log.e("FDroid", + "Exception during database reading:\n" + + Log.getStackTraceString(e)); + } finally { + if (c != null) { + c.close(); + } + } + Collections.sort(result); + return result; + } + + private static final String[] POPULATE_APP_COLS = new String[] { + "description", "webURL", "trackerURL", "sourceURL", + "donateURL", "bitcoinAddr", "flattrID", "litecoinAddr", "dogecoinAddr" }; + + private void populateAppDetails(App app) { + Cursor cursor = null; + try { + cursor = db.query(TABLE_APP, POPULATE_APP_COLS, "id = ?", + new String[] { app.id }, null, null, null, null); + cursor.moveToFirst(); + app.detail_description = cursor.getString(0); + app.detail_webURL = cursor.getString(1); + app.detail_trackerURL = cursor.getString(2); + app.detail_sourceURL = cursor.getString(3); + app.detail_donateURL = cursor.getString(4); + app.detail_bitcoinAddr = cursor.getString(5); + app.detail_flattrID = cursor.getString(6); + app.detail_litecoinAddr = cursor.getString(7); + app.detail_dogecoinAddr = cursor.getString(8); + app.detail_Populated = true; + } catch (Exception e) { + Log.d("FDroid", "Error populating app details " + app.id ); + Log.d("FDroid", e.getMessage()); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private static final String[] POPULATE_APK_COLS = new String[] { "hash", "hashType", "size", "permissions" }; + + private void populateApkDetails(Apk apk, int 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(); + } + } + } else { + Log.d("FDroid", "Not setting details for apk '" + apk.id + "' (version " + apk.version +") because it belongs to a different repo."); + } + } + + // Populate the details for the given app, if necessary. + // If 'apkrepo' is non-zero, only apks from that repo are + // populated (this is used during the update process) + public void populateDetails(App app, int apkRepo) { + if (!app.detail_Populated) { + populateAppDetails(app); + } + + for (Apk apk : app.apks) { + if (apk.detail_hash == null) { + populateApkDetails(apk, apkRepo); + } + } + } + + // 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. + public List getApps(boolean getinstalledinfo) { + + // If we're going to need it, get info in what's currently installed + Map systemApks = null; + if (getinstalledinfo) { + Log.d("FDroid", "Reading installed packages"); + systemApks = new HashMap(); + List installedPackages = mContext.getPackageManager() + .getInstalledPackages(0); + for (PackageInfo appInfo : installedPackages) { + systemApks.put(appInfo.packageName, appInfo); + } + } + + // Start the map at the actual number of apps we will have + Cursor c = db.rawQuery("select count(*) from "+TABLE_APP, null); + c.moveToFirst(); + int count = c.getInt(0); + c.close(); + c = null; + + Log.d("FDroid", "Will be fetching " + count + " apps, and this took us "); + + Map apps = new HashMap(count); + long startTime = System.currentTimeMillis(); + try { + + String cols[] = new String[] { "antiFeatures", "requirements", + "categories", "id", "name", "summary", "icon", "license", + "curVersion", "curVercode", "added", "lastUpdated", + "compatible", "ignoreAllUpdates", "ignoreThisUpdate" }; + c = db.query(TABLE_APP, cols, null, null, null, null, null); + c.moveToFirst(); + while (!c.isAfterLast()) { + + App app = new App(); + app.antiFeatures = DB.CommaSeparatedList.make(c.getString(0)); + app.requirements = DB.CommaSeparatedList.make(c.getString(1)); + app.categories = DB.CommaSeparatedList.make(c.getString(2)); + app.id = c.getString(3); + app.name = c.getString(4); + app.summary = c.getString(5); + app.icon = c.getString(6); + app.license = c.getString(7); + app.curVersion = c.getString(8); + app.curVercode = c.getInt(9); + String sAdded = c.getString(10); + app.added = (sAdded == null || sAdded.length() == 0) ? null + : mDateFormat.parse(sAdded); + String sLastUpdated = c.getString(11); + app.lastUpdated = (sLastUpdated == null || sLastUpdated + .length() == 0) ? null : mDateFormat + .parse(sLastUpdated); + app.compatible = c.getInt(12) == 1; + app.ignoreAllUpdates = c.getInt(13) == 1; + app.ignoreThisUpdate = c.getInt(14); + app.hasUpdates = false; + + if (getinstalledinfo && systemApks.containsKey(app.id)) { + PackageInfo sysapk = systemApks.get(app.id); + app.installedVersion = sysapk.versionName; + if (app.installedVersion == null) + app.installedVersion = "null"; + app.installedVerCode = sysapk.versionCode; + if (sysapk.applicationInfo != null) { + app.userInstalled = ((sysapk.applicationInfo.flags + & ApplicationInfo.FLAG_SYSTEM) != 1); + } + } else { + app.installedVersion = null; + app.installedVerCode = 0; + app.userInstalled = false; + } + + apps.put(app.id, app); + + c.moveToNext(); + } + c.close(); + c = null; + + Log.d("FDroid", "Read app data from database (took " + + (System.currentTimeMillis() - startTime) + " ms)"); + + List repos = getRepos(); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(mContext); + cols = new String[] { "id", "version", "vercode", "sig", "srcname", + "apkName", "minSdkVersion", "added", "features", "nativecode", + "compatible", "repo" }; + c = db.query(TABLE_APK, cols, null, null, null, null, + "vercode desc"); + c.moveToFirst(); + while (!c.isAfterLast()) { + String id = c.getString(0); + App app = apps.get(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 + : mDateFormat.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) { + for (DB.Repo repo : repos) { + if (repo.id == repoid) { + app.iconUrl = + repo.address + "/icons/" + app.icon; + break; + } + } + } + c.moveToNext(); + } + c.close(); + + } catch (Exception e) { + Log.e("FDroid", + "Exception during database reading:\n" + + Log.getStackTraceString(e)); + } finally { + if (c != null) { + c.close(); + } + + Log.d("FDroid", "Read app and apk data from database (took " + + (System.currentTimeMillis() - startTime) + " ms)"); + } + + List result = new ArrayList(apps.values()); + Collections.sort(result); + + // Fill in the hasUpdates fields if we have the necessary information... + if (getinstalledinfo) { + + // We'll say an application has updates if it's installed AND the + // version is older than the current one + for (App app : result) { + app.curApk = app.getCurrentVersion(); + if (app.curApk != null + && app.installedVerCode > 0 + && app.installedVerCode < app.curApk.vercode) { + app.hasUpdates = true; + } + } + } + + return result; + } + + + // Alternative to getApps() that only refreshes the installation details + // of those apps in invalidApps. Much faster when returning from + // installs/uninstalls, where getApps() was already called before. + public List refreshApps(List apps, List invalidApps) { + + List installedPackages = mContext.getPackageManager() + .getInstalledPackages(0); + long startTime = System.currentTimeMillis(); + List refreshedApps = new ArrayList(); + for (String appid : invalidApps) { + if (refreshedApps.contains(appid)) continue; + App app = null; + int index = -1; + for (App oldapp : apps) { + index++; + if (oldapp.id.equals(appid)) { + app = oldapp; + break; + } + } + + if (app == null) continue; + + PackageInfo installed = null; + + for (PackageInfo appInfo : installedPackages) { + if (appInfo.packageName.equals(appid)) { + installed = appInfo; + break; + } + } + + if (installed != null) { + app.installedVersion = installed.versionName; + if (app.installedVersion == null) + app.installedVersion = "null"; + app.installedVerCode = installed.versionCode; + } else { + app.installedVersion = null; + app.installedVerCode = 0; + } + + app.hasUpdates = false; + app.curApk = app.getCurrentVersion(); + if (app.curApk != null + && app.installedVersion != null + && app.installedVerCode < app.curApk.vercode) { + app.hasUpdates = true; + } + + apps.set(index, app); + refreshedApps.add(appid); + } + Log.d("FDroid", "Refreshing " + refreshedApps.size() + " apps took " + + (System.currentTimeMillis() - startTime) + " ms"); + + return apps; + } + + public List doSearch(String query) { + + List ids = new ArrayList(); + Cursor c = null; + try { + String filter = "%" + query + "%"; + c = db.query(TABLE_APP, new String[] { "id" }, + "id like ? or name like ? or summary like ? or description like ?", + new String[] { filter, filter, filter, filter }, null, null, null); + c.moveToFirst(); + while (!c.isAfterLast()) { + ids.add(c.getString(0)); + c.moveToNext(); + } + } finally { + if (c != null) + c.close(); + } + return ids; + } + + public static class CommaSeparatedList implements Iterable { + private String value; + + private CommaSeparatedList(String list) { + value = list; + } + + public static CommaSeparatedList make(String list) { + if (list == null || list.length() == 0) + return null; + else + return new CommaSeparatedList(list); + } + + public static String str(CommaSeparatedList instance) { + return (instance == null ? null : instance.toString()); + } + + @Override + public String toString() { + return value; + } + + @Override + public Iterator iterator() { + SimpleStringSplitter splitter = new SimpleStringSplitter(','); + splitter.setString(value); + return splitter.iterator(); + } + + public boolean contains(String v) { + for (String s : this) { + if (s.equals(v)) + return true; + } + return false; + } + } + + private List updateApps = null; + + // Called before a repo update starts. + public void beginUpdate(List apps) { + // Get a list of all apps. All the apps and apks in this list will + // have 'updated' set to false at this point, and we will only set + // it to true when we see the app/apk in a repository. Thus, at the + // end, any that are still false can be removed. + updateApps = apps; + Log.d("FDroid", "AppUpdate: " + updateApps.size() + " apps before starting."); + // Wrap the whole update in a transaction. Make sure to call + // either endUpdate or cancelUpdate to commit or discard it, + // respectively. + db.beginTransaction(); + } + + // Called when a repo update ends. Any applications that have not been + // updated (by a call to updateApplication) are assumed to be no longer + // in the repos. + public void endUpdate() { + if (updateApps == null) + return; + Log.d("FDroid", "Processing endUpdate - " + updateApps.size() + + " apps before"); + for (App app : updateApps) { + if (!app.updated) { + // The application hasn't been updated, so it's no longer + // 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 }); + } else { + for (Apk apk : app.apks) { + if (!apk.updated) { + // The package hasn't been updated, so this is a + // version that's no longer available. + 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 }); + } + } + } + } + // Commit updates to the database. + db.setTransactionSuccessful(); + db.endTransaction(); + Log.d("FDroid", "AppUpdate: " + updateApps.size() + + " apps on completion."); + updateApps = null; + } + + // Called instead of endUpdate if the update failed. + public void cancelUpdate() { + if (updateApps != null) { + db.endTransaction(); + updateApps = null; + } + } + + // Called during update to supply new details for an application (or + // details of a completely new one). Calls to this must be wrapped by + // a call to beginUpdate and a call to endUpdate. + public void updateApplication(App upapp) { + + if (updateApps == null) { + return; + } + + // Lazy initialise this... + if (compatChecker == null) { + compatChecker = new Apk.CompatibilityChecker(mContext); + } + + // See if it's compatible (by which we mean if it has at least one + // compatible apk) + upapp.compatible = false; + for (Apk apk : upapp.apks) { + if (compatChecker.isCompatible(apk)) { + apk.compatible = true; + upapp.compatible = true; + } + } + + boolean found = false; + for (App app : updateApps) { + if (app.id.equals(upapp.id)) { + updateApp(app, upapp); + app.updated = true; + found = true; + for (Apk upapk : upapp.apks) { + 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); + apk.updated = true; + afound = true; + break; + } + } + if (!afound) { + // A new version of this application. + updateApkIfDifferent(null, upapk); + upapk.updated = true; + app.apks.add(upapk); + } + } + break; + } + } + if (!found) { + // It's a brand new application... + updateApp(null, upapp); + for (Apk upapk : upapp.apks) { + updateApkIfDifferent(null, upapk); + upapk.updated = true; + } + upapp.updated = true; + updateApps.add(upapp); + } + + } + + // Update application details in the database. + // 'oldapp' - previous details - i.e. what's in the database. + // If null, this app is not in the database at all and + // should be added. + // 'upapp' - updated details + private void updateApp(App oldapp, App upapp) { + ContentValues values = new ContentValues(); + values.put("id", upapp.id); + values.put("name", upapp.name); + values.put("summary", upapp.summary); + values.put("icon", upapp.icon); + values.put("description", upapp.detail_description); + values.put("license", upapp.license); + values.put("webURL", upapp.detail_webURL); + values.put("trackerURL", upapp.detail_trackerURL); + values.put("sourceURL", upapp.detail_sourceURL); + values.put("donateURL", upapp.detail_donateURL); + values.put("bitcoinAddr", upapp.detail_bitcoinAddr); + values.put("litecoinAddr", upapp.detail_litecoinAddr); + values.put("dogecoinAddr", upapp.detail_dogecoinAddr); + values.put("flattrID", upapp.detail_flattrID); + values.put("added", + upapp.added == null ? "" : mDateFormat.format(upapp.added)); + values.put( + "lastUpdated", + upapp.added == null ? "" : mDateFormat + .format(upapp.lastUpdated)); + values.put("curVersion", upapp.curVersion); + values.put("curVercode", upapp.curVercode); + values.put("categories", CommaSeparatedList.str(upapp.categories)); + values.put("antiFeatures", CommaSeparatedList.str(upapp.antiFeatures)); + values.put("requirements", CommaSeparatedList.str(upapp.requirements)); + values.put("compatible", upapp.compatible ? 1 : 0); + + // Values to keep if already present + if (oldapp == null) { + values.put("ignoreAllUpdates", upapp.ignoreAllUpdates ? 1 : 0); + values.put("ignoreThisUpdate", upapp.ignoreThisUpdate); + } else { + values.put("ignoreAllUpdates", oldapp.ignoreAllUpdates ? 1 : 0); + values.put("ignoreThisUpdate", oldapp.ignoreThisUpdate); + } + + if (oldapp != null) { + db.update(TABLE_APP, values, "id = ?", new String[] { oldapp.id }); + } else { + db.insert(TABLE_APP, null, values); + } + } + + // 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 ? "" : mDateFormat.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); + } + } + + // Get details of a repo, given the ID. Returns null if the repo + // doesn't exist. + public Repo getRepo(int id) { + Cursor c = null; + try { + c = db.query(TABLE_REPO, new String[] { "address", "name", + "description", "version", "inuse", "priority", "pubkey", + "fingerprint", "maxage", "lastetag", "lastUpdated" }, + "id = ?", new String[] { Integer.toString(id) }, null, null, null); + if (!c.moveToFirst()) + return null; + Repo repo = new Repo(); + repo.id = id; + repo.address = c.getString(0); + repo.name = c.getString(1); + repo.description = c.getString(2); + repo.version = c.getInt(3); + repo.inuse = (c.getInt(4) == 1); + repo.priority = c.getInt(5); + repo.pubkey = c.getString(6); + repo.fingerprint = c.getString(7); + repo.maxage = c.getInt(8); + repo.lastetag = c.getString(9); + try { + repo.lastUpdated = c.getString(10) != null ? + mDateFormat.parse( c.getString(10)) : + null; + } catch (ParseException e) { + Log.e("FDroid", "Error parsing date " + c.getString(10)); + } + return repo; + } finally { + if (c != null) + c.close(); + } + } + + // Get a list of the configured repositories. + public List getRepos() { + List repos = new ArrayList(); + Cursor c = null; + try { + c = db.query(TABLE_REPO, new String[] { "id", "address", "name", + "description", "version", "inuse", "priority", "pubkey", + "fingerprint", "maxage", "lastetag" }, + null, null, null, null, "priority"); + c.moveToFirst(); + while (!c.isAfterLast()) { + Repo repo = new Repo(); + repo.id = c.getInt(0); + repo.address = c.getString(1); + repo.name = c.getString(2); + repo.description = c.getString(3); + repo.version = c.getInt(4); + repo.inuse = (c.getInt(5) == 1); + repo.priority = c.getInt(6); + repo.pubkey = c.getString(7); + repo.fingerprint = c.getString(8); + repo.maxage = c.getInt(9); + repo.lastetag = c.getString(10); + repos.add(repo); + c.moveToNext(); + } + } catch (Exception e) { + } finally { + if (c != null) { + c.close(); + } + } + return repos; + } + + public void enableRepos(List repos) { + if (repos.isEmpty()) return; + + ContentValues values = new ContentValues(1); + values.put("inuse", 1); + + String[] whereArgs = new String[repos.size()]; + StringBuilder where = new StringBuilder("address IN ("); + for (int i = 0; i < repos.size(); i ++) { + Repo repo = repos.get(i); + repo.inuse = true; + whereArgs[i] = repo.address; + where.append('?'); + if ( i < repos.size() - 1 ) { + where.append(','); + } + } + where.append(")"); + db.update(TABLE_REPO, values, where.toString(), whereArgs); + } + + public void changeServerStatus(String address) { + db.execSQL("update " + TABLE_REPO + + " set inuse=1-inuse, lastetag=null where address = ?", + new String[] { address }); + } + + public void setIgnoreUpdates(String appid, boolean All, int This) { + db.execSQL("update " + TABLE_APP + " set" + + " ignoreAllUpdates=" + (All ? '1' : '0') + + ", ignoreThisUpdate="+This + + " where id = ?", new String[] { appid }); + } + + public void updateRepoByAddress(Repo repo) { + updateRepo(repo, "address", repo.address); + } + + public void updateRepo(Repo repo) { + updateRepo(repo, "id", repo.id + ""); + } + + private void updateRepo(Repo repo, String field, String value) { + ContentValues values = new ContentValues(); + values.put("name", repo.name); + values.put("address", repo.address); + values.put("description", repo.description); + values.put("version", repo.version); + values.put("inuse", repo.inuse); + values.put("priority", repo.priority); + values.put("pubkey", repo.pubkey); + if (repo.pubkey != null && repo.fingerprint == null) { + // we got a new pubkey, so calc the fingerprint + values.put("fingerprint", DB.calcFingerprint(repo.pubkey)); + } else { + values.put("fingerprint", repo.fingerprint); + } + values.put("maxage", repo.maxage); + values.put("lastetag", (String) null); + db.update(TABLE_REPO, values, field + " = ?", + new String[] { value }); + } + + /** + * Updates the lastUpdated time for every enabled repo. + */ + public void refreshLastUpdates() { + ContentValues values = new ContentValues(); + values.put("lastUpdated", mDateFormat.format(new Date())); + db.update(TABLE_REPO, values, "inuse = 1", + new String[] {}); + } + + public void writeLastEtag(Repo repo) { + ContentValues values = new ContentValues(); + values.put("lastetag", repo.lastetag); + values.put("lastUpdated", mDateFormat.format(new Date())); + db.update(TABLE_REPO, values, "address = ?", + new String[] { repo.address }); + } + + public void addRepo(String address, String name, String description, + int version, int priority, String pubkey, String fingerprint, + int maxage, boolean inuse) + throws SecurityException { + ContentValues values = new ContentValues(); + values.put("address", address); + values.put("name", name); + values.put("description", description); + values.put("version", version); + values.put("inuse", inuse ? 1 : 0); + values.put("priority", priority); + values.put("pubkey", pubkey); + String calcedFingerprint = DB.calcFingerprint(pubkey); + if (fingerprint == null) { + fingerprint = calcedFingerprint; + } else if (calcedFingerprint != null) { + fingerprint = fingerprint.toUpperCase(); + if (!fingerprint.equals(calcedFingerprint)) { + throw new SecurityException("Given fingerprint does not match calculated one! (" + + fingerprint + " != " + calcedFingerprint); + } + } + values.put("fingerprint", fingerprint); + values.put("maxage", maxage); + values.put("lastetag", (String) null); + db.insert(TABLE_REPO, null, values); + } + + public void doDisableRepos(List repos, boolean remove) { + if (repos.isEmpty()) return; + db.beginTransaction(); + + // TODO: Replace with + // "delete from apk join repo where repo in (?, ?, ...) + // "update repo set inuse = 0 | delete from repo ] where repo in (?, ?, ...) + try { + for (Repo repo : repos) { + + String address = repo.address; + // Before removing the repo, remove any apks that are + // connected to it... + Cursor c = null; + try { + c = db.query(TABLE_REPO, new String[]{"id"}, + "address = ?", new String[]{address}, + null, null, null, null); + c.moveToFirst(); + if (!c.isAfterLast()) { + db.delete(TABLE_APK, "repo = ?", + new String[] { Integer.toString(c.getInt(0)) }); + } + } finally { + if (c != null) { + c.close(); + } + } + if (remove) + db.delete(TABLE_REPO, "address = ?", + new String[] { address }); + else { + ContentValues values = new ContentValues(2); + values.put("inuse", 0); + values.put("lastetag", (String)null); + db.update(TABLE_REPO, values, "address = ?", + new String[] { address }); + } + } + List apps = getApps(false); + for (App app : apps) { + if (app.apks.isEmpty()) { + db.delete(TABLE_APP, "id = ?", new String[] { app.id }); + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + public int getSynchronizationMode() { + Cursor cursor = db.rawQuery("PRAGMA synchronous", null); + cursor.moveToFirst(); + int mode = cursor.getInt(0); + cursor.close(); + return mode; + } + + public void setSynchronizationMode(int mode) { + db.execSQL("PRAGMA synchronous = " + mode); + } +} diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index a961c2fb7..a6d8a3f16 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -23,7 +23,6 @@ import android.content.*; import android.content.res.Configuration; import android.support.v4.view.MenuItemCompat; -import android.annotation.TargetApi; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.NotificationManager; @@ -218,7 +217,6 @@ public class FDroid extends FragmentActivity { return super.onOptionsItemSelected(item); } - @TargetApi(5) @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { diff --git a/src/org/fdroid/fdroid/FDroid.java.orig b/src/org/fdroid/fdroid/FDroid.java.orig new file mode 100644 index 000000000..a4e7cd841 --- /dev/null +++ b/src/org/fdroid/fdroid/FDroid.java.orig @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com + * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import android.content.*; +import android.content.res.Configuration; +import android.support.v4.view.MenuItemCompat; + +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.NotificationManager; +import android.content.pm.PackageInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; +import android.support.v4.view.ViewPager; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.*; +import org.fdroid.fdroid.compat.TabManager; +import org.fdroid.fdroid.views.AppListFragmentPageAdapter; + +public class FDroid extends FragmentActivity { + + public static final int REQUEST_APPDETAILS = 0; + public static final int REQUEST_MANAGEREPOS = 1; + public static final int REQUEST_PREFS = 2; + + public static final String EXTRA_TAB_UPDATE = "extraTab"; + + private static final int UPDATE_REPO = Menu.FIRST; + private static final int MANAGE_REPO = Menu.FIRST + 1; + private static final int PREFERENCES = Menu.FIRST + 2; + private static final int ABOUT = Menu.FIRST + 3; + private static final int SEARCH = Menu.FIRST + 4; + + private ViewPager viewPager; + + private AppListManager manager = null; + + private TabManager tabManager = null; + + public AppListManager getManager() { + return manager; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + ((FDroidApp) getApplication()).applyTheme(this); + + super.onCreate(savedInstanceState); + manager = new AppListManager(this); + setContentView(R.layout.fdroid); + createViews(); + getTabManager().createTabs(); + + // Start a search by just typing + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + + Intent i = getIntent(); + Uri data = i.getData(); + String appid = null; + if (data != null) { + if (data.isHierarchical()) { + // http(s)://f-droid.org/repository/browse?fdid=app.id + appid = data.getQueryParameter("fdid"); + } + } else if (i.hasExtra(EXTRA_TAB_UPDATE)) { + boolean showUpdateTab = i.getBooleanExtra(EXTRA_TAB_UPDATE, false); + if (showUpdateTab) { + getTabManager().selectTab(2); + } + } + if (appid != null && appid.length() > 0) { + Intent call = new Intent(this, AppDetails.class); + call.putExtra("appid", appid); + startActivityForResult(call, REQUEST_APPDETAILS); + } + } + + @Override + protected void onResume() { + super.onResume(); + repopulateViews(); + } + + /** + * Must be done *after* createViews, because it will involve a + * callback to update the tab label for the "update" tab. This + * will fail unless the tabs have actually been created. + */ + protected void repopulateViews() { + manager.repopulateLists(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + getTabManager().onConfigurationChanged(newConfig); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + + super.onCreateOptionsMenu(menu); +<<<<<<< HEAD +======= + menu.add(Menu.NONE, UPDATE_REPO, 1, R.string.menu_update_repo).setIcon( + android.R.drawable.ic_menu_rotate); +>>>>>>> master + menu.add(Menu.NONE, MANAGE_REPO, 2, R.string.menu_manage).setIcon( + android.R.drawable.ic_menu_agenda); + MenuItem search = menu.add(Menu.NONE, SEARCH, 3, R.string.menu_search).setIcon( + android.R.drawable.ic_menu_search); + menu.add(Menu.NONE, PREFERENCES, 4, R.string.menu_preferences).setIcon( + android.R.drawable.ic_menu_preferences); + menu.add(Menu.NONE, ABOUT, 5, R.string.menu_about).setIcon( + android.R.drawable.ic_menu_help); + MenuItemCompat.setShowAsAction(search, MenuItemCompat.SHOW_AS_ACTION_ALWAYS); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + switch (item.getItemId()) { + + case UPDATE_REPO: + updateRepos(); + return true; + + case MANAGE_REPO: + Intent i = new Intent(this, ManageRepo.class); + startActivityForResult(i, REQUEST_MANAGEREPOS); + return true; + + case PREFERENCES: + Intent prefs = new Intent(getBaseContext(), PreferencesActivity.class); + startActivityForResult(prefs, REQUEST_PREFS); + return true; + + case SEARCH: + onSearchRequested(); + return true; + + case ABOUT: + View view = null; + if (Build.VERSION.SDK_INT >= 11) { + LayoutInflater li = LayoutInflater.from(this); + view = li.inflate(R.layout.about, null); + } else { + view = View.inflate( + new ContextThemeWrapper(this, R.style.AboutDialogLight), + R.layout.about, null); + } + + // Fill in the version... + try { + PackageInfo pi = getPackageManager() + .getPackageInfo(getApplicationContext() + .getPackageName(), 0); + ((TextView) view.findViewById(R.id.version)) + .setText(pi.versionName); + } catch (Exception e) { + } + + Builder p = null; + if (Build.VERSION.SDK_INT >= 11) { + p = new AlertDialog.Builder(this).setView(view); + } else { + p = new AlertDialog.Builder( + new ContextThemeWrapper( + this, R.style.AboutDialogLight) + ).setView(view); + } + final AlertDialog alrt = p.create(); + alrt.setIcon(R.drawable.ic_launcher); + alrt.setTitle(getString(R.string.about_title)); + alrt.setButton(AlertDialog.BUTTON_NEUTRAL, + getString(R.string.about_website), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int whichButton) { + Uri uri = Uri.parse("https://f-droid.org"); + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + }); + alrt.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.ok), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int whichButton) { + } + }); + alrt.show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + + switch (requestCode) { + case REQUEST_APPDETAILS: + break; + case REQUEST_MANAGEREPOS: + if (data.hasExtra(ManageRepo.REQUEST_UPDATE)) { + AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); + ask_alrt.setTitle(getString(R.string.repo_update_title)); + ask_alrt.setIcon(android.R.drawable.ic_menu_rotate); + ask_alrt.setMessage(getString(R.string.repo_alrt)); + ask_alrt.setPositiveButton(getString(R.string.yes), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int whichButton) { + updateRepos(); + } + }); + ask_alrt.setNegativeButton(getString(R.string.no), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int whichButton) { + // do nothing + } + }); + AlertDialog alert = ask_alrt.create(); + alert.show(); + } + break; + case REQUEST_PREFS: + // The automatic update settings may have changed, so reschedule (or + // unschedule) the service accordingly. It's cheap, so no need to + // check if the particular setting has actually been changed. + UpdateService.schedule(getBaseContext()); + + if ((resultCode & PreferencesActivity.RESULT_RELOAD) != 0) { + ((FDroidApp) getApplication()).invalidateAllApps(); + } else if ((resultCode & PreferencesActivity.RESULT_REFILTER) != 0) { + ((FDroidApp) getApplication()).filterApps(); + } + + if ((resultCode & PreferencesActivity.RESULT_RESTART) != 0) { + ((FDroidApp) getApplication()).reloadTheme(); + final Intent intent = getIntent(); + overridePendingTransition(0, 0); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + finish(); + overridePendingTransition(0, 0); + startActivity(intent); + } + + break; + } + } + + private void createViews() { + viewPager = (ViewPager)findViewById(R.id.main_pager); + AppListFragmentPageAdapter viewPageAdapter = new AppListFragmentPageAdapter(this); + viewPager.setAdapter(viewPageAdapter); + viewPager.setOnPageChangeListener( new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + getTabManager().selectTab(position); + } + }); + } + + /** + * The first time the app is run, we will have an empty app list. + * If this is the case, we will attempt to update with the default repo. + * However, if we have tried this at least once, then don't try to do + * it automatically again, because the repos or internet connection may + * be bad. + */ + public boolean updateEmptyRepos() { + final String TRIED_EMPTY_UPDATE = "triedEmptyUpdate"; + boolean hasTriedEmptyUpdate = getPreferences(MODE_PRIVATE).getBoolean(TRIED_EMPTY_UPDATE, false); + if (!hasTriedEmptyUpdate) { + Log.d("FDroid", "Empty app list, and we haven't done an update yet. Forcing repo update."); + getPreferences(MODE_PRIVATE).edit().putBoolean(TRIED_EMPTY_UPDATE, true).commit(); + updateRepos(); + return true; + } else { + Log.d("FDroid", "Empty app list, but it looks like we've had an update previously. Will not force repo update."); + return false; + } + } + + // Force a repo update now. A progress dialog is shown and the UpdateService + // is told to do the update, which will result in the database changing. The + // UpdateReceiver class should get told when this is finished. + public void updateRepos() { + UpdateService.updateNow(this).setListener(new ProgressListener() { + @Override + public void onProgress(Event event) { + if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES){ + repopulateViews(); + } + } + }); + } + + private TabManager getTabManager() { + if (tabManager == null) { + tabManager = TabManager.create(this, viewPager); + } + return tabManager; + } + + public void refreshUpdateTabLabel() { + getTabManager().refreshTabLabel(TabManager.INDEX_CAN_UPDATE); + } + + public void removeNotification(int id) { + NotificationManager nMgr = (NotificationManager) getBaseContext() + .getSystemService(Context.NOTIFICATION_SERVICE); + nMgr.cancel(id); + } + +} diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java.orig b/src/org/fdroid/fdroid/RepoXMLHandler.java.orig new file mode 100644 index 000000000..a99f795a6 --- /dev/null +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java.orig @@ -0,0 +1,541 @@ +/* + * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com + * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import android.os.Bundle; +import org.fdroid.fdroid.updater.RepoUpdater; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RepoXMLHandler extends DefaultHandler { + + // The repo we're processing. + private DB.Repo repo; + + private Map apps; + private List appsList; + + private DB.App curapp = null; + private DB.Apk curapk = null; + private StringBuilder curchars = new StringBuilder(); + + // After processing the XML, these will be -1 if the index didn't specify + // them - otherwise it will be the value specified. + private int version = -1; + private int maxage = -1; + + // After processing the XML, this will be null if the index specified a + // public key - otherwise a public key. This is used for TOFU where an + // index.xml is read on the first connection, and a signed index.jar is + // expected on all subsequent connections. + private String pubkey; + + private String name; + private String description; + private String hashType; + + private int progressCounter = 0; + private ProgressListener progressListener; + + + // The date format used in the repo XML file. + private SimpleDateFormat mXMLDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + private int totalAppCount; + + public RepoXMLHandler(DB.Repo repo, List appsList, ProgressListener listener) { + this.repo = repo; + this.apps = new HashMap(); + for (DB.App app : appsList) this.apps.put(app.id, app); + this.appsList = appsList; + pubkey = null; + name = null; + description = null; + progressListener = listener; + } + + public int getMaxAge() { return maxage; } + + public int getVersion() { return version; } + + public String getDescription() { return description; } + + public String getName() { return name; } + + public String getPubKey() { + return pubkey; + } + + @Override + public void characters(char[] ch, int start, int length) { + curchars.append(ch, start, length); + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + + super.endElement(uri, localName, qName); + String curel = localName; + String str = curchars.toString(); + if (str != null) { + str = str.trim(); + } + + if (curel.equals("application") && curapp != null) { + + // If we already have this application (must be from scanning a + // different repo) then just merge in the apks. + DB.App app = apps.get(curapp.id); + if (app != null) { + app.apks.addAll(curapp.apks); + } else { + appsList.add(curapp); + apps.put(curapp.id, curapp); + } + + curapp = null; + + } else if (curel.equals("package") && curapk != null && curapp != null) { + curapp.apks.add(curapk); + curapk = null; + } else if (curapk != null && str != null) { + if (curel.equals("version")) { + curapk.version = str; + } else if (curel.equals("versioncode")) { + try { + curapk.vercode = Integer.parseInt(str); + } catch (NumberFormatException ex) { + curapk.vercode = -1; + } + } else if (curel.equals("size")) { + try { + curapk.detail_size = Integer.parseInt(str); + } catch (NumberFormatException ex) { + curapk.detail_size = 0; + } + } else if (curel.equals("hash")) { + if (hashType == null || hashType.equals("md5")) { + if (curapk.detail_hash == null) { + curapk.detail_hash = str; + curapk.detail_hashType = "MD5"; + } + } else if (hashType.equals("sha256")) { + curapk.detail_hash = str; + curapk.detail_hashType = "SHA-256"; + } + } else if (curel.equals("sig")) { + curapk.sig = str; + } else if (curel.equals("srcname")) { + curapk.srcname = str; + } else if (curel.equals("apkname")) { + curapk.apkName = str; + } else if (curel.equals("sdkver")) { + try { + curapk.minSdkVersion = Integer.parseInt(str); + } catch (NumberFormatException ex) { + curapk.minSdkVersion = 0; + } + } else if (curel.equals("added")) { + try { + curapk.added = str.length() == 0 ? null : mXMLDateFormat + .parse(str); + } catch (ParseException e) { + curapk.added = null; + } + } else if (curel.equals("permissions")) { + curapk.detail_permissions = DB.CommaSeparatedList.make(str); + } else if (curel.equals("features")) { + curapk.features = DB.CommaSeparatedList.make(str); + } else if (curel.equals("nativecode")) { + curapk.nativecode = DB.CommaSeparatedList.make(str); + } + } else if (curapp != null && str != null) { + if (curel.equals("name")) { + curapp.name = str; + } else if (curel.equals("icon")) { + curapp.icon = str; + } else if (curel.equals("description")) { + // This is the old-style description. We'll read it + // if present, to support old repos, but in newer + // repos it will get overwritten straight away! + curapp.detail_description = "

" + str + "

"; + } else if (curel.equals("desc")) { + // New-style description. + curapp.detail_description = str; + } else if (curel.equals("summary")) { + curapp.summary = str; + } else if (curel.equals("license")) { + curapp.license = str; + } else if (curel.equals("source")) { + curapp.detail_sourceURL = str; + } else if (curel.equals("donate")) { + curapp.detail_donateURL = str; + } else if (curel.equals("bitcoin")) { + curapp.detail_bitcoinAddr = str; + } else if (curel.equals("litecoin")) { + curapp.detail_litecoinAddr = str; + } else if (curel.equals("dogecoin")) { + curapp.detail_dogecoinAddr = str; + } else if (curel.equals("flattr")) { + curapp.detail_flattrID = str; + } else if (curel.equals("web")) { + curapp.detail_webURL = str; + } else if (curel.equals("tracker")) { + curapp.detail_trackerURL = str; + } else if (curel.equals("added")) { + try { + curapp.added = str.length() == 0 ? null : mXMLDateFormat + .parse(str); + } catch (ParseException e) { + curapp.added = null; + } + } else if (curel.equals("lastupdated")) { + try { + curapp.lastUpdated = str.length() == 0 ? null + : mXMLDateFormat.parse(str); + } catch (ParseException e) { + curapp.lastUpdated = null; + } + } else if (curel.equals("marketversion")) { + curapp.curVersion = str; + } else if (curel.equals("marketvercode")) { + try { + curapp.curVercode = Integer.parseInt(str); + } catch (NumberFormatException ex) { + curapp.curVercode = -1; + } + } else if (curel.equals("categories")) { + curapp.categories = DB.CommaSeparatedList.make(str); + } else if (curel.equals("antifeatures")) { + curapp.antiFeatures = DB.CommaSeparatedList.make(str); + } else if (curel.equals("requirements")) { + curapp.requirements = DB.CommaSeparatedList.make(str); + } + } else if (curel.equals("description")) { + description = str; + } + } + + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + super.startElement(uri, localName, qName, attributes); + + if (localName.equals("repo")) { + String pk = attributes.getValue("", "pubkey"); + if (pk != null) + pubkey = pk; + + String maxAgeAttr = attributes.getValue("", "maxage"); + if (maxAgeAttr != null) { + try { + maxage = Integer.parseInt(maxAgeAttr); + } catch (NumberFormatException nfe) {} + } + + String versionAttr = attributes.getValue("", "version"); + if (versionAttr != null) { + try { + version = Integer.parseInt(versionAttr); + } catch (NumberFormatException nfe) {} + } + + String nm = attributes.getValue("", "name"); + if (nm != null) + name = nm; + String dc = attributes.getValue("", "description"); + if (dc != null) + description = dc; + + } else if (localName.equals("application") && curapp == null) { + curapp = new DB.App(); + curapp.detail_Populated = true; + curapp.id = attributes.getValue("", "id"); + Bundle progressData = RepoUpdater.createProgressData(repo.address); + progressCounter ++; + progressListener.onProgress( + new ProgressListener.Event( + RepoUpdater.PROGRESS_TYPE_PROCESS_XML, progressCounter, + totalAppCount, progressData)); + + } else if (localName.equals("package") && curapp != null && curapk == null) { + curapk = new DB.Apk(); + curapk.id = curapp.id; + curapk.repo = repo.id; + hashType = null; + + } else if (localName.equals("hash") && curapk != null) { + hashType = attributes.getValue("", "type"); + } + curchars.setLength(0); + } + +<<<<<<< HEAD +======= + // Get a remote file. Returns the HTTP response code. + // If 'etag' is not null, it's passed to the server as an If-None-Match + // header, in which case expect a 304 response if nothing changed. + // In the event of a 200 response ONLY, 'retag' (which should be passed + // empty) may contain an etag value for the response, or it may be left + // empty if none was available. + private static int getRemoteFile(Context ctx, String url, String dest, + String etag, StringBuilder retag, + ProgressListener progressListener, + ProgressListener.Event progressEvent) throws MalformedURLException, + IOException { + + long startTime = System.currentTimeMillis(); + URL u = new URL(url); + HttpURLConnection connection = (HttpURLConnection) u.openConnection(); + if (etag != null) + connection.setRequestProperty("If-None-Match", etag); + int code = connection.getResponseCode(); + if (code == 200) { + // Testing in the emulator for me, showed that figuring out the filesize took about 1 to 1.5 seconds. + // To put this in context, downloading a repo of: + // - 400k takes ~6 seconds + // - 5k takes ~3 seconds + // on my connection. I think the 1/1.5 seconds is worth it, because as the repo grows, the tradeoff will + // become more worth it. + progressEvent.total = connection.getContentLength(); + Log.d("FDroid", "Downloading " + progressEvent.total + " bytes from " + url); + InputStream input = null; + OutputStream output = null; + try { + input = connection.getInputStream(); + output = ctx.openFileOutput(dest, Context.MODE_PRIVATE); + Utils.copy(input, output, progressListener, progressEvent); + } finally { + Utils.closeQuietly(output); + Utils.closeQuietly(input); + } + + String et = connection.getHeaderField("ETag"); + if (et != null) + retag.append(et); + } + Log.d("FDroid", "Fetched " + url + " (" + progressEvent.total + + " bytes) in " + (System.currentTimeMillis() - startTime) + + "ms"); + return code; + + } + + // Do an update from the given repo. All applications found, and their + // APKs, are added to 'apps'. (If 'apps' already contains an app, its + // APKs are merged into the existing one). + // Returns null if successful, otherwise an error message to be displayed + // to the user (if there is an interactive user!) + // 'newetag' should be passed empty. On success, it may contain an etag + // value for the index that was successfully processed, or it may contain + // null if none was available. + public static String doUpdate(Context ctx, DB.Repo repo, + List appsList, StringBuilder newetag, List keeprepos, + ProgressListener progressListener) { + try { + + int code = 0; + if (repo.pubkey != null) { + + // This is a signed repo - we download the jar file, + // check the signature, and extract the index... + Log.d("FDroid", "Getting signed index from " + repo.address + " at " + + logDateFormat.format(new Date(System.currentTimeMillis()))); + String address = repo.address + "/index.jar?client_version=" + + ctx.getString(R.string.version_name); + Bundle progressData = createProgressData(repo.address); + ProgressListener.Event event = new ProgressListener.Event( + RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, progressData); + code = getRemoteFile(ctx, address, "tempindex.jar", + repo.lastetag, newetag, progressListener, event ); + if (code == 200) { + String jarpath = ctx.getFilesDir() + "/tempindex.jar"; + JarFile jar = null; + JarEntry je; + Certificate[] certs; + try { + jar = new JarFile(jarpath, true); + je = (JarEntry) jar.getEntry("index.xml"); + File efile = new File(ctx.getFilesDir(), + "/tempindex.xml"); + InputStream input = null; + OutputStream output = null; + try { + input = jar.getInputStream(je); + output = new FileOutputStream(efile); + Utils.copy(input, output); + } finally { + Utils.closeQuietly(output); + Utils.closeQuietly(input); + } + certs = je.getCertificates(); + } catch (SecurityException e) { + Log.e("FDroid", "Invalid hash for index file"); + return "Invalid hash for index file"; + } finally { + if (jar != null) { + jar.close(); + } + } + if (certs == null) { + Log.d("FDroid", "No signature found in index"); + return "No signature found in index"; + } + Log.d("FDroid", "Index has " + certs.length + " signature" + + (certs.length > 1 ? "s." : ".")); + + boolean match = false; + for (Certificate cert : certs) { + String certdata = Hasher.hex(cert.getEncoded()); + if (repo.pubkey.equals(certdata)) { + match = true; + break; + } + } + if (!match) { + Log.d("FDroid", "Index signature mismatch"); + return "Index signature mismatch"; + } + } + + } else { + + // It's an old-fashioned unsigned repo... + Log.d("FDroid", "Getting unsigned index from " + repo.address); + Bundle eventData = createProgressData(repo.address); + ProgressListener.Event event = new ProgressListener.Event( + RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, eventData); + code = getRemoteFile(ctx, repo.address + "/index.xml", + "tempindex.xml", repo.lastetag, newetag, + progressListener, event); + } + + if (code == 200) { + // Process the index... + SAXParserFactory spf = SAXParserFactory.newInstance(); + SAXParser sp = spf.newSAXParser(); + XMLReader xr = sp.getXMLReader(); + RepoXMLHandler handler = new RepoXMLHandler(repo, appsList, progressListener); + xr.setContentHandler(handler); + + File tempIndex = new File(ctx.getFilesDir() + "/tempindex.xml"); + BufferedReader r = new BufferedReader(new FileReader(tempIndex)); + + // A bit of a hack, this might return false positives if an apps description + // or some other part of the XML file contains this, but it is a pretty good + // estimate and makes the progress counter more informative. + // As with asking the server about the size of the index before downloading, + // this also has a time tradeoff. It takes about three seconds to iterate + // through the file and count 600 apps on a slow emulator (v17), but if it is + // taking two minutes to update, the three second wait may be worth it. + final String APPLICATION = ">>>>>> master + public void setTotalAppCount(int totalAppCount) { + this.totalAppCount = totalAppCount; + } +} diff --git a/src/org/fdroid/fdroid/SearchResults.java b/src/org/fdroid/fdroid/SearchResults.java index 1a3ac216b..0ee545c98 100644 --- a/src/org/fdroid/fdroid/SearchResults.java +++ b/src/org/fdroid/fdroid/SearchResults.java @@ -118,18 +118,15 @@ public class SearchResults extends ListActivity { TextView tv = (TextView) findViewById(R.id.description); String headertext; - try { - if (apps.size() == 0) - headertext = String.format(getString(R.string.searchres_noapps), - mQuery); - else if (apps.size() == 1) - headertext = String.format(getString(R.string.searchres_oneapp), - mQuery); - else - headertext = String.format(getString(R.string.searchres_napps), - apps.size(), mQuery); - } catch(Exception ex) { - headertext = "TRANSLATION ERROR!"; + if (apps.size() == 0) { + headertext = String.format(getString(R.string.searchres_noapps), + mQuery); + } else if (apps.size() == 1) { + headertext = String.format(getString(R.string.searchres_oneapp), + mQuery); + } else { + headertext = String.format(getString(R.string.searchres_napps), + apps.size(), mQuery); } tv.setText(headertext); Log.d("FDroid", "Search for '" + mQuery + "' returned " + apps.size() diff --git a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java index ae303c2a8..0097921d4 100644 --- a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java @@ -83,7 +83,7 @@ public class SignedRepoUpdater extends RepoUpdater { } protected String getIndexAddress() { - return repo.address + "/index.jar?" + context.getString(R.string.version_name); + return repo.address + "/index.jar?client_version=" + context.getString(R.string.version_name); } /**