diff --git a/media/android/NewsBlur/src/com/newsblur/activity/Main.java b/media/android/NewsBlur/src/com/newsblur/activity/Main.java index 5f4b7d1ef..f20260673 100644 --- a/media/android/NewsBlur/src/com/newsblur/activity/Main.java +++ b/media/android/NewsBlur/src/com/newsblur/activity/Main.java @@ -16,6 +16,7 @@ import com.newsblur.fragment.FolderListFragment; import com.newsblur.fragment.LogoutDialogFragment; import com.newsblur.fragment.SyncUpdateFragment; import com.newsblur.service.SyncService; +import com.newsblur.util.PrefsUtils; import com.newsblur.view.StateToggleButton.StateChangedListener; public class Main extends NbFragmentActivity implements StateChangedListener, SyncUpdateFragment.SyncUpdateFragmentInterface { @@ -29,6 +30,9 @@ public class Main extends NbFragmentActivity implements StateChangedListener, Sy @Override public void onCreate(Bundle savedInstanceState) { + + PrefsUtils.checkForUpgrade(this); + requestWindowFeature(Window.FEATURE_PROGRESS); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); super.onCreate(savedInstanceState); diff --git a/media/android/NewsBlur/src/com/newsblur/database/BlurDatabase.java b/media/android/NewsBlur/src/com/newsblur/database/BlurDatabase.java index ea628a007..ed78ffadc 100644 --- a/media/android/NewsBlur/src/com/newsblur/database/BlurDatabase.java +++ b/media/android/NewsBlur/src/com/newsblur/database/BlurDatabase.java @@ -3,6 +3,7 @@ package com.newsblur.database; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.database.Cursor; import android.util.Log; public class BlurDatabase extends SQLiteOpenHelper { diff --git a/media/android/NewsBlur/src/com/newsblur/database/DatabaseConstants.java b/media/android/NewsBlur/src/com/newsblur/database/DatabaseConstants.java index 3c7a8cbf9..38ba1116d 100644 --- a/media/android/NewsBlur/src/com/newsblur/database/DatabaseConstants.java +++ b/media/android/NewsBlur/src/com/newsblur/database/DatabaseConstants.java @@ -136,8 +136,17 @@ public class DatabaseConstants { FOLDER_TABLE + "." + FOLDER_ID, FOLDER_TABLE + "." + FOLDER_NAME, " SUM(" + FEED_POSITIVE_COUNT + ") AS " + SUM_POS, " SUM(" + FEED_NEUTRAL_COUNT + ") AS " + SUM_NEUT, " SUM(" + FEED_NEGATIVE_COUNT + ") AS " + SUM_NEG }; - // this union clause lets folder queries also select the "root" folder that should appear whether or not it has unread stories - private static final String FOLDER_UNION_ROOT = " OR " + DatabaseConstants.FOLDER_TABLE + "." + DatabaseConstants.FOLDER_NAME + "='" + AppConstants.ROOT_FOLDER + "'"; + // this union clause lets folder queries also select the "root" folder that should appear whether or not + // it has unread stories. Note that this goes *before* the normal ALL_FOLDERS select statement. The zero-valued + // pseudo-columns are safe because said columns are ignored for the root folder. + public static final String FOLDER_UNION_ROOT = "SELECT " + + FOLDER_ID + ", " + + FOLDER_NAME + + ", 0 AS " + SUM_POS + + ", 0 AS " + SUM_NEUT + + ", 0 AS " + SUM_NEG + + " FROM " + FOLDER_TABLE + + " WHERE " + FOLDER_NAME + "='" + AppConstants.ROOT_FOLDER + "' UNION "; private static final String FOLDER_INTELLIGENCE_ALL = " HAVING SUM(" + DatabaseConstants.FEED_NEGATIVE_COUNT + " + " + DatabaseConstants.FEED_NEUTRAL_COUNT + " + " + DatabaseConstants.FEED_POSITIVE_COUNT + ") >= 0"; private static final String FOLDER_INTELLIGENCE_SOME = " HAVING SUM(" + DatabaseConstants.FEED_NEUTRAL_COUNT + " + " + DatabaseConstants.FEED_POSITIVE_COUNT + ") > 0"; @@ -186,19 +195,19 @@ public class DatabaseConstants { } /** - * Selection args to filter folders. This always additionally includes the root folder and assumes folders are joined with feed counts. + * Selection args to filter folders. */ public static String getFolderSelectionFromState(int state) { String selection = null; switch (state) { case (AppConstants.STATE_ALL): - selection = FOLDER_INTELLIGENCE_ALL + FOLDER_UNION_ROOT; + selection = FOLDER_INTELLIGENCE_ALL; break; case (AppConstants.STATE_SOME): - selection = FOLDER_INTELLIGENCE_SOME + FOLDER_UNION_ROOT; + selection = FOLDER_INTELLIGENCE_SOME; break; case (AppConstants.STATE_BEST): - selection = FOLDER_INTELLIGENCE_BEST + FOLDER_UNION_ROOT; + selection = FOLDER_INTELLIGENCE_BEST; break; } return selection; diff --git a/media/android/NewsBlur/src/com/newsblur/database/FeedProvider.java b/media/android/NewsBlur/src/com/newsblur/database/FeedProvider.java index bd2ebdf36..78dd33809 100644 --- a/media/android/NewsBlur/src/com/newsblur/database/FeedProvider.java +++ b/media/android/NewsBlur/src/com/newsblur/database/FeedProvider.java @@ -12,10 +12,18 @@ import android.util.Log; import com.newsblur.util.AppConstants; +/** + * A magic subclass of ContentProvider that enhances calls to the DB for presumably more simple caller syntax. + * + * TODO: the fact that most of the app uses this subclass of ContentProvider cast as such may + * deepy confuse future maintainers as to why the methods within magically do far, far more + * than suggested by the normal contract and provided args. When time and resources permit, + * this paradigm could be replaced with a much more straightforward if slightly more verbose + * use of Plain Old Raw Queries. Alternatively, the DB could be renormalized so that it is not + * necessary to use queries of such intense complexity. + */ public class FeedProvider extends ContentProvider { - private static final String TAG = "FeedProvider"; - public static final String AUTHORITY = "com.newsblur"; public static final String VERSION = "v1"; @@ -67,7 +75,8 @@ public class FeedProvider extends ContentProvider { private static UriMatcher uriMatcher; static { - // TODO: Tidy this url-structure. It's not forward-facing but it's kind of a mess. + // TODO: get rid of the hard-coded URL paths and replace then with the constant values in DatabaseConstants + // that they actually represent. uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(AUTHORITY, VERSION + "/feeds/", ALL_FEEDS); uriMatcher.addURI(AUTHORITY, VERSION + "/social_feeds/", ALL_SOCIAL_FEEDS); @@ -259,7 +268,7 @@ public class FeedProvider extends ContentProvider { break; case UriMatcher.NO_MATCH: - Log.e(TAG, "No match found for URI: " + uri.toString()); + Log.e(this.getClass().getName(), "No match found for URI: " + uri.toString()); break; } return resultUri; @@ -271,15 +280,28 @@ public class FeedProvider extends ContentProvider { return true; } + /** + * A simple utility wrapper that lets us log the insanely complex queries used below for debugging. + */ + class LoggingDatabase { + SQLiteDatabase mdb; + public LoggingDatabase(SQLiteDatabase db) { + mdb = db; + } + public Cursor rawQuery(String sql, String[] selectionArgs) { + //Log.d(LoggingDatabase.class.getName(), "rawQuery: " + sql); + return mdb.rawQuery(sql, selectionArgs); + } + public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) { + return mdb.query(table, columns, selection, selectionArgs, groupBy, having, orderBy); + } + } + @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - // TODO: the fact that most of the app uses this subclass of ContentProvider cast as such may - // deepy confuse future maintainers as to why the .query() method magically does far, far more - // than suggested by the normal contract and provided args. This method should be renamed - // to make it painfully obvious that it expands upon the normal ContentProvider.query contract. - - final SQLiteDatabase db = databaseHelper.getReadableDatabase(); + final SQLiteDatabase rdb = databaseHelper.getReadableDatabase(); + final LoggingDatabase db = new LoggingDatabase(rdb); switch (uriMatcher.match(uri)) { // Query for all feeds (by default only return those that have unread items in them) @@ -417,7 +439,9 @@ public class FeedProvider extends ContentProvider { // Querying for all folders with unread items case ALL_FOLDERS: - String folderQuery = "SELECT " + TextUtils.join(",", DatabaseConstants.FOLDER_COLUMNS) + " FROM " + DatabaseConstants.FEED_FOLDER_MAP_TABLE + + // Note the extra special pre-select UNION clause here! + String folderQuery = DatabaseConstants.FOLDER_UNION_ROOT + + "SELECT " + TextUtils.join(",", DatabaseConstants.FOLDER_COLUMNS) + " FROM " + DatabaseConstants.FEED_FOLDER_MAP_TABLE + " INNER JOIN " + DatabaseConstants.FOLDER_TABLE + " ON " + DatabaseConstants.FEED_FOLDER_MAP_TABLE + "." + DatabaseConstants.FEED_FOLDER_FOLDER_NAME + " = " + DatabaseConstants.FOLDER_TABLE + "." + DatabaseConstants.FOLDER_NAME + " INNER JOIN " + DatabaseConstants.FEED_TABLE + diff --git a/media/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java b/media/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java index 65b083a1f..d78d63f0d 100644 --- a/media/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java +++ b/media/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java @@ -83,6 +83,13 @@ public class FeedFolderResponse { } socialFeeds = socialFeedsList.toArray(new SocialFeed[socialFeedsArray.size()]); } + + // sometimes the API won't declare the top-level/root folder, but most of the + // codebase expects it to exist. Declare it as empty if missing. + if (!folders.containsKey(AppConstants.ROOT_FOLDER)) { + folders.put(AppConstants.ROOT_FOLDER, new ArrayList()); + Log.d( this.getClass().getName(), "root folder was missing. added it."); + } } /** diff --git a/media/android/NewsBlur/src/com/newsblur/util/AppConstants.java b/media/android/NewsBlur/src/com/newsblur/util/AppConstants.java index 574144f8f..bc1f1a6fb 100644 --- a/media/android/NewsBlur/src/com/newsblur/util/AppConstants.java +++ b/media/android/NewsBlur/src/com/newsblur/util/AppConstants.java @@ -17,4 +17,6 @@ public class AppConstants { // the name to give the "root" folder in the local DB since the API does not assign it one. // this name should be unique and such that it will sort to the beginning of a list, ideally. public static final String ROOT_FOLDER = "0000_TOP_LEVEL_"; + + public static final String LAST_APP_VERSION = "LAST_APP_VERSION"; } diff --git a/media/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java b/media/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java index 25c305b51..c54d1c47d 100644 --- a/media/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java +++ b/media/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java @@ -11,9 +11,11 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; +import android.util.Log; import com.newsblur.activity.Login; import com.newsblur.database.BlurDatabase; @@ -29,6 +31,35 @@ public class PrefsUtils { edit.commit(); } + /** + * Check to see if this is the first launch of the app after an upgrade, in which case + * we clear the DB to prevent bugs associated with non-forward-compatibility. + */ + public static void checkForUpgrade(Context context) { + + SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0); + + String version; + try { + version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; + } catch (NameNotFoundException nnfe) { + Log.w(PrefsUtils.class.getName(), "could not determine app version"); + return; + } + Log.i(PrefsUtils.class.getName(), "launching version: " + version); + + String oldVersion = prefs.getString(AppConstants.LAST_APP_VERSION, null); + if ( (oldVersion == null) || (!oldVersion.equals(version)) ) { + Log.i(PrefsUtils.class.getName(), "detected new version of app, clearing local data"); + // wipe the local DB + BlurDatabase databaseHelper = new BlurDatabase(context.getApplicationContext()); + databaseHelper.dropAndRecreateTables(); + // store the current version + prefs.edit().putString(AppConstants.LAST_APP_VERSION, version).commit(); + } + + } + public static void logout(Context context) { // TODO: stop or wait for any BG processes