diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 57f95bb7f..89646a26b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -33,6 +33,12 @@ android:allowBackup="true" android:theme="@style/AppThemeDark" android:supportsRtl="false" > + + + diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index a63b4bb29..c01eccdb5 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -25,6 +25,8 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.xml.sax.XMLReader; import android.app.AlertDialog; @@ -859,20 +861,13 @@ public class AppDetails extends ListActivity { // Install the version of this app denoted by 'app.curApk'. private void install() { - String ra = null; - try { - DB db = DB.getDB(); - DB.Repo repo = db.getRepo(app.curApk.repo); - if (repo != null) - ra = repo.address; - } catch (Exception ex) { - Log.d("FDroid", "Failed to get repo address - " + ex.getMessage()); - } finally { - DB.releaseDB(); - } - if (ra == null) + String [] projection = { RepoProvider.DataColumns.ADDRESS }; + Repo repo = RepoProvider.Helper.findById( + getContentResolver(), app.curApk.repo, projection); + if (repo == null || repo.address == null) { return; - final String repoaddress = ra; + } + final String repoaddress = repo.address; if (!app.curApk.compatible) { AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index 2ba046f4b..73099ee64 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -55,6 +55,7 @@ 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; public class DB { @@ -290,7 +291,7 @@ public class DB { 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 long repo; // ID of the repo it comes from public String detail_hash; public String detail_hashType; public int minSdkVersion; // 0 if unknown @@ -404,111 +405,9 @@ public class DB { } } - // The TABLE_REPO table stores the details of the repositories in use. - public static final String TABLE_REPO = "fdroid_repo"; - - 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) { + public int countAppsForRepo(long id) { String[] selection = { "COUNT(distinct id)" }; - String[] selectionArgs = { Integer.toString(id) }; + String[] selectionArgs = { Long.toString(id) }; Cursor result = db.query( TABLE_APK, selection, "repo = ?", selectionArgs, "repo", null, null); if (result.getCount() > 0) { @@ -554,8 +453,7 @@ public class DB { // The date format used for storing dates (e.g. lastupdated, added) in the // database. - public static SimpleDateFormat dateFormat = new SimpleDateFormat( - "yyyy-MM-dd", Locale.ENGLISH); + public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); private DB(Context ctx) { @@ -659,7 +557,7 @@ public class DB { private static final String[] POPULATE_APK_COLS = new String[] { "hash", "hashType", "size", "permissions" }; - private void populateApkDetails(Apk apk, int repo) { + private void populateApkDetails(Apk apk, long repo) { if (repo == 0 || repo == apk.repo) { Cursor cursor = null; try { @@ -692,7 +590,7 @@ public class DB { // 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) { + public void populateDetails(App app, long apkRepo) { if (!app.detail_Populated) { populateAppDetails(app); } @@ -747,10 +645,10 @@ public class DB { app.curVercode = c.getInt(9); String sAdded = c.getString(10); app.added = (sAdded == null || sAdded.length() == 0) ? null - : dateFormat.parse(sAdded); + : DATE_FORMAT.parse(sAdded); String sLastUpdated = c.getString(11); app.lastUpdated = (sLastUpdated == null || sLastUpdated - .length() == 0) ? null : dateFormat + .length() == 0) ? null : DATE_FORMAT .parse(sLastUpdated); app.compatible = c.getInt(12) == 1; app.ignoreAllUpdates = c.getInt(13) == 1; @@ -783,12 +681,16 @@ public class DB { Log.d("FDroid", "Read app data from database " + " (took " + (System.currentTimeMillis() - startTime) + " ms)"); - List repos = getRepos(); - 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"); + 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() @@ -829,21 +731,19 @@ public class DB { apk.minSdkVersion = c.getInt(6); String sApkAdded = c.getString(7); apk.added = (sApkAdded == null || sApkAdded.length() == 0) ? null - : dateFormat.parse(sApkAdded); + : 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) { - for (DB.Repo repo : repos) { - if (repo.id != repoid) continue; - if (repo.version >= 11) { - app.iconUrl = repo.address + iconsDir + app.icon; - } else { - app.iconUrl = repo.address + "/icons/" + app.icon; - } - break; + int repoVersion = c.getInt(12); + String repoAddress = c.getString(13); + if (repoVersion >= 11) { + app.iconUrl = repoAddress + iconsDir + app.icon; + } else { + app.iconUrl = repoAddress + "/icons/" + app.icon; } } c.moveToNext(); @@ -1153,10 +1053,10 @@ public class DB { values.put("dogecoinAddr", upapp.detail_dogecoinAddr); values.put("flattrID", upapp.detail_flattrID); values.put("added", - upapp.added == null ? "" : dateFormat.format(upapp.added)); + upapp.added == null ? "" : DATE_FORMAT.format(upapp.added)); values.put( "lastUpdated", - upapp.added == null ? "" : dateFormat + upapp.added == null ? "" : DATE_FORMAT .format(upapp.lastUpdated)); values.put("curVersion", upapp.curVersion); values.put("curVercode", upapp.curVercode); @@ -1201,7 +1101,7 @@ public class DB { values.put("apkName", upapk.apkName); values.put("minSdkVersion", upapk.minSdkVersion); values.put("added", - upapk.added == null ? "" : dateFormat.format(upapk.added)); + upapk.added == null ? "" : DATE_FORMAT.format(upapk.added)); values.put("permissions", CommaSeparatedList.str(upapk.detail_permissions)); values.put("features", CommaSeparatedList.str(upapk.features)); @@ -1216,105 +1116,6 @@ public class DB { } } - // 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 ? - dateFormat.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') @@ -1322,120 +1123,11 @@ public class DB { + " 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", dateFormat.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", dateFormat.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(Locale.ENGLISH); - 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; + public void purgeApps(Repo repo, FDroidApp fdroid) { 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 }); - } - } + db.delete(TABLE_APK, "repo = ?", new String[] { Long.toString(repo.getId()) }); List apps = getApps(false); for (App app : apps) { if (app.apks.isEmpty()) { @@ -1446,6 +1138,8 @@ public class DB { } finally { db.endTransaction(); } + + fdroid.invalidateAllApps(); } public int getSynchronizationMode() { diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java index 50191ff71..765999a76 100644 --- a/src/org/fdroid/fdroid/ManageRepo.java +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -21,34 +21,39 @@ package org.fdroid.fdroid; import android.app.Activity; import android.app.AlertDialog; -import android.app.ListActivity; +import android.content.ContentValues; import android.content.DialogInterface; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.ListFragment; +import android.support.v4.content.CursorLoader; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.support.v4.app.LoaderManager; import android.support.v4.app.NavUtils; +import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; import android.util.Log; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.*; -import org.fdroid.fdroid.DB.Repo; + import org.fdroid.fdroid.compat.ActionBarCompat; import org.fdroid.fdroid.compat.ClipboardCompat; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.views.RepoAdapter; import org.fdroid.fdroid.views.RepoDetailsActivity; import org.fdroid.fdroid.views.fragments.RepoDetailsFragment; import java.net.MalformedURLException; import java.net.URL; -import java.util.*; +import java.util.Locale; -public class ManageRepo extends ListActivity { - - private static final String DEFAULT_NEW_REPO_TEXT = "https://"; - private final int ADD_REPO = 1; - private final int UPDATE_REPOS = 2; +public class ManageRepo extends FragmentActivity { /** * If we have a new repo added, or the address of a repo has changed, then @@ -58,6 +63,122 @@ public class ManageRepo extends ListActivity { */ public static final String REQUEST_UPDATE = "update"; + private RepoListFragment listFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ((FDroidApp) getApplication()).applyTheme(this); + + if (savedInstanceState == null) { + listFragment = new RepoListFragment(); + getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, listFragment) + .commit(); + } + + ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); + } + + public void finish() { + Intent ret = new Intent(); + if (listFragment.hasChanged()) { + Log.i("FDroid", "Repo details have changed, prompting for update."); + ret.putExtra(REQUEST_UPDATE, true); + } + setResult(Activity.RESULT_OK, ret); + super.finish(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } + +} + +class RepoListFragment extends ListFragment + implements LoaderManager.LoaderCallbacks,RepoAdapter.EnabledListener { + + private static final String DEFAULT_NEW_REPO_TEXT = "https://"; + private final int ADD_REPO = 1; + private final int UPDATE_REPOS = 2; + + public boolean hasChanged() { + return changed; + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + Uri uri = RepoProvider.getContentUri(); + Log.i("FDroid", "Creating repo loader '" + uri + "'."); + String[] projection = new String[] { + RepoProvider.DataColumns._ID, + RepoProvider.DataColumns.NAME, + RepoProvider.DataColumns.PUBLIC_KEY, + RepoProvider.DataColumns.FINGERPRINT, + RepoProvider.DataColumns.IN_USE + }; + return new CursorLoader(getActivity(), uri, projection, null, null, null); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + Log.i("FDroid", "Repo cursor loaded."); + repoAdapter.swapCursor(cursor); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + Log.i("FDroid", "Repo cursor reset."); + repoAdapter.swapCursor(null); + } + + + /** + * NOTE: If somebody toggles a repo off then on again, it will have removed + * all apps from the index when it was toggled off, so when it is toggled on + * again, then it will require a refresh. + * + * Previously, I toyed with the idea of remembering whether they had + * toggled on or off, and then only actually performing the function when + * the activity stopped, but I think that will be problematic. What about + * when they press the home button, or edit a repos details? It will start + * to become somewhat-random as to when the actual enabling, disabling is + * performed. + * + * So now, it just does the disable as soon as the user clicks "Off" and + * then removes the apps. To compensate for the removal of apps from + * index, it notifies the user via a toast that the apps have been removed. + * Also, as before, it will still prompt the user to update the repos if + * you toggled on on. + */ + @Override + public void onSetEnabled(Repo repo, boolean isEnabled) { + if (repo.inuse != isEnabled ) { + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, isEnabled ? 1 : 0); + RepoProvider.Helper.update( + getActivity().getContentResolver(), repo, values); + + if (isEnabled) { + changed = true; + } else { + FDroidApp app = (FDroidApp)getActivity().getApplication(); + RepoProvider.Helper.purgeApps(repo, app); + String notification = getString(R.string.repo_disabled_notification, repo.name); + Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show(); + } + } + } + private enum PositiveAction { ADD_NEW, ENABLE, IGNORE } @@ -74,16 +195,14 @@ public class ManageRepo extends ListActivity { private boolean isImportingRepo = false; @Override - protected void onCreate(Bundle savedInstanceState) { - - ((FDroidApp) getApplication()).applyTheme(this); + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ActionBarCompat abCompat = ActionBarCompat.create(this); - abCompat.setDisplayHomeAsUpEnabled(true); + setHasOptionsMenu(true); - repoAdapter = new RepoAdapter(this); + repoAdapter = new RepoAdapter(getActivity(), null); + repoAdapter.setEnabledListener(this); setListAdapter(repoAdapter); /* @@ -106,7 +225,7 @@ public class ManageRepo extends ListActivity { */ /* let's see if someone is trying to send us a new repo */ - Intent intent = getIntent(); + Intent intent = getActivity().getIntent(); /* an URL from a click or a QRCode scan */ Uri uri = intent.getData(); if (uri != null) { @@ -139,27 +258,25 @@ public class ManageRepo extends ListActivity { } @Override - protected void onResume() { + public void onResume() { super.onResume(); - refreshList(); + + //Starts a new or restarts an existing Loader in this manager + getLoaderManager().restartLoader(0, null, this); } @Override - protected void onListItemClick(ListView l, View v, int position, long id) { + public void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); - DB.Repo repo = (DB.Repo)getListView().getItemAtPosition(position); + Repo repo = new Repo((Cursor)getListView().getItemAtPosition(position)); editRepo(repo); } - private void refreshList() { - repoAdapter.refresh(); - } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); MenuItem updateItem = menu.add(Menu.NONE, UPDATE_REPOS, 1, R.string.menu_update_repo).setIcon(R.drawable.ic_menu_refresh); @@ -171,102 +288,23 @@ public class ManageRepo extends ListActivity { android.R.drawable.ic_menu_add); MenuItemCompat.setShowAsAction(addItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS | - MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); - - return true; + MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT); } public static final int SHOW_REPO_DETAILS = 1; - public void editRepo(DB.Repo repo) { - Log.d("FDroid", "Showing details screen for repo: '" + repo + "'."); - Intent intent = new Intent(this, RepoDetailsActivity.class); - intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.id); + public void editRepo(Repo repo) { + Intent intent = new Intent(getActivity(), RepoDetailsActivity.class); + intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.getId()); startActivityForResult(intent, SHOW_REPO_DETAILS); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - - if (requestCode == SHOW_REPO_DETAILS && resultCode == RESULT_OK) { - - boolean wasDeleted = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_DELETED, false); - boolean wasEnabled = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_ENABLED, false); - boolean wasDisabled = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_DISABLED, false); - boolean wasChanged = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_CHANGED, false); - - if (wasDeleted) { - int repoId = data.getIntExtra(RepoDetailsActivity.DATA_REPO_ID, 0); - remove(repoId); - } else if (wasEnabled || wasDisabled || wasChanged) { - changed = true; - } - } - } - - private DB.Repo getRepoById(int repoId) { - for (int i = 0; i < getListAdapter().getCount(); i ++) { - DB.Repo repo = (DB.Repo)getListAdapter().getItem(i); - if (repo.id == repoId) { - return repo; - } - } - return null; - } - - private void remove(int repoId) { - DB.Repo repo = getRepoById(repoId); - if (repo == null) { - return; - } - - List reposToRemove = new ArrayList(1); - reposToRemove.add(repo); - try { - DB db = DB.getDB(); - db.doDisableRepos(reposToRemove, true); - } finally { - DB.releaseDB(); - } - refreshList(); - } - - protected List getRepos() { - List repos = null; - try { - DB db = DB.getDB(); - repos = db.getRepos(); - } finally { - DB.releaseDB(); - } - return repos; - } - - protected Repo getRepoByAddress(String address, List repos) { - if (address != null) - for (Repo repo : repos) - if (address.equals(repo.address)) - return repo; - return null; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - private void updateRepos() { - UpdateService.updateNow(this).setListener(new ProgressListener() { + UpdateService.updateNow(getActivity()).setListener(new ProgressListener() { @Override public void onProgress(Event event) { // No need to prompt to update any more, we just did it! changed = false; - refreshList(); } }); } @@ -276,14 +314,14 @@ public class ManageRepo extends ListActivity { } private void showAddRepo(String newAddress, String newFingerprint) { - - View view = getLayoutInflater().inflate(R.layout.addrepo, null); - final AlertDialog alrt = new AlertDialog.Builder(this).setView(view).create(); + View view = getLayoutInflater(null).inflate(R.layout.addrepo, null); + final AlertDialog alrt = new AlertDialog.Builder(getActivity()).setView(view).create(); final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri); final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint); - List repos = getRepos(); - final Repo repo = newAddress != null && isImportingRepo ? getRepoByAddress(newAddress, repos) : null; + final Repo repo = ( newAddress != null && isImportingRepo ) + ? RepoProvider.Helper.findByAddress(getActivity().getContentResolver(), newAddress) + : null; alrt.setIcon(android.R.drawable.ic_menu_add); alrt.setTitle(getString(R.string.repo_add_title)); @@ -312,8 +350,8 @@ public class ManageRepo extends ListActivity { new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - setResult(Activity.RESULT_CANCELED); - finish(); + getActivity().setResult(Activity.RESULT_CANCELED); + getActivity().finish(); return; } }); @@ -338,7 +376,7 @@ public class ManageRepo extends ListActivity { // this entry already exists and is not enabled, offer to enable it if (repo.inuse) { alrt.dismiss(); - Toast.makeText(this, R.string.repo_exists_and_enabled, Toast.LENGTH_LONG).show(); + Toast.makeText(getActivity(), R.string.repo_exists_and_enabled, Toast.LENGTH_LONG).show(); return; } else { overwriteMessage.setText(R.string.repo_exists_enable); @@ -373,12 +411,10 @@ public class ManageRepo extends ListActivity { * Adds a new repo to the database. */ private void createNewRepo(String address, String fingerprint) { - try { - DB db = DB.getDB(); - db.addRepo(address, null, null, 0, 10, null, fingerprint, 0, true); - } finally { - DB.releaseDB(); - } + ContentValues values = new ContentValues(2); + values.put(RepoProvider.DataColumns.ADDRESS, address); + values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint); + RepoProvider.Helper.insert(getActivity().getContentResolver(), values); finishedAddingRepo(); } @@ -386,13 +422,10 @@ public class ManageRepo extends ListActivity { * Seeing as this repo already exists, we will force it to be enabled again. */ private void createNewRepo(Repo repo) { + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, 1); + RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); repo.inuse = true; - try { - DB db = DB.getDB(); - db.updateRepoByAddress(repo); - } finally { - DB.releaseDB(); - } finishedAddingRepo(); } @@ -404,17 +437,13 @@ public class ManageRepo extends ListActivity { private void finishedAddingRepo() { changed = true; if (isImportingRepo) { - setResult(Activity.RESULT_OK); - finish(); - } else { - refreshList(); + getActivity().setResult(Activity.RESULT_OK); + getActivity().finish(); } } @Override - public boolean onMenuItemSelected(int featureId, MenuItem item) { - - super.onMenuItemSelected(featureId, item); + public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == ADD_REPO) { showAddRepo(); @@ -424,7 +453,7 @@ public class ManageRepo extends ListActivity { return true; } - return false; + return super.onOptionsItemSelected(item); } /** @@ -432,7 +461,7 @@ public class ManageRepo extends ListActivity { * Otherwise return "https://". */ private String getNewRepoUri() { - ClipboardCompat clipboard = ClipboardCompat.create(this); + ClipboardCompat clipboard = ClipboardCompat.create(getActivity()); String text = clipboard.getText(); if (text != null) { try { @@ -447,46 +476,4 @@ public class ManageRepo extends ListActivity { } return text; } - - @Override - public void finish() { - Intent ret = new Intent(); - if (changed) { - Log.i("FDroid", "Repo details have changed, prompting for update."); - ret.putExtra(REQUEST_UPDATE, true); - } - setResult(RESULT_OK, ret); - super.finish(); - } - - /** - * NOTE: If somebody toggles a repo off then on again, it will have removed - * all apps from the index when it was toggled off, so when it is toggled on - * again, then it will require a refresh. - * - * Previously, I toyed with the idea of remembering whether they had - * toggled on or off, and then only actually performing the function when - * the activity stopped, but I think that will be problematic. What about - * when they press the home button, or edit a repos details? It will start - * to become somewhat-random as to when the actual enabling, disabling is - * performed. - * - * So now, it just does the disable as soon as the user clicks "Off" and - * then removes the apps. To compensate for the removal of apps from - * index, it notifies the user via a toast that the apps have been removed. - * Also, as before, it will still prompt the user to update the repos if - * you toggled on on. - */ - public void setRepoEnabled(DB.Repo repo, boolean enabled) { - FDroidApp app = (FDroidApp)getApplication(); - if (enabled) { - repo.enable(app); - changed = true; - } else { - repo.disable(app); - String notification = getString(R.string.repo_disabled_notification, repo.toString()); - Toast.makeText(this, notification, Toast.LENGTH_LONG).show(); - } - } - } diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index a41c867d1..59a109b7a 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.Repo; import org.fdroid.fdroid.updater.RepoUpdater; import org.xml.sax.Attributes; import org.xml.sax.SAXException; @@ -33,7 +34,7 @@ import java.util.Map; public class RepoXMLHandler extends DefaultHandler { // The repo we're processing. - private DB.Repo repo; + private Repo repo; private Map apps; private List appsList; @@ -62,7 +63,7 @@ public class RepoXMLHandler extends DefaultHandler { private int totalAppCount; - public RepoXMLHandler(DB.Repo repo, List appsList, ProgressListener listener) { + public RepoXMLHandler(Repo repo, List appsList, ProgressListener listener) { this.repo = repo; this.apps = new HashMap(); for (DB.App app : appsList) this.apps.put(app.id, app); @@ -157,7 +158,7 @@ public class RepoXMLHandler extends DefaultHandler { } } else if (curel.equals("added")) { try { - curapk.added = str.length() == 0 ? null : DB.dateFormat + curapk.added = str.length() == 0 ? null : DB.DATE_FORMAT .parse(str); } catch (ParseException e) { curapk.added = null; @@ -204,7 +205,7 @@ public class RepoXMLHandler extends DefaultHandler { curapp.detail_trackerURL = str; } else if (curel.equals("added")) { try { - curapp.added = str.length() == 0 ? null : DB.dateFormat + curapp.added = str.length() == 0 ? null : DB.DATE_FORMAT .parse(str); } catch (ParseException e) { curapp.added = null; @@ -212,7 +213,7 @@ public class RepoXMLHandler extends DefaultHandler { } else if (curel.equals("lastupdated")) { try { curapp.lastUpdated = str.length() == 0 ? null - : DB.dateFormat.parse(str); + : DB.DATE_FORMAT.parse(str); } catch (ParseException e) { curapp.lastUpdated = null; } @@ -281,7 +282,7 @@ public class RepoXMLHandler extends DefaultHandler { } else if (localName.equals("package") && curapp != null && curapk == null) { curapk = new DB.Apk(); curapk.id = curapp.id; - curapk.repo = repo.id; + curapk.repo = repo.getId(); hashType = null; } else if (localName.equals("hash") && curapk != null) { diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java index f2c13b760..d161cc5ae 100644 --- a/src/org/fdroid/fdroid/UpdateService.java +++ b/src/org/fdroid/fdroid/UpdateService.java @@ -41,12 +41,16 @@ import android.os.Parcelable; import android.os.ResultReceiver; import android.os.SystemClock; import android.preference.PreferenceManager; +import android.util.Log; + import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.updater.RepoUpdater; public class UpdateService extends IntentService implements ProgressListener { @@ -280,22 +284,23 @@ public class UpdateService extends IntentService implements ProgressListener { // Grab some preliminary information, then we can release the // database while we do all the downloading, etc... int updates = 0; - List repos; + List repos; List apps; try { DB db = DB.getDB(); - repos = db.getRepos(); apps = db.getApps(false); } finally { DB.releaseDB(); } + repos = RepoProvider.Helper.all(getContentResolver()); + // Process each repo... List updatingApps = new ArrayList(); - Set keeprepos = new TreeSet(); + Set keeprepos = new TreeSet(); boolean changes = false; boolean update; - for (DB.Repo repo : repos) { + for (Repo repo : repos) { if (!repo.inuse) continue; // are we updating all repos, or just one? @@ -306,7 +311,7 @@ public class UpdateService extends IntentService implements ProgressListener { if (address.equals(repo.address)) { update = true; } else { - keeprepos.add(repo.id); + keeprepos.add(repo.getId()); update = false; } } @@ -321,7 +326,7 @@ public class UpdateService extends IntentService implements ProgressListener { updatingApps.addAll(updater.getApps()); changes = true; } else { - keeprepos.add(repo.id); + keeprepos.add(repo.getId()); } } catch (RepoUpdater.UpdateException e) { errmsg += (errmsg.length() == 0 ? "" : "\n") + e.getMessage(); @@ -343,7 +348,7 @@ public class UpdateService extends IntentService implements ProgressListener { // Need to flag things we're keeping despite having received // no data about during the update. (i.e. stuff from a repo // that we know is unchanged due to the etag) - for (int keep : keeprepos) { + for (long keep : keeprepos) { for (DB.App app : apps) { boolean keepapp = false; for (DB.Apk apk : app.apks) { @@ -378,8 +383,6 @@ public class UpdateService extends IntentService implements ProgressListener { db.updateApplication(app); } db.endUpdate(); - for (DB.Repo repo : repos) - db.writeLastEtag(repo); } catch (Exception ex) { db.cancelUpdate(); Log.e("FDroid", "Exception during update processing:\n" diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index 37d4920fa..c6b397df2 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -36,6 +36,7 @@ import java.util.Locale; import android.content.Context; import com.nostra13.universalimageloader.utils.StorageUtils; +import org.fdroid.fdroid.data.Repo; public final class Utils { @@ -159,7 +160,7 @@ public final class Utils { return count; } - public static String formatFingerprint(DB.Repo repo) { + public static String formatFingerprint(Repo repo) { return formatFingerprint(repo.pubkey); } diff --git a/src/org/fdroid/fdroid/compat/SwitchCompat.java b/src/org/fdroid/fdroid/compat/SwitchCompat.java index e683fb625..b5cbb7815 100644 --- a/src/org/fdroid/fdroid/compat/SwitchCompat.java +++ b/src/org/fdroid/fdroid/compat/SwitchCompat.java @@ -1,26 +1,27 @@ package org.fdroid.fdroid.compat; import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; import android.widget.CompoundButton; import android.widget.Switch; import android.widget.ToggleButton; -import org.fdroid.fdroid.ManageRepo; public abstract class SwitchCompat extends Compatibility { - protected final ManageRepo activity; + protected final Context context; - protected SwitchCompat(ManageRepo activity) { - this.activity = activity; + protected SwitchCompat(Context context) { + this.context = context; } public abstract CompoundButton createSwitch(); - public static SwitchCompat create(ManageRepo activity) { + public static SwitchCompat create(Context context) { if (hasApi(14)) { - return new IceCreamSwitch(activity); + return new IceCreamSwitch(context); } else { - return new OldSwitch(activity); + return new OldSwitch(context); } } @@ -29,24 +30,24 @@ public abstract class SwitchCompat extends Compatibility { @TargetApi(14) class IceCreamSwitch extends SwitchCompat { - protected IceCreamSwitch(ManageRepo activity) { - super(activity); + protected IceCreamSwitch(Context context) { + super(context); } @Override public CompoundButton createSwitch() { - return new Switch(activity); + return new Switch(context); } } class OldSwitch extends SwitchCompat { - protected OldSwitch(ManageRepo activity) { - super(activity); + protected OldSwitch(Context context) { + super(context); } @Override public CompoundButton createSwitch() { - return new ToggleButton(activity); + return new ToggleButton(context); } } diff --git a/src/org/fdroid/fdroid/data/DBHelper.java b/src/org/fdroid/fdroid/data/DBHelper.java index 272a29551..26c1b3582 100644 --- a/src/org/fdroid/fdroid/data/DBHelper.java +++ b/src/org/fdroid/fdroid/data/DBHelper.java @@ -16,11 +16,15 @@ public class DBHelper extends SQLiteOpenHelper { public static final String DATABASE_NAME = "fdroid"; + public static final String TABLE_REPO = "fdroid_repo"; + private static final String CREATE_TABLE_REPO = "create table " - + DB.TABLE_REPO + " (id integer primary key, address text not null, " + + 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," + + "maxage integer not null default 0, " + + "version integer not null default 0, " + "lastetag text, lastUpdated string);"; private static final String CREATE_TABLE_APK = "create table " + DB.TABLE_APK @@ -49,7 +53,7 @@ public class DBHelper extends SQLiteOpenHelper { + "ignoreThisUpdate int not null," + "primary key(id));"; - private static final int DB_VERSION = 35; + private static final int DB_VERSION = 37; private Context context; @@ -58,6 +62,79 @@ public class DBHelper extends SQLiteOpenHelper { this.context = context; } + private void populateRepoNames(SQLiteDatabase db, int oldVersion) { + if (oldVersion < 37) { + String[] columns = { "address", "_id" }; + Cursor cursor = db.query(TABLE_REPO, columns, + "name IS NULL OR name = ''", null, null, null, null); + cursor.moveToFirst(); + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + String address = cursor.getString(0); + long id = cursor.getInt(1); + ContentValues values = new ContentValues(1); + values.put("name", Repo.addressToName(address)); + String[] args = { Long.toString( id ) }; + db.update(TABLE_REPO, values, "_id = ?", args); + cursor.moveToNext(); + } + } + } + } + + private void renameRepoId(SQLiteDatabase db, int oldVersion) { + if (oldVersion < 36) { + + Log.d("FDroid", "Renaming " + TABLE_REPO + ".id to _id"); + + db.beginTransaction(); + + try { + // http://stackoverflow.com/questions/805363/how-do-i-rename-a-column-in-a-sqlite-database-table#805508 + String tempTableName = TABLE_REPO + "__temp__"; + db.execSQL("ALTER TABLE " + TABLE_REPO + " RENAME TO " + tempTableName + ";" ); + + // I realise this is available in the CREATE_TABLE_REPO above, + // however I have a feeling that it will need to be the same as the + // current structure of the table as of DBVersion 36, or else we may + // get into strife. For example, if there was a field that + // got removed, then it will break the "insert select" + // statement. Therefore, I've put a copy of CREATE_TABLE_REPO + // here that is the same as it was at DBVersion 36. + String createTableDdl = "create table " + TABLE_REPO + " (" + + "_id integer not null 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, " + + "lastUpdated string);"; + + db.execSQL(createTableDdl); + + String nonIdFields = "address, name, description, inuse, priority, " + + "pubkey, fingerprint, maxage, version, lastetag, lastUpdated"; + + String insertSql = "INSERT INTO " + TABLE_REPO + + "(_id, " + nonIdFields + " ) " + + "SELECT id, " + nonIdFields + " FROM " + tempTableName + ";"; + + db.execSQL(insertSql); + db.execSQL("DROP TABLE " + tempTableName + ";"); + db.setTransactionSuccessful(); + } catch (Exception e) { + Log.e("FDroid", "Error renaming id to _id: " + e.getMessage()); + } + db.endTransaction(); + } + } + @Override public void onCreate(SQLiteDatabase db) { @@ -80,7 +157,7 @@ public class DBHelper extends SQLiteOpenHelper { values.put("inuse", 1); values.put("priority", 10); values.put("lastetag", (String) null); - db.insert(DB.TABLE_REPO, null, values); + db.insert(TABLE_REPO, null, values); values = new ContentValues(); values.put("address", @@ -97,7 +174,7 @@ public class DBHelper extends SQLiteOpenHelper { values.put("inuse", 0); values.put("priority", 20); values.put("lastetag", (String) null); - db.insert(DB.TABLE_REPO, null, values); + db.insert(TABLE_REPO, null, values); } @Override @@ -118,6 +195,8 @@ public class DBHelper extends SQLiteOpenHelper { addMaxAgeToRepo(db, oldVersion); addVersionToRepo(db, oldVersion); addLastUpdatedToRepo(db, oldVersion); + renameRepoId(db, oldVersion); + populateRepoNames(db, oldVersion); } /** @@ -126,13 +205,13 @@ public class DBHelper extends SQLiteOpenHelper { */ private void migradeRepoTable(SQLiteDatabase db, int oldVersion) { if (oldVersion < 20) { - List oldrepos = new ArrayList(); - Cursor c = db.query(DB.TABLE_REPO, + List oldrepos = new ArrayList(); + Cursor c = db.query(TABLE_REPO, new String[] { "address", "inuse", "pubkey" }, null, null, null, null, null); c.moveToFirst(); while (!c.isAfterLast()) { - DB.Repo repo = new DB.Repo(); + Repo repo = new Repo(); repo.address = c.getString(0); repo.inuse = (c.getInt(1) == 1); repo.pubkey = c.getString(2); @@ -140,16 +219,16 @@ public class DBHelper extends SQLiteOpenHelper { c.moveToNext(); } c.close(); - db.execSQL("drop table " + DB.TABLE_REPO); + db.execSQL("drop table " + TABLE_REPO); db.execSQL(CREATE_TABLE_REPO); - for (DB.Repo repo : oldrepos) { + for (Repo repo : oldrepos) { ContentValues values = new ContentValues(); values.put("address", repo.address); values.put("inuse", repo.inuse); values.put("priority", 10); values.put("pubkey", repo.pubkey); values.put("lastetag", (String) null); - db.insert(DB.TABLE_REPO, null, values); + db.insert(TABLE_REPO, null, values); } } } @@ -160,19 +239,19 @@ public class DBHelper extends SQLiteOpenHelper { */ private void addNameAndDescriptionToRepo(SQLiteDatabase db, int oldVersion) { if (oldVersion < 21) { - if (!columnExists(db, DB.TABLE_REPO, "name")) - db.execSQL("alter table " + DB.TABLE_REPO + " add column name text"); - if (!columnExists(db, DB.TABLE_REPO, "description")) - db.execSQL("alter table " + DB.TABLE_REPO + " add column description text"); + if (!columnExists(db, TABLE_REPO, "name")) + db.execSQL("alter table " + TABLE_REPO + " add column name text"); + if (!columnExists(db, TABLE_REPO, "description")) + db.execSQL("alter table " + TABLE_REPO + " add column description text"); ContentValues values = new ContentValues(); values.put("name", context.getString(R.string.default_repo_name)); values.put("description", context.getString(R.string.default_repo_description)); - db.update(DB.TABLE_REPO, values, "address = ?", new String[]{ + db.update(TABLE_REPO, values, "address = ?", new String[]{ context.getString(R.string.default_repo_address)}); values.clear(); values.put("name", context.getString(R.string.default_repo_name2)); values.put("description", context.getString(R.string.default_repo_description2)); - db.update(DB.TABLE_REPO, values, "address = ?", new String[] { + db.update(TABLE_REPO, values, "address = ?", new String[] { context.getString(R.string.default_repo_address2) }); } @@ -184,44 +263,44 @@ public class DBHelper extends SQLiteOpenHelper { */ private void addFingerprintToRepo(SQLiteDatabase db, int oldVersion) { if (oldVersion < 29) { - if (!columnExists(db, DB.TABLE_REPO, "fingerprint")) - db.execSQL("alter table " + DB.TABLE_REPO + " add column fingerprint text"); - List oldrepos = new ArrayList(); - Cursor c = db.query(DB.TABLE_REPO, + if (!columnExists(db, TABLE_REPO, "fingerprint")) + db.execSQL("alter table " + TABLE_REPO + " add column fingerprint text"); + List oldrepos = new ArrayList(); + Cursor c = db.query(TABLE_REPO, new String[] { "address", "pubkey" }, null, null, null, null, null); c.moveToFirst(); while (!c.isAfterLast()) { - DB.Repo repo = new DB.Repo(); + Repo repo = new Repo(); repo.address = c.getString(0); repo.pubkey = c.getString(1); oldrepos.add(repo); c.moveToNext(); } c.close(); - for (DB.Repo repo : oldrepos) { + for (Repo repo : oldrepos) { ContentValues values = new ContentValues(); values.put("fingerprint", DB.calcFingerprint(repo.pubkey)); - db.update(DB.TABLE_REPO, values, "address = ?", new String[] { repo.address }); + db.update(TABLE_REPO, values, "address = ?", new String[] { repo.address }); } } } private void addMaxAgeToRepo(SQLiteDatabase db, int oldVersion) { if (oldVersion < 30) { - db.execSQL("alter table " + DB.TABLE_REPO + " add column maxage integer not null default 0"); + db.execSQL("alter table " + TABLE_REPO + " add column maxage integer not null default 0"); } } private void addVersionToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion < 33 && !columnExists(db, DB.TABLE_REPO, "version")) { - db.execSQL("alter table " + DB.TABLE_REPO + " add column version integer not null default 0"); + if (oldVersion < 33 && !columnExists(db, TABLE_REPO, "version")) { + db.execSQL("alter table " + TABLE_REPO + " add column version integer not null default 0"); } } private void addLastUpdatedToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion < 35 && !columnExists(db, DB.TABLE_REPO, "lastUpdated")) { - db.execSQL("Alter table " + DB.TABLE_REPO + " add column lastUpdated string"); + if (oldVersion < 35 && !columnExists(db, TABLE_REPO, "lastUpdated")) { + db.execSQL("Alter table " + TABLE_REPO + " add column lastUpdated string"); } } @@ -230,7 +309,7 @@ public class DBHelper extends SQLiteOpenHelper { .putBoolean("triedEmptyUpdate", false).commit(); db.execSQL("drop table " + DB.TABLE_APP); db.execSQL("drop table " + DB.TABLE_APK); - db.execSQL("update " + DB.TABLE_REPO + " set lastetag = NULL"); + db.execSQL("update " + TABLE_REPO + " set lastetag = NULL"); createAppApk(db); } diff --git a/src/org/fdroid/fdroid/data/FDroidProvider.java b/src/org/fdroid/fdroid/data/FDroidProvider.java new file mode 100644 index 000000000..cd8308bd2 --- /dev/null +++ b/src/org/fdroid/fdroid/data/FDroidProvider.java @@ -0,0 +1,59 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentProvider; +import android.content.UriMatcher; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +abstract class FDroidProvider extends ContentProvider { + + public static final String AUTHORITY = "org.fdroid.fdroid.data"; + + protected static final int CODE_LIST = 1; + protected static final int CODE_SINGLE = 2; + + private DBHelper dbHelper; + + abstract protected String getTableName(); + + abstract protected String getProviderName(); + + @Override + public boolean onCreate() { + dbHelper = new DBHelper(getContext()); + return true; + } + + protected final DBHelper db() { + return dbHelper; + } + + protected final SQLiteDatabase read() { + return db().getReadableDatabase(); + } + + protected final SQLiteDatabase write() { + return db().getWritableDatabase(); + } + + @Override + public String getType(Uri uri) { + String type; + switch(getMatcher().match(uri)) { + case CODE_LIST: + type = "dir"; + break; + + case CODE_SINGLE: + default: + type = "item"; + break; + + } + return "vnd.android.cursor." + type + "/vnd." + AUTHORITY + "." + getProviderName(); + } + + abstract protected UriMatcher getMatcher(); + +} + diff --git a/src/org/fdroid/fdroid/data/Repo.java b/src/org/fdroid/fdroid/data/Repo.java new file mode 100644 index 000000000..ca01bde09 --- /dev/null +++ b/src/org/fdroid/fdroid/data/Repo.java @@ -0,0 +1,176 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentValues; +import android.database.Cursor; +import android.util.Log; +import org.fdroid.fdroid.DB; + +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.util.Date; + +public class Repo { + + private long 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; + + public Repo() { + + } + + public Repo(Cursor cursor) { + for(int i = 0; i < cursor.getColumnCount(); i ++ ) { + String column = cursor.getColumnName(i); + if (column.equals(RepoProvider.DataColumns._ID)) { + id = cursor.getInt(i); + } else if (column.equals(RepoProvider.DataColumns.LAST_ETAG)) { + lastetag = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.ADDRESS)) { + address = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.DESCRIPTION)) { + description = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.FINGERPRINT)) { + fingerprint = cursor.getString(i); + } 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); + } + } + } else if (column.equals(RepoProvider.DataColumns.MAX_AGE)) { + maxage = cursor.getInt(i); + } else if (column.equals(RepoProvider.DataColumns.VERSION)) { + version = cursor.getInt(i); + } else if (column.equals(RepoProvider.DataColumns.NAME)) { + name = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.PUBLIC_KEY)) { + pubkey = cursor.getString(i); + } else if (column.equals(RepoProvider.DataColumns.PRIORITY)) { + priority = cursor.getInt(i); + } + } + } + + public long getId() { return id; } + + public String getName() { + return name; + } + + public String toString() { + 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; + } + + public boolean hasBeenUpdated() { + return this.lastetag != null; + } + /** + * 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 static String addressToName(String address) { + String tempName; + try { + URL url = new URL(address); + tempName = url.getHost() + url.getPath(); + } catch (MalformedURLException e) { + tempName = address; + } + return tempName; + } + + private static int toInt(Integer value) { + if (value == null) { + return 0; + } else { + return value; + } + } + + public void setValues(ContentValues values) { + + if (values.containsKey(RepoProvider.DataColumns._ID)) { + id = toInt(values.getAsInteger(RepoProvider.DataColumns._ID)); + } + + if (values.containsKey(RepoProvider.DataColumns.LAST_ETAG)) { + lastetag = values.getAsString(RepoProvider.DataColumns.LAST_ETAG); + } + + if (values.containsKey(RepoProvider.DataColumns.ADDRESS)) { + address = values.getAsString(RepoProvider.DataColumns.ADDRESS); + } + + if (values.containsKey(RepoProvider.DataColumns.DESCRIPTION)) { + description = values.getAsString(RepoProvider.DataColumns.DESCRIPTION); + } + + if (values.containsKey(RepoProvider.DataColumns.FINGERPRINT)) { + fingerprint = values.getAsString(RepoProvider.DataColumns.FINGERPRINT); + } + + if (values.containsKey(RepoProvider.DataColumns.IN_USE)) { + inuse = toInt(values.getAsInteger(RepoProvider.DataColumns.FINGERPRINT)) == 1; + } + + if (values.containsKey(RepoProvider.DataColumns.LAST_UPDATED)) { + String dateString = values.getAsString(RepoProvider.DataColumns.LAST_UPDATED); + if (dateString != null) { + try { + lastUpdated = DB.DATE_FORMAT.parse(dateString); + } catch (ParseException e) { + Log.e("FDroid", "Error parsing date " + dateString); + } + } + } + + if (values.containsKey(RepoProvider.DataColumns.MAX_AGE)) { + maxage = toInt(values.getAsInteger(RepoProvider.DataColumns.MAX_AGE)); + } + + if (values.containsKey(RepoProvider.DataColumns.VERSION)) { + version = toInt(values.getAsInteger(RepoProvider.DataColumns.VERSION)); + } + + if (values.containsKey(RepoProvider.DataColumns.NAME)) { + name = values.getAsString(RepoProvider.DataColumns.NAME); + } + + if (values.containsKey(RepoProvider.DataColumns.PUBLIC_KEY)) { + pubkey = values.getAsString(RepoProvider.DataColumns.PUBLIC_KEY); + } + + if (values.containsKey(RepoProvider.DataColumns.PRIORITY)) { + priority = toInt(values.getAsInteger(RepoProvider.DataColumns.PRIORITY)); + } + } +} diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java new file mode 100644 index 000000000..8232d7cf9 --- /dev/null +++ b/src/org/fdroid/fdroid/data/RepoProvider.java @@ -0,0 +1,294 @@ +package org.fdroid.fdroid.data; + +import android.content.*; +import android.database.Cursor; +import android.net.Uri; +import android.provider.BaseColumns; +import android.text.TextUtils; +import android.util.Log; +import org.fdroid.fdroid.DB; +import org.fdroid.fdroid.FDroidApp; + +import java.util.ArrayList; +import java.util.List; + +public class RepoProvider extends FDroidProvider { + + public static final class Helper { + + private Helper() {} + + public static Repo findById(ContentResolver resolver, long repoId) { + return findById(resolver, repoId, DataColumns.ALL); + } + + public static Repo findById(ContentResolver resolver, long repoId, + String[] projection) { + Uri uri = RepoProvider.getContentUri(repoId); + Cursor cursor = resolver.query(uri, projection, null, null, null); + Repo repo = null; + if (cursor != null) { + cursor.moveToFirst(); + repo = new Repo(cursor); + } + return repo; + } + + public static Repo findByAddress(ContentResolver resolver, + String address) { + return findByAddress(resolver, address, DataColumns.ALL); + } + + public static Repo findByAddress(ContentResolver resolver, + String address, String[] projection) { + List repos = findBy( + resolver, DataColumns.ADDRESS, address, projection); + return repos.size() > 0 ? repos.get(0) : null; + } + + public static List all(ContentResolver resolver) { + return all(resolver, DataColumns.ALL); + } + + public static List all(ContentResolver resolver, String[] projection) { + Uri uri = RepoProvider.getContentUri(); + Cursor cursor = resolver.query(uri, projection, null, null, null); + return cursorToList(cursor); + } + + private static List findBy(ContentResolver resolver, + String fieldName, + String fieldValue, + String[] projection) { + Uri uri = RepoProvider.getContentUri(); + String[] args = { fieldValue }; + Cursor cursor = resolver.query( + uri, projection, fieldName + " = ?", args, null ); + return cursorToList(cursor); + } + + private static List cursorToList(Cursor cursor) { + List repos = new ArrayList(); + if (cursor != null) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + repos.add(new Repo(cursor)); + cursor.moveToNext(); + } + } + return repos; + } + + public static void update(ContentResolver resolver, Repo repo, + ContentValues values) { + + // Change the name to the new address. Next time we update the repo + // index file, it will populate the name field with the proper + // name, but the best we can do is guess right now. + if (values.containsKey(DataColumns.ADDRESS) && + !values.containsKey(DataColumns.NAME)) { + String name = Repo.addressToName(values.getAsString(DataColumns.ADDRESS)); + values.put(DataColumns.NAME, name); + } + + // Recalculate the fingerprint if we are saving a public key (it is + // probably a new public key. If not, we will get the same + // fingerprint anyhow). + if (values.containsKey(DataColumns.PUBLIC_KEY) && + values.containsKey(DataColumns.FINGERPRINT)) { + + String publicKey = values.getAsString(DataColumns.PUBLIC_KEY); + String fingerprint = values.getAsString(DataColumns.FINGERPRINT); + if (publicKey != null && fingerprint == null) { + values.put(DataColumns.FINGERPRINT, DB.calcFingerprint(publicKey)); + } + } + + if (values.containsKey(DataColumns.IN_USE)) { + Integer inUse = values.getAsInteger(DataColumns.IN_USE); + if (inUse != null && inUse == 0) { + values.put(DataColumns.LAST_ETAG, (String)null); + } + } + + Uri uri = getContentUri(repo.getId()); + String[] args = { Long.toString(repo.getId()) }; + resolver.update(uri, values, DataColumns._ID + " = ?", args ); + repo.setValues(values); + } + + /** + * 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(ContentResolver resolver, + ContentValues values) { + Uri uri = RepoProvider.getContentUri(); + resolver.insert(uri, values); + } + + public static void remove(ContentResolver resolver, long repoId) { + Uri uri = RepoProvider.getContentUri(repoId); + resolver.delete(uri, null, null); + } + + public static void purgeApps(Repo repo, FDroidApp app) { + // TODO: Once we have content providers for apps and apks, use them + // to do this... + DB db = DB.getDB(); + try { + db.purgeApps(repo, app); + } finally { + DB.releaseDB(); + } + } + + } + + public interface DataColumns extends BaseColumns { + public static String ADDRESS = "address"; + public static String NAME = "name"; + public static String DESCRIPTION = "description"; + public static String IN_USE = "inuse"; + public static String PRIORITY = "priority"; + public static String PUBLIC_KEY = "pubkey"; + public static String FINGERPRINT = "fingerprint"; + public static String MAX_AGE = "maxage"; + public static String LAST_ETAG = "lastetag"; + public static String LAST_UPDATED = "lastUpdated"; + public static String VERSION = "version"; + + public static String[] ALL = { + _ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, PUBLIC_KEY, + FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION + }; + } + + private static final String PROVIDER_NAME = "RepoProvider"; + + private static final UriMatcher matcher = new UriMatcher(-1); + + static { + matcher.addURI(AUTHORITY, PROVIDER_NAME, CODE_LIST); + matcher.addURI(AUTHORITY, PROVIDER_NAME + "/#", CODE_SINGLE); + } + + public static Uri getContentUri() { + return Uri.parse("content://" + AUTHORITY + "/" + PROVIDER_NAME); + } + + public static Uri getContentUri(long repoId) { + return ContentUris.withAppendedId(getContentUri(), repoId); + } + + @Override + protected String getTableName() { + return DBHelper.TABLE_REPO; + } + + @Override + protected String getProviderName() { + return "RepoProvider"; + } + + protected UriMatcher getMatcher() { + return matcher; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + + switch (matcher.match(uri)) { + case CODE_LIST: + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = "_ID ASC"; + } + break; + + case CODE_SINGLE: + selection = ( selection == null ? "" : selection ) + + "_ID = " + uri.getLastPathSegment(); + break; + + default: + Log.e("FDroid", "Invalid URI for repo content provider: " + uri); + throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); + } + + Cursor cursor = read().query(getTableName(), projection, selection, + selectionArgs, null, null, sortOrder); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + + if (!values.containsKey(DataColumns.ADDRESS)) { + throw new UnsupportedOperationException("Cannot add repo without an address."); + } + + // The following fields have NOT NULL constraints in the DB, so need + // to be present. + + if (!values.containsKey(DataColumns.IN_USE)) { + values.put(DataColumns.IN_USE, 1); + } + + if (!values.containsKey(DataColumns.PRIORITY)) { + values.put(DataColumns.PRIORITY, 10); + } + + if (!values.containsKey(DataColumns.MAX_AGE)) { + values.put(DataColumns.MAX_AGE, 0); + } + + if (!values.containsKey(DataColumns.VERSION)) { + values.put(DataColumns.VERSION, 0); + } + + if (!values.containsKey(DataColumns.NAME)) { + String address = values.getAsString(DataColumns.ADDRESS); + values.put(DataColumns.NAME, Repo.addressToName(address)); + } + + long id = write().insertOrThrow(getTableName(), null, values); + Log.i("FDroid", "Inserted repo. Notifying provider change: '" + uri + "'."); + getContext().getContentResolver().notifyChange(uri, null); + return getContentUri(id); + } + + @Override + public int delete(Uri uri, String where, String[] whereArgs) { + + switch (matcher.match(uri)) { + case CODE_LIST: + // Don't support deleting of multiple repos. + return 0; + + case CODE_SINGLE: + where = ( where == null ? "" : where ) + + "_ID = " + uri.getLastPathSegment(); + break; + + default: + Log.e("FDroid", "Invalid URI for repo content provider: " + uri); + throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); + } + + int rowsAffected = write().delete(getTableName(), where, whereArgs); + Log.i("FDroid", "Deleted repos. Notifying provider change: '" + uri + "'."); + getContext().getContentResolver().notifyChange(uri, null); + return rowsAffected; + } + + @Override + public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { + int numRows = write().update(getTableName(), values, where, whereArgs); + Log.i("FDroid", "Updated repo. Notifying provider change: '" + uri + "'."); + getContext().getContentResolver().notifyChange(uri, null); + return numRows; + } + +} diff --git a/src/org/fdroid/fdroid/updater/RepoUpdater.java b/src/org/fdroid/fdroid/updater/RepoUpdater.java index 2893495b8..f77bbccbc 100644 --- a/src/org/fdroid/fdroid/updater/RepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/RepoUpdater.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.updater; +import android.content.ContentValues; import android.content.Context; import android.os.Bundle; import android.util.Log; @@ -7,6 +8,8 @@ import org.fdroid.fdroid.DB; import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.RepoXMLHandler; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.net.Downloader; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -18,6 +21,7 @@ import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.*; import java.util.ArrayList; +import java.util.Date; import java.util.List; abstract public class RepoUpdater { @@ -26,7 +30,7 @@ abstract public class RepoUpdater { public static final int PROGRESS_TYPE_PROCESS_XML = 2; public static final String PROGRESS_DATA_REPO = "repo"; - public static RepoUpdater createUpdaterFor(Context ctx, DB.Repo repo) { + public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) { if (repo.pubkey != null) { return new SignedRepoUpdater(ctx, repo); } else { @@ -35,12 +39,12 @@ abstract public class RepoUpdater { } protected final Context context; - protected final DB.Repo repo; + protected final Repo repo; protected final List apps = new ArrayList(); protected boolean hasChanged = false; protected ProgressListener progressListener; - public RepoUpdater(Context ctx, DB.Repo repo) { + public RepoUpdater(Context ctx, Repo repo) { this.context = ctx; this.repo = repo; } @@ -164,8 +168,6 @@ abstract public class RepoUpdater { if (hasChanged) { downloadedFile = downloader.getFile(); - repo.lastetag = downloader.getETag(); - indexFile = getIndexFromFile(downloadedFile); // Process the index... @@ -184,7 +186,7 @@ abstract public class RepoUpdater { new BufferedReader(new FileReader(indexFile))); reader.parse(is); - updateRepo(handler); + updateRepo(handler, downloader.getETag()); } } catch (SAXException e) { throw new UpdateException( @@ -210,9 +212,15 @@ abstract public class RepoUpdater { } } - private void updateRepo(RepoXMLHandler handler) { + private void updateRepo(RepoXMLHandler handler, String etag) { - boolean repoChanged = false; + ContentValues values = new ContentValues(); + + values.put(RepoProvider.DataColumns.LAST_UPDATED, DB.DATE_FORMAT.format(new Date())); + + if (repo.lastetag == null || !repo.lastetag.equals(etag)) { + values.put(RepoProvider.DataColumns.LAST_ETAG, etag); + } // We read an unsigned index, but that indicates that // a signed version is now available... @@ -224,54 +232,42 @@ abstract public class RepoUpdater { // information as the unsigned one does not... Log.d("FDroid", "Public key found - switching to signed repo for future updates"); - repo.pubkey = handler.getPubKey(); - repoChanged = true; + values.put(RepoProvider.DataColumns.PUBLIC_KEY, handler.getPubKey()); } if (handler.getVersion() != -1 && handler.getVersion() != repo.version) { Log.d("FDroid", "Repo specified a new version: from " + repo.version + " to " + handler.getVersion()); - repo.version = handler.getVersion(); - repoChanged = true; + values.put(RepoProvider.DataColumns.VERSION, handler.getVersion()); } if (handler.getMaxAge() != -1 && handler.getMaxAge() != repo.maxage) { Log.d("FDroid", "Repo specified a new maximum age - updated"); - repo.maxage = handler.getMaxAge(); - repoChanged = true; + values.put(RepoProvider.DataColumns.MAX_AGE, handler.getMaxAge()); } if (handler.getDescription() != null && !handler.getDescription().equals(repo.description)) { - repo.description = handler.getDescription(); - repoChanged = true; + values.put(RepoProvider.DataColumns.DESCRIPTION, handler.getDescription()); } if (handler.getName() != null && !handler.getName().equals(repo.name)) { - repo.name = handler.getName(); - repoChanged = true; + values.put(RepoProvider.DataColumns.NAME, handler.getName()); } - if (repoChanged) { - try { - DB db = DB.getDB(); - db.updateRepoByAddress(repo); - } finally { - DB.releaseDB(); - } - } + RepoProvider.Helper.update(context.getContentResolver(), repo, values); } public static class UpdateException extends Exception { - public final DB.Repo repo; + public final Repo repo; - public UpdateException(DB.Repo repo, String message) { + public UpdateException(Repo repo, String message) { super(message); this.repo = repo; } - public UpdateException(DB.Repo repo, String message, Exception cause) { + public UpdateException(Repo repo, String message, Exception cause) { super(message, cause); this.repo = repo; } diff --git a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java index 0097921d4..8f3f75aaa 100644 --- a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java @@ -6,6 +6,7 @@ import org.fdroid.fdroid.DB; import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.net.Downloader; import java.io.*; @@ -16,7 +17,7 @@ import java.util.jar.JarFile; public class SignedRepoUpdater extends RepoUpdater { - public SignedRepoUpdater(Context ctx, DB.Repo repo) { + public SignedRepoUpdater(Context ctx, Repo repo) { super(ctx, repo); } diff --git a/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java index 6fd6d5699..ec9b0712b 100644 --- a/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java @@ -3,13 +3,14 @@ package org.fdroid.fdroid.updater; import android.content.Context; import android.util.Log; import org.fdroid.fdroid.DB; +import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.net.Downloader; import java.io.File; public class UnsignedRepoUpdater extends RepoUpdater { - public UnsignedRepoUpdater(Context ctx, DB.Repo repo) { + public UnsignedRepoUpdater(Context ctx, Repo repo) { super(ctx, repo); } diff --git a/src/org/fdroid/fdroid/views/RepoAdapter.java b/src/org/fdroid/fdroid/views/RepoAdapter.java index 76a5c848e..3fcb3c3aa 100644 --- a/src/org/fdroid/fdroid/views/RepoAdapter.java +++ b/src/org/fdroid/fdroid/views/RepoAdapter.java @@ -1,34 +1,48 @@ package org.fdroid.fdroid.views; +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.*; -import org.fdroid.fdroid.DB; -import org.fdroid.fdroid.ManageRepo; +import android.widget.CompoundButton; +import android.widget.RelativeLayout; +import android.widget.TextView; import org.fdroid.fdroid.R; import org.fdroid.fdroid.compat.SwitchCompat; +import org.fdroid.fdroid.data.Repo; -import java.util.List; +public class RepoAdapter extends CursorAdapter { -public class RepoAdapter extends BaseAdapter { - - private List repositories; - private final ManageRepo activity; - - public RepoAdapter(ManageRepo activity) { - this.activity = activity; - refresh(); + public interface EnabledListener { + public void onSetEnabled(Repo repo, boolean isEnabled); } - public void refresh() { - try { - DB db = DB.getDB(); - repositories = db.getRepos(); - } finally { - DB.releaseDB(); - } - notifyDataSetChanged(); + private static final int SWITCH_ID = 10000; + + private final LayoutInflater inflater; + + private EnabledListener enabledListener; + + public RepoAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + inflater = LayoutInflater.from(context); + } + + public RepoAdapter(Context context, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + inflater = LayoutInflater.from(context); + } + + public RepoAdapter(Context context, Cursor c) { + super(context, c); + inflater = LayoutInflater.from(context); + } + + public void setEnabledListener(EnabledListener listener) { + enabledListener = listener; } public boolean hasStableIds() { @@ -36,54 +50,45 @@ public class RepoAdapter extends BaseAdapter { } @Override - public int getCount() { - return repositories.size(); + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = inflater.inflate(R.layout.repo_item, null); + CompoundButton switchView = addSwitchToView(view, context); + setupView(cursor, view, switchView); + return view; } @Override - public Object getItem(int position) { - return repositories.get(position); + public void bindView(View view, Context context, Cursor cursor) { + CompoundButton switchView = (CompoundButton)view.findViewById(SWITCH_ID); + + // Remove old listener (because we are reusing this view, we don't want + // to invoke the listener for the last repo to use it - particularly + // because we are potentially about to change the checked status + // which would in turn invoke this listener.... + switchView.setOnCheckedChangeListener(null); + setupView(cursor, view, switchView); } - @Override - public long getItemId(int position) { - return getItem(position).hashCode(); - } - private static final int SWITCH_ID = 10000; + private void setupView(Cursor cursor, View view, CompoundButton switchView) { - @Override - public View getView(int position, View view, ViewGroup parent) { + final Repo repo = new Repo(cursor); - final DB.Repo repository = repositories.get(position); - - CompoundButton switchView; - if (view == null) { - view = activity.getLayoutInflater().inflate(R.layout.repo_item,null); - switchView = addSwitchToView(view); - } else { - switchView = (CompoundButton)view.findViewById(SWITCH_ID); - - // Remove old listener (because we are reusing this view, we don't want - // to invoke the listener for the last repo to use it - particularly - // because we are potentially about to change the checked status - // which would in turn invoke this listener.... - switchView.setOnCheckedChangeListener(null); - } - - switchView.setChecked(repository.inuse); + switchView.setChecked(repo.inuse); // Add this listener *after* setting the checked status, so we don't // invoke the listener while setting up the view... switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - activity.setRepoEnabled(repository, isChecked); + if (enabledListener != null) { + enabledListener.onSetEnabled(repo, isChecked); + } } }); TextView nameView = (TextView)view.findViewById(R.id.repo_name); - nameView.setText(repository.getName()); + nameView.setText(repo.getName()); RelativeLayout.LayoutParams nameViewLayout = (RelativeLayout.LayoutParams)nameView.getLayoutParams(); nameViewLayout.addRule(RelativeLayout.LEFT_OF, switchView.getId()); @@ -91,17 +96,15 @@ public class RepoAdapter extends BaseAdapter { // If we set the signed view to GONE instead of INVISIBLE, then the // height of each list item varies. View signedView = view.findViewById(R.id.repo_unsigned); - if (repository.isSigned()) { + if (repo.isSigned()) { signedView.setVisibility(View.INVISIBLE); } else { signedView.setVisibility(View.VISIBLE); } - - return view; } - private CompoundButton addSwitchToView(View parent) { - SwitchCompat switchBuilder = SwitchCompat.create(activity); + private CompoundButton addSwitchToView(View parent, Context context) { + SwitchCompat switchBuilder = SwitchCompat.create(context); CompoundButton switchView = switchBuilder.createSwitch(); switchView.setId(SWITCH_ID); RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams( diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java index edf438d53..b0d4ca81f 100644 --- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -1,32 +1,23 @@ package org.fdroid.fdroid.views; -import android.content.Intent; import android.os.Bundle; import android.support.v4.app.FragmentActivity; -import org.fdroid.fdroid.DB; -import org.fdroid.fdroid.DB.Repo; import org.fdroid.fdroid.compat.ActionBarCompat; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.views.fragments.RepoDetailsFragment; -public class RepoDetailsActivity extends FragmentActivity implements RepoDetailsFragment.OnRepoChangeListener { - - public static final String ACTION_IS_DELETED = "isDeleted"; - public static final String ACTION_IS_ENABLED = "isEnabled"; - public static final String ACTION_IS_DISABLED = "isDisabled"; - public static final String ACTION_IS_CHANGED = "isChanged"; - - public static final String DATA_REPO_ID = "repoId"; - - private int repoId; +public class RepoDetailsActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + long repoId = getIntent().getLongExtra(RepoDetailsFragment.ARG_REPO_ID, 0); + if (savedInstanceState == null) { - RepoDetailsFragment fragment = new RepoDetailsFragment(); - fragment.setRepoChangeListener(this); + RepoDetailsFragment fragment = new RepoDetailsFragment(repoId); fragment.setArguments(getIntent().getExtras()); getSupportFragmentManager() .beginTransaction() @@ -34,48 +25,11 @@ public class RepoDetailsActivity extends FragmentActivity implements RepoDetails .commit(); } - repoId = getIntent().getIntExtra(RepoDetailsFragment.ARG_REPO_ID, -1); + String[] projection = new String[] { RepoProvider.DataColumns.NAME }; + Repo repo = RepoProvider.Helper.findById(getContentResolver(), repoId, projection); - DB db = DB.getDB(); - Repo repo = db.getRepo(repoId); - DB.releaseDB(); - - ActionBarCompat abCompat = ActionBarCompat.create(this); - abCompat.setDisplayHomeAsUpEnabled(true); + ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true); setTitle(repo.getName()); } - private void finishWithAction(String actionName) { - Intent data = new Intent(); - data.putExtra(actionName, true); - data.putExtra(DATA_REPO_ID, repoId); - setResult(RESULT_OK, data); - finish(); - } - - @Override - public void onDeleteRepo(DB.Repo repo) { - finishWithAction(ACTION_IS_DELETED); - } - - @Override - public void onRepoDetailsChanged(DB.Repo repo) { - // Do nothing... - } - - @Override - public void onEnableRepo(DB.Repo repo) { - finishWithAction(ACTION_IS_ENABLED); - } - - @Override - public void onDisableRepo(DB.Repo repo) { - finishWithAction(ACTION_IS_DISABLED); - } - - @Override - public void onUpdatePerformed(DB.Repo repo) { - // do nothing - the actual update is done by the repo fragment... - } - } diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java index 96d2b3d45..76995486a 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java @@ -2,8 +2,12 @@ package org.fdroid.fdroid.views.fragments; import android.app.Activity; import android.app.AlertDialog; +import android.content.ContentValues; import android.content.DialogInterface; +import android.database.ContentObserver; +import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.text.Editable; @@ -12,8 +16,8 @@ import android.util.Log; import android.view.*; import android.widget.*; import org.fdroid.fdroid.*; - -import java.util.List; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; public class RepoDetailsFragment extends Fragment { @@ -49,34 +53,15 @@ public class RepoDetailsFragment extends Fragment { private static final int DELETE = 0; private static final int UPDATE = 1; - public void setRepoChangeListener(OnRepoChangeListener listener) { - repoChangeListener = listener; - } - - private OnRepoChangeListener repoChangeListener; - - public static interface OnRepoChangeListener { - - /** - * This fragment is responsible for getting confirmation from the - * user, so you should presume that the user has already consented - * and confirmed to the deletion. - */ - public void onDeleteRepo(DB.Repo repo); - - public void onRepoDetailsChanged(DB.Repo repo); - - public void onEnableRepo(DB.Repo repo); - - public void onDisableRepo(DB.Repo repo); - - public void onUpdatePerformed(DB.Repo repo); + private final long repoId; + public RepoDetailsFragment(long repoId) { + this.repoId = repoId; } // TODO: Currently initialised in onCreateView. Not sure if that is the // best way to go about this... - private DB.Repo repo; + private Repo repo; public void onAttach(Activity activity) { super.onAttach(activity); @@ -88,20 +73,13 @@ public class RepoDetailsFragment extends Fragment { * have been updated. The safest way to deal with this is to reload the * repo object directly from the database. */ - private void reloadRepoDetails() { - try { - DB db = DB.getDB(); - repo = db.getRepo(repo.id); - } finally { - DB.releaseDB(); - } + private Repo loadRepoDetails() { + return RepoProvider.Helper.findById(getActivity().getContentResolver(), repoId); } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - int repoId = getArguments().getInt(ARG_REPO_ID); - DB db = DB.getDB(); - repo = db.getRepo(repoId); - DB.releaseDB(); + + repo = loadRepoDetails(); if (repo == null) { Log.e("FDroid", "Error showing details for repo '" + repoId + "'"); @@ -186,7 +164,7 @@ public class RepoDetailsFragment extends Fragment { lastUpdated.setText(lastUpdate); } - private void setupDescription(ViewGroup parent, DB.Repo repo) { + private void setupDescription(ViewGroup parent, Repo repo) { TextView descriptionLabel = (TextView)parent.findViewById(R.id.label_description); TextView description = (TextView)parent.findViewById(R.id.text_description); @@ -210,20 +188,20 @@ public class RepoDetailsFragment extends Fragment { * list can be updated. We will perform the update ourselves though. */ private void performUpdate() { - repo.enable((FDroidApp)getActivity().getApplication()); + // Ensure repo is enabled before updating... + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.IN_USE, 1); + RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); + UpdateService.updateRepoNow(repo.address, getActivity()).setListener(new ProgressListener() { @Override public void onProgress(Event event) { - if (event.type == UpdateService.STATUS_COMPLETE_AND_SAME || - event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) { - reloadRepoDetails(); + if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) { + repo = loadRepoDetails(); updateView((ViewGroup)getView()); } } }); - if (repoChangeListener != null) { - repoChangeListener.onUpdatePerformed(repo); - } } /** @@ -238,18 +216,15 @@ public class RepoDetailsFragment extends Fragment { public void afterTextChanged(Editable s) {} @Override + // TODO: This is called each character change, resulting in a DB query. + // Doesn't exactly cause performance problems, + // but seems silly not to go for more of a "focus out" event then + // this "text changed" event. public void onTextChanged(CharSequence s, int start, int before, int count) { if (!repo.address.equals(s.toString())) { - repo.address = s.toString(); - try { - DB db = DB.getDB(); - db.updateRepo(repo); - } finally { - DB.releaseDB(); - } - if (repoChangeListener != null) { - repoChangeListener.onRepoDetailsChanged(repo); - } + ContentValues values = new ContentValues(1); + values.put(RepoProvider.DataColumns.ADDRESS, s.toString()); + RepoProvider.Helper.update(getActivity().getContentResolver(), repo, values); } } } @@ -293,24 +268,23 @@ public class RepoDetailsFragment extends Fragment { .setIcon(android.R.drawable.ic_menu_delete) .setMessage(R.string.repo_confirm_delete_body) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (repoChangeListener != null) { - DB.Repo repo = RepoDetailsFragment.this.repo; - repoChangeListener.onDeleteRepo(repo); - } - } - }).setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - // Do nothing... + Repo repo = RepoDetailsFragment.this.repo; + RepoProvider.Helper.remove(getActivity().getContentResolver(), repo.getId()); + getActivity().finish(); + } + }).setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Do nothing... + } } - } ).show(); } - private void setupRepoFingerprint(ViewGroup parent, DB.Repo repo) { + private void setupRepoFingerprint(ViewGroup parent, Repo repo) { TextView repoFingerprintView = (TextView)parent.findViewById(R.id.text_repo_fingerprint); TextView repoFingerprintDescView = (TextView)parent.findViewById(R.id.text_repo_fingerprint_description);