From ff7671c7b1b101c070cb018d1d70f98092a49943 Mon Sep 17 00:00:00 2001 From: sictiru Date: Wed, 8 May 2024 18:12:33 -0700 Subject: [PATCH 01/22] #1857 Override activity transition API 34+ --- .../java/com/newsblur/activity/ItemsList.java | 11 +-- .../newsblur/util/PendingTransitionUtils.kt | 68 +++++++++++++++++++ .../main/java/com/newsblur/util/UIUtils.java | 4 +- 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 clients/android/NewsBlur/app/src/main/java/com/newsblur/util/PendingTransitionUtils.kt diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/ItemsList.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/ItemsList.java index fc4f4bde2..275d29acb 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/ItemsList.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/ItemsList.java @@ -35,6 +35,7 @@ import com.newsblur.util.AppConstants; import com.newsblur.util.FeedSet; import com.newsblur.util.FeedUtils; import com.newsblur.util.Log; +import com.newsblur.util.PendingTransitionUtils; import com.newsblur.util.ReadingActionListener; import com.newsblur.util.PrefsUtils; import com.newsblur.util.Session; @@ -82,7 +83,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe Trace.beginSection("ItemsListOnCreate"); super.onCreate(bundle); - overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left); + PendingTransitionUtils.overrideEnterTransition(this); contextMenuDelegate = new ItemListContextMenuDelegateImpl(this, feedUtils); viewModel = new ViewModelProvider(this).get(ItemListViewModel.class); @@ -313,13 +314,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe @Override public void finish() { super.finish(); - /* - * Animate out the list by sliding it to the right and the Main activity in from - * the left. Do this when going back to Main as a subtle hint to the swipe gesture, - * to make the gesture feel more natural, and to override the really ugly transition - * used in some of the newer platforms. - */ - overridePendingTransition(R.anim.slide_in_from_left, R.anim.slide_out_to_right); + PendingTransitionUtils.overrideExitTransition(this); } abstract String getSaveSearchFeedId(); diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/PendingTransitionUtils.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/PendingTransitionUtils.kt new file mode 100644 index 000000000..60cddb30d --- /dev/null +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/PendingTransitionUtils.kt @@ -0,0 +1,68 @@ +@file:Suppress("DEPRECATION") + +package com.newsblur.util + +import android.app.Activity +import android.os.Build +import com.newsblur.R + +object PendingTransitionUtils { + + @JvmStatic + fun overrideEnterTransition(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activity.overrideActivityTransition( + Activity.OVERRIDE_TRANSITION_OPEN, + R.anim.slide_in_from_right, + R.anim.slide_out_to_left, + ) + } else { + activity.overridePendingTransition( + R.anim.slide_in_from_right, + R.anim.slide_out_to_left, + ) + } + } + + @JvmStatic + fun overrideNoEnterTransition(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activity.overrideActivityTransition( + Activity.OVERRIDE_TRANSITION_OPEN, + 0, + 0, + ) + } else { + activity.overridePendingTransition( + 0, + 0, + ) + } + } + + @JvmStatic + fun overrideExitTransition(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activity.overrideActivityTransition( + Activity.OVERRIDE_TRANSITION_CLOSE, + R.anim.slide_in_from_left, + R.anim.slide_out_to_right, + ) + } else { + activity.overridePendingTransition(R.anim.slide_in_from_left, R.anim.slide_out_to_right) + } + } + + @JvmStatic + fun overrideNoExitTransition(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activity.overrideActivityTransition( + Activity.OVERRIDE_TRANSITION_CLOSE, + 0, + 0, + ) + } else { + activity.overridePendingTransition(0, 0) + } + } +} \ No newline at end of file diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java index 1c343747c..ff98ee055 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java @@ -232,10 +232,10 @@ public class UIUtils { public void run() { Intent intent = activity.getIntent(); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); - activity.overridePendingTransition(0, 0); + PendingTransitionUtils.overrideNoExitTransition(activity); activity.finish(); - activity.overridePendingTransition(0, 0); + PendingTransitionUtils.overrideNoEnterTransition(activity); activity.startActivity(intent); } }); From 3e9d452cfc8f4f089d081c902c00820b83eea363 Mon Sep 17 00:00:00 2001 From: sictiru Date: Tue, 14 May 2024 15:33:38 -0700 Subject: [PATCH 02/22] Fix crash when the sync state was collected by the activity before any possible fragments were attached --- .../app/src/main/java/com/newsblur/activity/NbActivity.kt | 2 +- .../src/main/java/com/newsblur/fragment/ItemSetFragment.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/NbActivity.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/NbActivity.kt index 7bb0152c4..5055a5ce8 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/NbActivity.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/NbActivity.kt @@ -65,7 +65,7 @@ open class NbActivity : AppCompatActivity() { // Facilitates the db updates by the sync service on the UI lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { + repeatOnLifecycle(Lifecycle.State.STARTED) { launch { NbSyncManager.state.collectLatest { withContext(Dispatchers.Main) { diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ItemSetFragment.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ItemSetFragment.java index aa0d48663..f71c969af 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ItemSetFragment.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ItemSetFragment.java @@ -275,7 +275,7 @@ public class ItemSetFragment extends NbFragment { } protected FeedSet getFeedSet() { - return ((ItemsList) getActivity()).getFeedSet(); + return ((ItemsList) requireActivity()).getFeedSet(); } public void hasUpdated() { From d78d42a82ad96d6b80f85f24ab7c2febc3b5f702 Mon Sep 17 00:00:00 2001 From: Samuel Clay Date: Wed, 15 May 2024 16:48:14 -0400 Subject: [PATCH 03/22] Android v13.2.5 --- clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index e699d1fe5..b87d65eec 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 24 const val targetSdk = 34 - const val versionCode = 220 - const val versionName = "13.2.4" + const val versionCode = 221 + const val versionName = "13.2.5" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" From 1dbe85e689d2d606b84751305412067dcb4146fa Mon Sep 17 00:00:00 2001 From: sictiru Date: Fri, 24 May 2024 10:13:21 -0700 Subject: [PATCH 04/22] #1853 Remove read write db mutex --- .../newsblur/database/BlurDatabaseHelper.java | 706 ++++++++---------- 1 file changed, 329 insertions(+), 377 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java index 0bc123d80..918a2837b 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java @@ -52,36 +52,27 @@ import java.util.concurrent.Executors; */ public class BlurDatabaseHelper { - // manual synchro isn't needed if you only use one DBHelper, but at present the app uses several - public final static Object RW_MUTEX = new Object(); - private final BlurDatabase dbWrapper; private final SQLiteDatabase dbRO; private final SQLiteDatabase dbRW; public BlurDatabaseHelper(Context context) { com.newsblur.util.Log.d(this.getClass().getName(), "new DB conn requested"); - synchronized (RW_MUTEX) { - dbWrapper = new BlurDatabase(context); - dbRO = dbWrapper.getRO(); - dbRW = dbWrapper.getRW(); - } + dbWrapper = new BlurDatabase(context); + dbRO = dbWrapper.getRO(); + dbRW = dbWrapper.getRW(); } public void close() { // when asked to close, do so via an AsyncTask. This is so that (since becoming serial in android 4.0) // the closure will happen after other async tasks are done using the conn ExecutorService executorService = Executors.newSingleThreadExecutor(); - executorService.execute(() -> { - synchronized (RW_MUTEX) { - dbWrapper.close(); - } - }); + executorService.execute(dbWrapper::close); } public void dropAndRecreateTables() { com.newsblur.util.Log.i(this.getClass().getName(), "dropping and recreating all tables . . ."); - synchronized (RW_MUTEX) {dbWrapper.dropAndRecreateTables();} + dbWrapper.dropAndRecreateTables(); com.newsblur.util.Log.i(this.getClass().getName(), ". . . tables recreated."); } @@ -144,14 +135,13 @@ public class BlurDatabaseHelper { public void cleanupVeryOldStories() { Calendar cutoffDate = Calendar.getInstance(); cutoffDate.add(Calendar.MONTH, -1); - synchronized (RW_MUTEX) { - int count = dbRW.delete(DatabaseConstants.STORY_TABLE, - DatabaseConstants.STORY_TIMESTAMP + " < ?" + - " AND " + DatabaseConstants.STORY_TEXT_STORY_HASH + " NOT IN " + - "( SELECT " + DatabaseConstants.READING_SESSION_STORY_HASH + " FROM " + DatabaseConstants.READING_SESSION_TABLE + ")", - new String[]{Long.toString(cutoffDate.getTime().getTime())}); - com.newsblur.util.Log.d(this, "cleaned up ancient stories: " + count); - } + int count = dbRW.delete(DatabaseConstants.STORY_TABLE, + DatabaseConstants.STORY_TIMESTAMP + " < ?" + + " AND " + DatabaseConstants.STORY_TEXT_STORY_HASH + + " NOT IN " + "( SELECT " + DatabaseConstants.READING_SESSION_STORY_HASH + + " FROM " + DatabaseConstants.READING_SESSION_TABLE + ")", + new String[]{Long.toString(cutoffDate.getTime().getTime())}); + com.newsblur.util.Log.d(this, "cleaned up ancient stories: " + count); } /** @@ -159,14 +149,13 @@ public class BlurDatabaseHelper { * displayed to the user. */ public void cleanupReadStories() { - synchronized (RW_MUTEX) { - int count = dbRW.delete(DatabaseConstants.STORY_TABLE, - DatabaseConstants.STORY_READ + " = 1" + - " AND " + DatabaseConstants.STORY_TEXT_STORY_HASH + " NOT IN " + - "( SELECT " + DatabaseConstants.READING_SESSION_STORY_HASH + " FROM " + DatabaseConstants.READING_SESSION_TABLE + ")", - null); - com.newsblur.util.Log.d(this, "cleaned up read stories: " + count); - } + int count = dbRW.delete(DatabaseConstants.STORY_TABLE, + DatabaseConstants.STORY_READ + " = 1" + + " AND " + DatabaseConstants.STORY_TEXT_STORY_HASH + + " NOT IN " + "( SELECT " + DatabaseConstants.READING_SESSION_STORY_HASH + + " FROM " + DatabaseConstants.READING_SESSION_TABLE + ")", + null); + com.newsblur.util.Log.d(this, "cleaned up read stories: " + count); } public void cleanupStoryText() { @@ -174,37 +163,37 @@ public class BlurDatabaseHelper { " WHERE " + DatabaseConstants.STORY_TEXT_STORY_HASH + " NOT IN " + "( SELECT " + DatabaseConstants.STORY_HASH + " FROM " + DatabaseConstants.STORY_TABLE + ")"; - synchronized (RW_MUTEX) {dbRW.execSQL(q);} + dbRW.execSQL(q); } public void vacuum() { - synchronized (RW_MUTEX) {dbRW.execSQL("VACUUM");} + dbRW.execSQL("VACUUM"); } public void deleteFeed(String feedId) { String[] selArgs = new String[] {feedId}; - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.FEED_TABLE, DatabaseConstants.FEED_ID + " = ?", selArgs);} - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs);} + dbRW.delete(DatabaseConstants.FEED_TABLE, DatabaseConstants.FEED_ID + " = ?", selArgs); + dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs); } public void deleteSocialFeed(String userId) { String[] selArgs = new String[] {userId}; - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, DatabaseConstants.SOCIAL_FEED_ID + " = ?", selArgs);} - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs);} - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, DatabaseConstants.SOCIALFEED_STORY_USER_ID + " = ?", selArgs);} + dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, DatabaseConstants.SOCIAL_FEED_ID + " = ?", selArgs); + dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs); + dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, DatabaseConstants.SOCIALFEED_STORY_USER_ID + " = ?", selArgs); } public void deleteSavedSearch(String feedId, String query) { String q = "DELETE FROM " + DatabaseConstants.SAVED_SEARCH_TABLE + " WHERE " + DatabaseConstants.SAVED_SEARCH_FEED_ID + " = '" + feedId + "'" + " AND " + DatabaseConstants.SAVED_SEARCH_QUERY + " = '" + query + "'"; - synchronized (RW_MUTEX) {dbRW.execSQL(q);} + dbRW.execSQL(q); } public void deleteStories() { vacuum(); - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TABLE, null, null);} - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TEXT_TABLE, null, null);} + dbRW.delete(DatabaseConstants.STORY_TABLE, null, null); + dbRW.delete(DatabaseConstants.STORY_TEXT_TABLE, null, null); } public Feed getFeed(String feedId) { @@ -218,29 +207,25 @@ public class BlurDatabaseHelper { } public void updateFeed(Feed feed) { - synchronized (RW_MUTEX) { - dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, feed.getValues(), SQLiteDatabase.CONFLICT_REPLACE); - } + dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, feed.getValues(), SQLiteDatabase.CONFLICT_REPLACE); } private void bulkInsertValues(String table, List valuesList) { - if (valuesList.size() < 1) return; - synchronized (RW_MUTEX) { - dbRW.beginTransaction(); - try { - for (ContentValues values : valuesList) { - dbRW.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); - } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + if (valuesList.isEmpty()) return; + dbRW.beginTransaction(); + try { + for (ContentValues values : valuesList) { + dbRW.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } } // just like bulkInsertValues, but leaves sync/transactioning to the caller private void bulkInsertValuesExtSync(String table, List valuesList) { - if (valuesList.size() < 1) return; + if (valuesList.isEmpty()) return; for (ContentValues values : valuesList) { dbRW.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); } @@ -251,26 +236,24 @@ public class BlurDatabaseHelper { List socialFeedValues, List starredCountValues, List savedSearchValues) { - synchronized (RW_MUTEX) { - dbRW.beginTransaction(); - try { - dbRW.delete(DatabaseConstants.FEED_TABLE, null, null); - dbRW.delete(DatabaseConstants.FOLDER_TABLE, null, null); - dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, null, null); - dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, null, null); - dbRW.delete(DatabaseConstants.COMMENT_TABLE, null, null); - dbRW.delete(DatabaseConstants.REPLY_TABLE, null, null); - dbRW.delete(DatabaseConstants.STARREDCOUNTS_TABLE, null, null); - dbRW.delete(DatabaseConstants.SAVED_SEARCH_TABLE, null, null); - bulkInsertValuesExtSync(DatabaseConstants.FOLDER_TABLE, folderValues); - bulkInsertValuesExtSync(DatabaseConstants.FEED_TABLE, feedValues); - bulkInsertValuesExtSync(DatabaseConstants.SOCIALFEED_TABLE, socialFeedValues); - bulkInsertValuesExtSync(DatabaseConstants.STARREDCOUNTS_TABLE, starredCountValues); - bulkInsertValuesExtSync(DatabaseConstants.SAVED_SEARCH_TABLE, savedSearchValues); - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); - } + dbRW.beginTransaction(); + try { + dbRW.delete(DatabaseConstants.FEED_TABLE, null, null); + dbRW.delete(DatabaseConstants.FOLDER_TABLE, null, null); + dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, null, null); + dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, null, null); + dbRW.delete(DatabaseConstants.COMMENT_TABLE, null, null); + dbRW.delete(DatabaseConstants.REPLY_TABLE, null, null); + dbRW.delete(DatabaseConstants.STARREDCOUNTS_TABLE, null, null); + dbRW.delete(DatabaseConstants.SAVED_SEARCH_TABLE, null, null); + bulkInsertValuesExtSync(DatabaseConstants.FOLDER_TABLE, folderValues); + bulkInsertValuesExtSync(DatabaseConstants.FEED_TABLE, feedValues); + bulkInsertValuesExtSync(DatabaseConstants.SOCIALFEED_TABLE, socialFeedValues); + bulkInsertValuesExtSync(DatabaseConstants.STARREDCOUNTS_TABLE, starredCountValues); + bulkInsertValuesExtSync(DatabaseConstants.SAVED_SEARCH_TABLE, savedSearchValues); + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } } @@ -326,105 +309,95 @@ public class BlurDatabaseHelper { } public void insertStories(StoriesResponse apiResponse, StateFilter stateFilter, boolean forImmediateReading) { - synchronized (RW_MUTEX) { - // do not attempt to use beginTransactionNonExclusive() to reduce lock time for this very heavy set - // of calls. most versions of Android incorrectly implement the underlying SQLite calls and will - // result in crashes that poison the DB beyond repair - dbRW.beginTransaction(); - try { - - // to insert classifiers, we need to determine the feed ID of the stories in this - // response, so sniff one out. - String impliedFeedId = null; - - // handle users - if (apiResponse.users != null) { - List userValues = new ArrayList(apiResponse.users.length); - for (UserProfile user : apiResponse.users) { - userValues.add(user.getValues()); - } - bulkInsertValuesExtSync(DatabaseConstants.USER_TABLE, userValues); + // do not attempt to use beginTransactionNonExclusive() to reduce lock time for this very heavy set + // of calls. most versions of Android incorrectly implement the underlying SQLite calls and will + // result in crashes that poison the DB beyond repair + dbRW.beginTransaction(); + try { + // to insert classifiers, we need to determine the feed ID of the stories in this + // response, so sniff one out. + String impliedFeedId = null; + // handle users + if (apiResponse.users != null) { + List userValues = new ArrayList(apiResponse.users.length); + for (UserProfile user : apiResponse.users) { + userValues.add(user.getValues()); } - - // handle supplemental feed data that may have been included (usually in social requests) - if (apiResponse.feeds != null) { - List feedValues = new ArrayList(apiResponse.feeds.size()); - for (Feed feed : apiResponse.feeds) { - feedValues.add(feed.getValues()); - } - bulkInsertValuesExtSync(DatabaseConstants.FEED_TABLE, feedValues); - } - - // handle story content - if (apiResponse.stories != null) { - storiesloop: for (Story story : apiResponse.stories) { - if ((story.storyHash == null) || (story.storyHash.length() < 1)) { - // this is incredibly rare, but has been seen in crash reports at least twice. - com.newsblur.util.Log.e(this, "story received without story hash: " + story.id); - continue storiesloop; - } - insertSingleStoryExtSync(story); - // if the story is being fetched for the immediate session, also add the hash to the session table - if (forImmediateReading && story.isStoryVisibleInState(stateFilter)) { - ContentValues sessionHashValues = new ContentValues(); - sessionHashValues.put(DatabaseConstants.READING_SESSION_STORY_HASH, story.storyHash); - dbRW.insert(DatabaseConstants.READING_SESSION_TABLE, null, sessionHashValues); - } - impliedFeedId = story.feedId; - } - } - if (apiResponse.story != null) { - if ((apiResponse.story.storyHash == null) || (apiResponse.story.storyHash.length() < 1)) { - com.newsblur.util.Log.e(this, "story received without story hash: " + apiResponse.story.id); - return; - } - insertSingleStoryExtSync(apiResponse.story); - impliedFeedId = apiResponse.story.feedId; - } - - // handle classifiers - if (apiResponse.classifiers != null) { - for (Map.Entry entry : apiResponse.classifiers.entrySet()) { - // the API might not have included a feed ID, in which case it deserialized as -1 and must be implied - String classifierFeedId = entry.getKey(); - if (classifierFeedId.equals("-1")) { - classifierFeedId = impliedFeedId; - } - List classifierValues = entry.getValue().getContentValues(); - for (ContentValues values : classifierValues) { - values.put(DatabaseConstants.CLASSIFIER_ID, classifierFeedId); - } - dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", new String[] { classifierFeedId }); - bulkInsertValuesExtSync(DatabaseConstants.CLASSIFIER_TABLE, classifierValues); - } - } - - if (apiResponse.feedTags != null ) { - List feedTags = new ArrayList(apiResponse.feedTags.length); - for (String[] tuple : apiResponse.feedTags) { - // the API returns a list of lists, but all we care about is the tag name/id which is the first item in the tuple - if (tuple.length > 0) { - feedTags.add(tuple[0]); - } - } - putFeedTagsExtSync(impliedFeedId, feedTags); - } - - if (apiResponse.feedAuthors != null ) { - List feedAuthors = new ArrayList(apiResponse.feedAuthors.length); - for (String[] tuple : apiResponse.feedAuthors) { - // the API returns a list of lists, but all we care about is the author name/id which is the first item in the tuple - if (tuple.length > 0) { - feedAuthors.add(tuple[0]); - } - } - putFeedAuthorsExtSync(impliedFeedId, feedAuthors); - } - - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + bulkInsertValuesExtSync(DatabaseConstants.USER_TABLE, userValues); } + // handle supplemental feed data that may have been included (usually in social requests) + if (apiResponse.feeds != null) { + List feedValues = new ArrayList(apiResponse.feeds.size()); + for (Feed feed : apiResponse.feeds) { + feedValues.add(feed.getValues()); + } + bulkInsertValuesExtSync(DatabaseConstants.FEED_TABLE, feedValues); + } + // handle story content + if (apiResponse.stories != null) { + storiesloop: for (Story story : apiResponse.stories) { + if ((story.storyHash == null) || (story.storyHash.isEmpty())) { + // this is incredibly rare, but has been seen in crash reports at least twice. + com.newsblur.util.Log.e(this, "story received without story hash: " + story.id); + continue storiesloop; + } + insertSingleStoryExtSync(story); + // if the story is being fetched for the immediate session, also add the hash to the session table + if (forImmediateReading && story.isStoryVisibleInState(stateFilter)) { + ContentValues sessionHashValues = new ContentValues(); + sessionHashValues.put(DatabaseConstants.READING_SESSION_STORY_HASH, story.storyHash); + dbRW.insert(DatabaseConstants.READING_SESSION_TABLE, null, sessionHashValues); + } + impliedFeedId = story.feedId; + } + } + if (apiResponse.story != null) { + if ((apiResponse.story.storyHash == null) || (apiResponse.story.storyHash.isEmpty())) { + com.newsblur.util.Log.e(this, "story received without story hash: " + apiResponse.story.id); + return; + } + insertSingleStoryExtSync(apiResponse.story); + impliedFeedId = apiResponse.story.feedId; + } + // handle classifiers + if (apiResponse.classifiers != null) { + for (Map.Entry entry : apiResponse.classifiers.entrySet()) { + // the API might not have included a feed ID, in which case it deserialized as -1 and must be implied + String classifierFeedId = entry.getKey(); + if (classifierFeedId.equals("-1")) { + classifierFeedId = impliedFeedId; + } + List classifierValues = entry.getValue().getContentValues(); + for (ContentValues values : classifierValues) { + values.put(DatabaseConstants.CLASSIFIER_ID, classifierFeedId); + } + dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", new String[] { classifierFeedId }); + bulkInsertValuesExtSync(DatabaseConstants.CLASSIFIER_TABLE, classifierValues); + } + } + if (apiResponse.feedTags != null ) { + List feedTags = new ArrayList(apiResponse.feedTags.length); + for (String[] tuple : apiResponse.feedTags) { + // the API returns a list of lists, but all we care about is the tag name/id which is the first item in the tuple + if (tuple.length > 0) { + feedTags.add(tuple[0]); + } + } + putFeedTagsExtSync(impliedFeedId, feedTags); + } + if (apiResponse.feedAuthors != null ) { + List feedAuthors = new ArrayList(apiResponse.feedAuthors.length); + for (String[] tuple : apiResponse.feedAuthors) { + // the API returns a list of lists, but all we care about is the author name/id which is the first item in the tuple + if (tuple.length > 0) { + feedAuthors.add(tuple[0]); + } + } + putFeedAuthorsExtSync(impliedFeedId, feedAuthors); + } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } } @@ -504,33 +477,30 @@ public class BlurDatabaseHelper { * API. Most social APIs vend an updated view that replaces any old or placeholder records. */ public void updateComment(CommentResponse apiResponse, String storyId) { - synchronized (RW_MUTEX) { - // comments often contain enclosed replies, so batch them. - dbRW.beginTransaction(); - try { - // the API might include new supplemental user metadata if new replies have shown up. - if (apiResponse.users != null) { - List userValues = new ArrayList(apiResponse.users.length); - for (UserProfile user : apiResponse.users) { - userValues.add(user.getValues()); - } - bulkInsertValuesExtSync(DatabaseConstants.USER_TABLE, userValues); + // comments often contain enclosed replies, so batch them. + dbRW.beginTransaction(); + try { + // the API might include new supplemental user metadata if new replies have shown up. + if (apiResponse.users != null) { + List userValues = new ArrayList(apiResponse.users.length); + for (UserProfile user : apiResponse.users) { + userValues.add(user.getValues()); } - - // we store all comments in the context of the associated story, but the social API doesn't - // reference the story when responding, so fix that from our context - apiResponse.comment.storyId = storyId; - insertSingleCommentExtSync(apiResponse.comment); - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + bulkInsertValuesExtSync(DatabaseConstants.USER_TABLE, userValues); } + // we store all comments in the context of the associated story, but the social API doesn't + // reference the story when responding, so fix that from our context + apiResponse.comment.storyId = storyId; + insertSingleCommentExtSync(apiResponse.comment); + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } } public void fixMissingStoryFeeds(Story[] stories) { // start off with feeds mentioned by the set of stories - Set feedIds = new HashSet(); + Set feedIds = new HashSet<>(); for (Story story : stories) { feedIds.add(story.feedId); } @@ -543,7 +513,7 @@ public class BlurDatabaseHelper { } c.close(); // if any feeds are left, they are phantoms and need a fake entry - if (feedIds.size() < 1) return; + if (feedIds.isEmpty()) return; android.util.Log.i(this.getClass().getName(), "inserting missing metadata for " + feedIds.size() + " feeds used by new stories"); List feedValues = new ArrayList(feedIds.size()); for (String feedId : feedIds) { @@ -551,16 +521,14 @@ public class BlurDatabaseHelper { missingFeed.feedId = feedId; feedValues.add(missingFeed.getValues()); } - synchronized (RW_MUTEX) { - dbRW.beginTransaction(); - try { - for (ContentValues values : feedValues) { - dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, values, SQLiteDatabase.CONFLICT_IGNORE); - } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + dbRW.beginTransaction(); + try { + for (ContentValues values : feedValues) { + dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, values, SQLiteDatabase.CONFLICT_IGNORE); } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } } @@ -580,61 +548,55 @@ public class BlurDatabaseHelper { public void touchStory(String hash) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_LAST_READ_DATE, (new Date()).getTime()); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_LAST_READ_DATE + " < 1 AND " + DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_LAST_READ_DATE + " < 1 AND " + DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); } public void markStoryHashesRead(Collection hashes) { - synchronized (RW_MUTEX) { - dbRW.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_READ, true); - for (String hash : hashes) { - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); - } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + dbRW.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_READ, true); + for (String hash : hashes) { + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } } public void markStoryHashesStarred(Collection hashes, boolean isStarred) { - synchronized (RW_MUTEX) { - dbRW.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_STARRED, isStarred); - for (String hash : hashes) { - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); - } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + dbRW.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_STARRED, isStarred); + for (String hash : hashes) { + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } } public void setFeedsActive(Set feedIds, boolean active) { - synchronized (RW_MUTEX) { - dbRW.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.FEED_ACTIVE, active); - for (String feedId : feedIds) { - dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); - } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + dbRW.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.FEED_ACTIVE, active); + for (String feedId : feedIds) { + dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } } public void setFeedFetchPending(String feedId) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.FEED_FETCH_PENDING, true); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} + dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); } public boolean isFeedSetFetchPending(FeedSet fs) { @@ -660,7 +622,7 @@ public class BlurDatabaseHelper { public void setStoryReadState(String hash, boolean read) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_READ, read); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); } /** @@ -679,65 +641,63 @@ public class BlurDatabaseHelper { if (story.friendUserIds != null) { socialIds.addAll(Arrays.asList(story.friendUserIds)); } - if (socialIds.size() > 0) { + if (!socialIds.isEmpty()) { impactedFeeds.add(FeedSet.multipleSocialFeeds(socialIds)); } // check the story's starting state and the desired state and adjust it as an atom so we // know if it truly changed or not - synchronized (RW_MUTEX) { - dbRW.beginTransaction(); - try { - // get a fresh copy of the story from the DB so we know if it changed - Cursor c = dbRW.query(DatabaseConstants.STORY_TABLE, - new String[]{DatabaseConstants.STORY_READ}, - DatabaseConstants.STORY_HASH + " = ?", - new String[]{story.storyHash}, - null, null, null); - if (c.getCount() < 1) { - Log.w(this.getClass().getName(), "story removed before finishing mark-read"); - return impactedFeeds; - } - c.moveToFirst(); - boolean origState = (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.STORY_READ)) > 0); - c.close(); - // if there is nothing to be done, halt - if (origState == read) { - dbRW.setTransactionSuccessful(); - return impactedFeeds; - } - // update the story's read state - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_READ, read); - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{story.storyHash}); - // which column to inc/dec depends on story intel - String impactedCol; - String impactedSocialCol; - if (story.intelligence.calcTotalIntel() < 0) { - // negative stories don't affect counts - dbRW.setTransactionSuccessful(); - return impactedFeeds; - } else if (story.intelligence.calcTotalIntel() == 0 ) { - impactedCol = DatabaseConstants.FEED_NEUTRAL_COUNT; - impactedSocialCol = DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT; - } else { - impactedCol = DatabaseConstants.FEED_POSITIVE_COUNT; - impactedSocialCol = DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT; - } - String operator = (read ? " - 1" : " + 1"); - StringBuilder q = new StringBuilder("UPDATE " + DatabaseConstants.FEED_TABLE); - q.append(" SET ").append(impactedCol).append(" = ").append(impactedCol).append(operator); - q.append(" WHERE " + DatabaseConstants.FEED_ID + " = ").append(story.feedId); - dbRW.execSQL(q.toString()); - for (String socialId : socialIds) { - q = new StringBuilder("UPDATE " + DatabaseConstants.SOCIALFEED_TABLE); - q.append(" SET ").append(impactedSocialCol).append(" = ").append(impactedSocialCol).append(operator); - q.append(" WHERE " + DatabaseConstants.SOCIAL_FEED_ID + " = ").append(socialId); - dbRW.execSQL(q.toString()); - } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + dbRW.beginTransaction(); + try { + // get a fresh copy of the story from the DB so we know if it changed + Cursor c = dbRW.query(DatabaseConstants.STORY_TABLE, + new String[]{DatabaseConstants.STORY_READ}, + DatabaseConstants.STORY_HASH + " = ?", + new String[]{story.storyHash}, + null, null, null); + if (c.getCount() < 1) { + Log.w(this.getClass().getName(), "story removed before finishing mark-read"); + return impactedFeeds; } + c.moveToFirst(); + boolean origState = (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.STORY_READ)) > 0); + c.close(); + // if there is nothing to be done, halt + if (origState == read) { + dbRW.setTransactionSuccessful(); + return impactedFeeds; + } + // update the story's read state + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_READ, read); + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{story.storyHash}); + // which column to inc/dec depends on story intel + String impactedCol; + String impactedSocialCol; + if (story.intelligence.calcTotalIntel() < 0) { + // negative stories don't affect counts + dbRW.setTransactionSuccessful(); + return impactedFeeds; + } else if (story.intelligence.calcTotalIntel() == 0 ) { + impactedCol = DatabaseConstants.FEED_NEUTRAL_COUNT; + impactedSocialCol = DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT; + } else { + impactedCol = DatabaseConstants.FEED_POSITIVE_COUNT; + impactedSocialCol = DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT; + } + String operator = (read ? " - 1" : " + 1"); + StringBuilder q = new StringBuilder("UPDATE " + DatabaseConstants.FEED_TABLE); + q.append(" SET ").append(impactedCol).append(" = ").append(impactedCol).append(operator); + q.append(" WHERE " + DatabaseConstants.FEED_ID + " = ").append(story.feedId); + dbRW.execSQL(q.toString()); + for (String socialId : socialIds) { + q = new StringBuilder("UPDATE " + DatabaseConstants.SOCIALFEED_TABLE); + q.append(" SET ").append(impactedSocialCol).append(" = ").append(impactedSocialCol).append(operator); + q.append(" WHERE " + DatabaseConstants.SOCIAL_FEED_ID + " = ").append(socialId); + dbRW.execSQL(q.toString()); + } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } return impactedFeeds; } @@ -768,7 +728,7 @@ public class BlurDatabaseHelper { } else { throw new IllegalStateException("Asked to mark stories for FeedSet of unknown type."); } - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, conjoinSelections(feedSelection, rangeSelection), null);} + dbRW.update(DatabaseConstants.STORY_TABLE, values, conjoinSelections(feedSelection, rangeSelection), null); } /** @@ -830,11 +790,11 @@ public class BlurDatabaseHelper { } public void updateFeedCounts(String feedId, ContentValues values) { - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} + dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); } public void updateSocialFeedCounts(String feedId, ContentValues values) { - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{feedId});} + dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{feedId}); } /** @@ -867,7 +827,7 @@ public class BlurDatabaseHelper { values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEG)); values.put(DatabaseConstants.FEED_NEUTRAL_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEUT)); values.put(DatabaseConstants.FEED_POSITIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.BEST)); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} + dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); } for (String socialId : socialFeedIds) { @@ -876,7 +836,7 @@ public class BlurDatabaseHelper { values.put(DatabaseConstants.SOCIAL_FEED_NEGATIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEG)); values.put(DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEUT)); values.put(DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.BEST)); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{socialId});} + dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{socialId}); } } @@ -897,11 +857,11 @@ public class BlurDatabaseHelper { public void clearInfrequentSession() { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_INFREQUENT, false); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, null, null);} + dbRW.update(DatabaseConstants.STORY_TABLE, values, null, null); } public void enqueueAction(ReadingAction ra) { - synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.ACTION_TABLE, null, ra.toContentValues());} + dbRW.insertOrThrow(DatabaseConstants.ACTION_TABLE, null, ra.toContentValues()); } public Cursor getActions() { @@ -910,12 +870,10 @@ public class BlurDatabaseHelper { } public void incrementActionTried(String actionId) { - synchronized (RW_MUTEX) { - String q = "UPDATE " + DatabaseConstants.ACTION_TABLE + - " SET " + DatabaseConstants.ACTION_TRIED + " = " + DatabaseConstants.ACTION_TRIED + " + 1" + - " WHERE " + DatabaseConstants.ACTION_ID + " = ?"; - dbRW.execSQL(q, new String[]{actionId}); - } + String q = "UPDATE " + DatabaseConstants.ACTION_TABLE + + " SET " + DatabaseConstants.ACTION_TRIED + " = " + DatabaseConstants.ACTION_TRIED + " + 1" + + " WHERE " + DatabaseConstants.ACTION_ID + " = ?"; + dbRW.execSQL(q, new String[]{actionId}); } public int getUntriedActionCount() { @@ -927,54 +885,52 @@ public class BlurDatabaseHelper { } public void clearAction(String actionId) { - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.ACTION_TABLE, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId});} + dbRW.delete(DatabaseConstants.ACTION_TABLE, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId}); } public void setStoryStarred(String hash, @Nullable List userTags, boolean starred) { // check the story's starting state and the desired state and adjust it as an atom so we // know if it truly changed or not and thus whether to update counts - synchronized (RW_MUTEX) { - dbRW.beginTransaction(); - try { - // get a fresh copy of the story from the DB so we know if it changed - Cursor c = dbRW.query(DatabaseConstants.STORY_TABLE, - new String[]{DatabaseConstants.STORY_STARRED}, - DatabaseConstants.STORY_HASH + " = ?", - new String[]{hash}, - null, null, null); - if (c.getCount() < 1) { - Log.w(this.getClass().getName(), "story removed before finishing mark-starred"); - return; - } - c.moveToFirst(); - boolean origState = (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.STORY_STARRED)) > 0); - c.close(); - // if already stared, update user tags - if (origState == starred && starred && userTags != null) { - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_USER_TAGS, TextUtils.join(",", userTags)); - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); - return; - } - // if there is nothing to be done, halt - else if (origState == starred) { - return; - } - // fix the state - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_STARRED, starred); - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); - // adjust counts - String operator = (starred ? " + 1" : " - 1"); - StringBuilder q = new StringBuilder("UPDATE " + DatabaseConstants.STARREDCOUNTS_TABLE); - q.append(" SET " + DatabaseConstants.STARREDCOUNTS_COUNT + " = " + DatabaseConstants.STARREDCOUNTS_COUNT).append(operator); - q.append(" WHERE " + DatabaseConstants.STARREDCOUNTS_TAG + " = '" + StarredCount.TOTAL_STARRED + "'"); - // TODO: adjust counts per feed (and tags?) - dbRW.execSQL(q.toString()); - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + dbRW.beginTransaction(); + try { + // get a fresh copy of the story from the DB so we know if it changed + Cursor c = dbRW.query(DatabaseConstants.STORY_TABLE, + new String[]{DatabaseConstants.STORY_STARRED}, + DatabaseConstants.STORY_HASH + " = ?", + new String[]{hash}, + null, null, null); + if (c.getCount() < 1) { + Log.w(this.getClass().getName(), "story removed before finishing mark-starred"); + return; } + c.moveToFirst(); + boolean origState = (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.STORY_STARRED)) > 0); + c.close(); + // if already stared, update user tags + if (origState == starred && starred && userTags != null) { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_USER_TAGS, TextUtils.join(",", userTags)); + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + return; + } + // if there is nothing to be done, halt + else if (origState == starred) { + return; + } + // fix the state + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_STARRED, starred); + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + // adjust counts + String operator = (starred ? " + 1" : " - 1"); + StringBuilder q = new StringBuilder("UPDATE " + DatabaseConstants.STARREDCOUNTS_TABLE); + q.append(" SET " + DatabaseConstants.STARREDCOUNTS_COUNT + " = " + DatabaseConstants.STARREDCOUNTS_COUNT).append(operator); + q.append(" WHERE " + DatabaseConstants.STARREDCOUNTS_TAG + " = '" + StarredCount.TOTAL_STARRED + "'"); + // TODO: adjust counts per feed (and tags?) + dbRW.execSQL(q.toString()); + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } } @@ -1004,7 +960,7 @@ public class BlurDatabaseHelper { } ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_SHARED_USER_IDS, TextUtils.join(",", newIds)); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); } public String getStoryText(String hash) { @@ -1044,7 +1000,7 @@ public class BlurDatabaseHelper { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_TEXT_STORY_HASH, hash); values.put(DatabaseConstants.STORY_TEXT_STORY_TEXT, text); - synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.STORY_TEXT_TABLE, null, values);} + dbRW.insertOrThrow(DatabaseConstants.STORY_TEXT_TABLE, null, values); } public Cursor getSocialFeedsCursor(CancellationSignal cancellationSignal) { @@ -1174,7 +1130,7 @@ public class BlurDatabaseHelper { public void clearStorySession() { com.newsblur.util.Log.i(this, "reading session reset"); - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.READING_SESSION_TABLE, null, null);} + dbRW.delete(DatabaseConstants.READING_SESSION_TABLE, null, null); } /** @@ -1195,7 +1151,7 @@ public class BlurDatabaseHelper { q.append(" (" + DatabaseConstants.READING_SESSION_STORY_HASH + ") "); q.append(sel); - synchronized (RW_MUTEX) {dbRW.execSQL(q.toString(), selArgs.toArray(new String[0]));} + dbRW.execSQL(q.toString(), selArgs.toArray(new String[0])); } /** @@ -1285,17 +1241,17 @@ public class BlurDatabaseHelper { public void setSessionFeedSet(FeedSet fs) { if (fs == null) { - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SYNC_METADATA_TABLE, DatabaseConstants.SYNC_METADATA_KEY + " = ?", new String[] {DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET});} + dbRW.delete(DatabaseConstants.SYNC_METADATA_TABLE, DatabaseConstants.SYNC_METADATA_KEY + " = ?", new String[] {DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET}); } else { ContentValues values = new ContentValues(); values.put(DatabaseConstants.SYNC_METADATA_KEY, DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET); values.put(DatabaseConstants.SYNC_METADATA_VALUE, fs.toCompactSerial()); - synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.SYNC_METADATA_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);} + dbRW.insertWithOnConflict(DatabaseConstants.SYNC_METADATA_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE); } } public FeedSet getSessionFeedSet() { - FeedSet fs = null; + FeedSet fs; Cursor c = dbRO.query(DatabaseConstants.SYNC_METADATA_TABLE, null, DatabaseConstants.SYNC_METADATA_KEY + " = ?", new String[] {DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET}, null, null, null, null); if (c.getCount() < 1) return null; c.moveToFirst(); @@ -1310,7 +1266,7 @@ public class BlurDatabaseHelper { public void clearClassifiersForFeed(String feedId) { String[] selArgs = new String[] {feedId}; - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs);} + dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs); } public void insertClassifier(Classifier classifier) { @@ -1365,31 +1321,29 @@ public class BlurDatabaseHelper { if (TextUtils.isEmpty(commentText)) { comment.isPseudo = true; } - synchronized (RW_MUTEX) { - // in order to make this method idempotent (so it can be attempted before, during, or after - // the real comment is done, we have to check for a real one - if (getComment(storyId, userId) != null) { - com.newsblur.util.Log.i(this.getClass().getName(), "electing not to insert placeholder comment over live one"); - return; - } - dbRW.insertWithOnConflict(DatabaseConstants.COMMENT_TABLE, null, comment.getValues(), SQLiteDatabase.CONFLICT_REPLACE); + // in order to make this method idempotent (so it can be attempted before, during, or after + // the real comment is done, we have to check for a real one + if (getComment(storyId, userId) != null) { + com.newsblur.util.Log.i(this.getClass().getName(), "electing not to insert placeholder comment over live one"); + return; } + dbRW.insertWithOnConflict(DatabaseConstants.COMMENT_TABLE, null, comment.getValues(), SQLiteDatabase.CONFLICT_REPLACE); } public void editReply(String replyId, String replyText) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.REPLY_TEXT, replyText); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.REPLY_TABLE, values, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId});} + dbRW.update(DatabaseConstants.REPLY_TABLE, values, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId}); } public void deleteReply(String replyId) { - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId});} + dbRW.delete(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId}); } public void clearSelfComments(String storyId, @Nullable String userId) { - synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.COMMENT_TABLE, - DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?", - new String[]{storyId, userId});} + dbRW.delete(DatabaseConstants.COMMENT_TABLE, + DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?", + new String[]{storyId, userId}); } public void setCommentLiked(String storyId, String commentUserId, @Nullable String currentUserId, boolean liked) { @@ -1418,7 +1372,7 @@ public class BlurDatabaseHelper { } ContentValues values = new ContentValues(); values.put(DatabaseConstants.COMMENT_LIKING_USERS, TextUtils.join(",", newIds)); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.COMMENT_TABLE, values, DatabaseConstants.COMMENT_ID + " = ?", new String[]{comment.id});} + dbRW.update(DatabaseConstants.COMMENT_TABLE, values, DatabaseConstants.COMMENT_ID + " = ?", new String[]{comment.id}); } public UserProfile getUserProfile(String userId) { @@ -1464,14 +1418,14 @@ public class BlurDatabaseHelper { reply.userId = userId; reply.date = new Date(); reply.id = Reply.PLACEHOLDER_COMMENT_ID + storyId + comment.id + reply.userId; - synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE);} + dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE); } public void putStoryDismissed(String storyHash) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.NOTIFY_DISMISS_STORY_HASH, storyHash); values.put(DatabaseConstants.NOTIFY_DISMISS_TIME, Calendar.getInstance().getTime().getTime()); - synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, values);} + dbRW.insertOrThrow(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, values); } public boolean isStoryDismissed(String storyHash) { @@ -1486,12 +1440,10 @@ public class BlurDatabaseHelper { public void cleanupDismissals() { Calendar cutoffDate = Calendar.getInstance(); cutoffDate.add(Calendar.MONTH, -1); - synchronized (RW_MUTEX) { - int count = dbRW.delete(DatabaseConstants.NOTIFY_DISMISS_TABLE, - DatabaseConstants.NOTIFY_DISMISS_TIME + " < ?", - new String[]{Long.toString(cutoffDate.getTime().getTime())}); - com.newsblur.util.Log.d(this.getClass().getName(), "cleaned up dismissals: " + count); - } + int count = dbRW.delete(DatabaseConstants.NOTIFY_DISMISS_TABLE, + DatabaseConstants.NOTIFY_DISMISS_TIME + " < ?", + new String[]{Long.toString(cutoffDate.getTime().getTime())}); + com.newsblur.util.Log.d(this.getClass().getName(), "cleaned up dismissals: " + count); } private void putFeedTagsExtSync(String feedId, Collection tags) { @@ -1561,7 +1513,7 @@ public class BlurDatabaseHelper { public void renameFeed(String feedId, String newFeedName) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.FEED_TITLE, newFeedName); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} + dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); } public static void closeQuietly(Cursor c) { From f5dd0bf8fcb36523b980ca422c2bae0fa9e0eb1b Mon Sep 17 00:00:00 2001 From: sictiru Date: Fri, 14 Jun 2024 11:34:55 -0700 Subject: [PATCH 05/22] Catch OperationCanceledException exception when loading active stories --- .../java/com/newsblur/fragment/ItemSetFragment.java | 2 +- .../java/com/newsblur/viewModel/StoriesViewModel.kt | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ItemSetFragment.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ItemSetFragment.java index f71c969af..20c7b93b2 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ItemSetFragment.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ItemSetFragment.java @@ -299,7 +299,7 @@ public class ItemSetFragment extends NbFragment { ensureSufficientStories(); } - private void setCursor(Cursor cursor) { + private void setCursor(@Nullable Cursor cursor) { if (cursor != null) { if (!dbHelper.isFeedSetReady(getFeedSet())) { // the DB hasn't caught up yet from the last story list; don't display stale stories. diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/viewModel/StoriesViewModel.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/viewModel/StoriesViewModel.kt index 876de8a26..e1e5ed90f 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/viewModel/StoriesViewModel.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/viewModel/StoriesViewModel.kt @@ -2,6 +2,7 @@ package com.newsblur.viewModel import android.database.Cursor import android.os.CancellationSignal +import android.os.OperationCanceledException import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -9,6 +10,7 @@ import androidx.lifecycle.viewModelScope import com.newsblur.database.BlurDatabaseHelper import com.newsblur.util.CursorFilters import com.newsblur.util.FeedSet +import com.newsblur.util.Log import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -24,8 +26,12 @@ class StoriesViewModel fun getActiveStories(fs: FeedSet, cursorFilters: CursorFilters) { viewModelScope.launch(Dispatchers.IO) { - dbHelper.getActiveStoriesCursor(fs, cursorFilters, cancellationSignal).let { - _activeStoriesLiveData.postValue(it) + try { + dbHelper.getActiveStoriesCursor(fs, cursorFilters, cancellationSignal).let { + _activeStoriesLiveData.postValue(it) + } + } catch (e: OperationCanceledException) { + Log.e(this.javaClass.name, "Caught ${e.javaClass.name} in getActiveStories.") } } } From 192e94df529e538ac5d3a983d8cfd5e3006cae1d Mon Sep 17 00:00:00 2001 From: sictiru Date: Sun, 16 Jun 2024 18:31:21 -0700 Subject: [PATCH 06/22] Android v13.2.6 --- clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index b87d65eec..80ece09e3 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 24 const val targetSdk = 34 - const val versionCode = 221 - const val versionName = "13.2.5" + const val versionCode = 222 + const val versionName = "13.2.6" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" From fbea670d2f01c4f5b633bb0f3dcf4c5a0a4dc153 Mon Sep 17 00:00:00 2001 From: sictiru Date: Tue, 18 Jun 2024 11:29:51 -0700 Subject: [PATCH 07/22] Remove play core in favor or play review. Update other dependencies --- .../benchmark/BaselineProfileGenerator.kt | 9 +++++---- clients/android/NewsBlur/app/build.gradle.kts | 2 +- .../com/newsblur/activity/FeedItemsList.java | 2 +- .../buildSrc/src/main/java/Dependencies.kt | 2 +- .../NewsBlur/buildSrc/src/main/java/Version.kt | 18 +++++++++--------- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/clients/android/NewsBlur/app/benchmark/src/main/java/com/newsblur/benchmark/BaselineProfileGenerator.kt b/clients/android/NewsBlur/app/benchmark/src/main/java/com/newsblur/benchmark/BaselineProfileGenerator.kt index 6b795d11a..67862aac4 100644 --- a/clients/android/NewsBlur/app/benchmark/src/main/java/com/newsblur/benchmark/BaselineProfileGenerator.kt +++ b/clients/android/NewsBlur/app/benchmark/src/main/java/com/newsblur/benchmark/BaselineProfileGenerator.kt @@ -1,6 +1,7 @@ package com.newsblur.benchmark -import androidx.benchmark.macro.ExperimentalBaselineProfilesApi +import android.os.Build +import androidx.annotation.RequiresApi import androidx.benchmark.macro.junit4.BaselineProfileRule import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Rule @@ -10,7 +11,7 @@ import org.junit.runner.RunWith /** * Runs in its own process */ -@OptIn(ExperimentalBaselineProfilesApi::class) +@RequiresApi(Build.VERSION_CODES.P) @RunWith(AndroidJUnit4::class) class BaselineProfileGenerator { @@ -19,7 +20,7 @@ class BaselineProfileGenerator { @Test fun generateSimpleStartupProfile() { - rule.collectBaselineProfile(packageName = "com.newsblur") { + rule.collect(packageName = "com.newsblur") { pressHome() startActivityAndWait() } @@ -28,7 +29,7 @@ class BaselineProfileGenerator { @Test fun generateUserJourneyProfile() { var needsLogin = true - rule.collectBaselineProfile(packageName = "com.newsblur") { + rule.collect(packageName = "com.newsblur") { pressHome() startActivityAndWait() diff --git a/clients/android/NewsBlur/app/build.gradle.kts b/clients/android/NewsBlur/app/build.gradle.kts index 4bfd97f59..1705b7a63 100644 --- a/clients/android/NewsBlur/app/build.gradle.kts +++ b/clients/android/NewsBlur/app/build.gradle.kts @@ -57,7 +57,7 @@ dependencies { implementation(Dependencies.okHttp) implementation(Dependencies.gson) implementation(Dependencies.billing) - implementation(Dependencies.playCore) + implementation(Dependencies.playReview) implementation(Dependencies.material) implementation(Dependencies.preference) implementation(Dependencies.browser) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/FeedItemsList.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/FeedItemsList.java index ae7bb1a75..6eea8a4db 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/FeedItemsList.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/FeedItemsList.java @@ -10,10 +10,10 @@ import androidx.fragment.app.DialogFragment; import android.view.Menu; import android.view.MenuItem; +import com.google.android.gms.tasks.Task; import com.google.android.play.core.review.ReviewInfo; import com.google.android.play.core.review.ReviewManager; import com.google.android.play.core.review.ReviewManagerFactory; -import com.google.android.play.core.tasks.Task; import com.newsblur.R; import com.newsblur.di.IconLoader; import com.newsblur.domain.Feed; diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Dependencies.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Dependencies.kt index faa769120..74756ce07 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Dependencies.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Dependencies.kt @@ -6,7 +6,6 @@ object Dependencies { const val okHttp = "com.squareup.okhttp3:okhttp:${Version.okHttp}" const val gson = "com.google.code.gson:gson:${Version.gson}" const val billing = "com.android.billingclient:billing:${Version.billing}" - const val playCore = "com.google.android.play:core:${Version.playCore}" const val material = "com.google.android.material:material:${Version.material}" const val preference = "androidx.preference:preference-ktx:${Version.preference}" const val browser = "androidx.browser:browser:${Version.browser}" @@ -16,6 +15,7 @@ object Dependencies { const val hiltAndroid = "com.google.dagger:hilt-android:${Version.hilt}" const val hiltCompiler = "com.google.dagger:hilt-compiler:${Version.hilt}" const val profileInstaller = "androidx.profileinstaller:profileinstaller:${Version.profileInstaller}" + const val playReview = "com.google.android.play:review:${Version.playReview}" // test const val junit = "junit:junit:${Version.junit}" diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Version.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Version.kt index 2fca678e1..14fc81d0f 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Version.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Version.kt @@ -1,35 +1,35 @@ object Version { - const val android = "8.3.1" + const val android = "8.5.0" const val kotlin = "1.9.23" - const val fragment = "1.6.2" + const val fragment = "1.8.0" const val recyclerView = "1.3.2" const val swipeRefreshLayout = "1.1.0" const val okHttp = "4.12.0" - const val gson = "2.10.1" + const val gson = "2.11.0" const val billing = "6.2.0" - const val playCore = "1.10.3" - const val material = "1.11.0" + const val playReview = "2.0.1" + const val material = "1.12.0" const val preference = "1.2.1" const val browser = "1.8.0" - const val lifecycle = "2.7.0" + const val lifecycle = "2.8.2" const val splashScreen = "1.0.1" - const val hilt = "2.51" + const val hilt = "2.51.1" const val profileInstaller = "1.3.1" const val junit = "4.13.2" - const val mockk = "1.13.10" + const val mockk = "1.13.11" const val junitExt = "1.1.5" const val espresso = "3.5.1" const val uiAutomator = "2.3.0" - const val benchmarkMacroJunit4 = "1.2.3" + const val benchmarkMacroJunit4 = "1.2.4" const val benManesVersions = "0.51.0" } \ No newline at end of file From 71233a0a72e3a0ddfaa04e98c239c16e326a7ab7 Mon Sep 17 00:00:00 2001 From: sictiru Date: Wed, 19 Jun 2024 14:28:04 -0700 Subject: [PATCH 08/22] Android v13.2.7 --- clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index 80ece09e3..17bba2bcb 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 24 const val targetSdk = 34 - const val versionCode = 222 - const val versionName = "13.2.6" + const val versionCode = 223 + const val versionName = "13.2.7" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" From 7d472dea7df3633088e86fcddea50846414fec47 Mon Sep 17 00:00:00 2001 From: sictiru Date: Thu, 27 Jun 2024 10:02:02 -0700 Subject: [PATCH 09/22] Cleanup web view binding on destroy view --- .../newsblur/fragment/ReadingItemFragment.kt | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt index 1e33501cc..f2e4d1385 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt @@ -1,6 +1,8 @@ package com.newsblur.fragment -import android.content.* +import android.content.Context +import android.content.DialogInterface +import android.content.Intent import android.content.res.Configuration import android.graphics.Color import android.graphics.Typeface @@ -9,8 +11,12 @@ import android.net.Uri import android.os.Bundle import android.text.TextUtils import android.util.Log -import android.view.* +import android.view.ContextMenu import android.view.ContextMenu.ContextMenuInfo +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.webkit.WebView.HitTestResult import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu @@ -38,8 +44,20 @@ import com.newsblur.service.NbSyncManager.UPDATE_SOCIAL import com.newsblur.service.NbSyncManager.UPDATE_STORY import com.newsblur.service.NbSyncManager.UPDATE_TEXT import com.newsblur.service.OriginalTextService -import com.newsblur.util.* +import com.newsblur.util.DefaultFeedView +import com.newsblur.util.FeedSet +import com.newsblur.util.FeedUtils +import com.newsblur.util.FileCache +import com.newsblur.util.Font +import com.newsblur.util.ImageLoader +import com.newsblur.util.MarkStoryReadBehavior import com.newsblur.util.PrefConstants.ThemeValue +import com.newsblur.util.PrefsUtils +import com.newsblur.util.ReadingTextSize +import com.newsblur.util.StoryChangesState +import com.newsblur.util.StoryUtils +import com.newsblur.util.UIUtils +import com.newsblur.util.executeAsyncTask import dagger.hilt.android.AndroidEntryPoint import java.util.regex.Pattern import javax.inject.Inject @@ -104,8 +122,11 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { private var savedScrollPosRel = 0f private val webViewContentMutex = Any() - private lateinit var binding: FragmentReadingitemBinding - private lateinit var readingItemActionsBinding: ReadingItemActionsBinding + private var _binding: FragmentReadingitemBinding? = null + private var _readingItemActionsBinding: ReadingItemActionsBinding? = null + private val binding get() = _binding!! + private val readingItemActionsBinding get() = _readingItemActionsBinding!! + private lateinit var markStoryReadBehavior: MarkStoryReadBehavior private var sampledQueue: SampledQueue? = null @@ -145,16 +166,11 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { override fun onDestroyView() { sampledQueue?.close() + _readingItemActionsBinding = null + _binding = null super.onDestroyView() } - override fun onDestroy() { - binding.readingWebview.setOnTouchListener(null) - binding.root.setOnTouchListener(null) - requireActivity().window.decorView.setOnSystemUiVisibilityChangeListener(null) - super.onDestroy() - } - // WebViews don't automatically pause content like audio and video when they lose focus. Chain our own // state into the webview so it behaves. override fun onPause() { @@ -169,9 +185,8 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.fragment_readingitem, container, false) - binding = FragmentReadingitemBinding.bind(view) - readingItemActionsBinding = ReadingItemActionsBinding.bind(binding.root) + _binding = FragmentReadingitemBinding.inflate(inflater, container, false) + _readingItemActionsBinding = ReadingItemActionsBinding.bind(binding.root) val readingActivity = requireActivity() as Reading fs = readingActivity.fs @@ -194,7 +209,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { binding.readingScrollview.registerScrollChangeListener(readingActivity) - return view + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { From feb1f23cec85148db9f08176e4a6eb2003ef77a7 Mon Sep 17 00:00:00 2001 From: sictiru Date: Thu, 27 Jun 2024 10:06:43 -0700 Subject: [PATCH 10/22] Android v13.2.8 --- clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index 17bba2bcb..4d11ab117 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 24 const val targetSdk = 34 - const val versionCode = 223 - const val versionName = "13.2.7" + const val versionCode = 224 + const val versionName = "13.2.8" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" From ebc1025e5e479f9b705d1115338d83244e93bc58 Mon Sep 17 00:00:00 2001 From: sictiru Date: Fri, 5 Jul 2024 15:26:14 -0700 Subject: [PATCH 11/22] Android v13.2.9. Destroy webview on fragment view destroy --- .../main/java/com/newsblur/fragment/ReadingItemFragment.kt | 1 + clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt index f2e4d1385..07ee97877 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt @@ -166,6 +166,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { override fun onDestroyView() { sampledQueue?.close() + binding.readingWebview.destroy() _readingItemActionsBinding = null _binding = null super.onDestroyView() diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index 4d11ab117..da6967611 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 24 const val targetSdk = 34 - const val versionCode = 224 - const val versionName = "13.2.8" + const val versionCode = 225 + const val versionName = "13.2.9" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" From 0fdeb7071b1f31de5ab354fdb88c1c1bf6ae62ab Mon Sep 17 00:00:00 2001 From: sictiru Date: Mon, 8 Jul 2024 14:03:01 -0700 Subject: [PATCH 12/22] Android v13.2.10. Clear web chrome client on web view destroy --- .../main/java/com/newsblur/fragment/ReadingItemFragment.kt | 2 +- clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt index 07ee97877..f64736d1f 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt @@ -166,7 +166,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { override fun onDestroyView() { sampledQueue?.close() - binding.readingWebview.destroy() + binding.readingWebview.webChromeClient = null _readingItemActionsBinding = null _binding = null super.onDestroyView() diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index da6967611..e6b30b17e 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 24 const val targetSdk = 34 - const val versionCode = 225 - const val versionName = "13.2.9" + const val versionCode = 226 + const val versionName = "13.2.10" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" From ab2490a99b4f9f75e6f02215e45cbaa0be3a29a9 Mon Sep 17 00:00:00 2001 From: sictiru Date: Tue, 9 Jul 2024 13:37:02 -0700 Subject: [PATCH 13/22] Revert "#1853 Remove read write db mutex" This reverts commit 1dbe85e689d2d606b84751305412067dcb4146fa. --- .../newsblur/database/BlurDatabaseHelper.java | 686 ++++++++++-------- 1 file changed, 367 insertions(+), 319 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java index 918a2837b..0bc123d80 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java @@ -52,27 +52,36 @@ import java.util.concurrent.Executors; */ public class BlurDatabaseHelper { + // manual synchro isn't needed if you only use one DBHelper, but at present the app uses several + public final static Object RW_MUTEX = new Object(); + private final BlurDatabase dbWrapper; private final SQLiteDatabase dbRO; private final SQLiteDatabase dbRW; public BlurDatabaseHelper(Context context) { com.newsblur.util.Log.d(this.getClass().getName(), "new DB conn requested"); - dbWrapper = new BlurDatabase(context); - dbRO = dbWrapper.getRO(); - dbRW = dbWrapper.getRW(); + synchronized (RW_MUTEX) { + dbWrapper = new BlurDatabase(context); + dbRO = dbWrapper.getRO(); + dbRW = dbWrapper.getRW(); + } } public void close() { // when asked to close, do so via an AsyncTask. This is so that (since becoming serial in android 4.0) // the closure will happen after other async tasks are done using the conn ExecutorService executorService = Executors.newSingleThreadExecutor(); - executorService.execute(dbWrapper::close); + executorService.execute(() -> { + synchronized (RW_MUTEX) { + dbWrapper.close(); + } + }); } public void dropAndRecreateTables() { com.newsblur.util.Log.i(this.getClass().getName(), "dropping and recreating all tables . . ."); - dbWrapper.dropAndRecreateTables(); + synchronized (RW_MUTEX) {dbWrapper.dropAndRecreateTables();} com.newsblur.util.Log.i(this.getClass().getName(), ". . . tables recreated."); } @@ -135,13 +144,14 @@ public class BlurDatabaseHelper { public void cleanupVeryOldStories() { Calendar cutoffDate = Calendar.getInstance(); cutoffDate.add(Calendar.MONTH, -1); - int count = dbRW.delete(DatabaseConstants.STORY_TABLE, - DatabaseConstants.STORY_TIMESTAMP + " < ?" + - " AND " + DatabaseConstants.STORY_TEXT_STORY_HASH + - " NOT IN " + "( SELECT " + DatabaseConstants.READING_SESSION_STORY_HASH + - " FROM " + DatabaseConstants.READING_SESSION_TABLE + ")", - new String[]{Long.toString(cutoffDate.getTime().getTime())}); - com.newsblur.util.Log.d(this, "cleaned up ancient stories: " + count); + synchronized (RW_MUTEX) { + int count = dbRW.delete(DatabaseConstants.STORY_TABLE, + DatabaseConstants.STORY_TIMESTAMP + " < ?" + + " AND " + DatabaseConstants.STORY_TEXT_STORY_HASH + " NOT IN " + + "( SELECT " + DatabaseConstants.READING_SESSION_STORY_HASH + " FROM " + DatabaseConstants.READING_SESSION_TABLE + ")", + new String[]{Long.toString(cutoffDate.getTime().getTime())}); + com.newsblur.util.Log.d(this, "cleaned up ancient stories: " + count); + } } /** @@ -149,13 +159,14 @@ public class BlurDatabaseHelper { * displayed to the user. */ public void cleanupReadStories() { - int count = dbRW.delete(DatabaseConstants.STORY_TABLE, - DatabaseConstants.STORY_READ + " = 1" + - " AND " + DatabaseConstants.STORY_TEXT_STORY_HASH + - " NOT IN " + "( SELECT " + DatabaseConstants.READING_SESSION_STORY_HASH + - " FROM " + DatabaseConstants.READING_SESSION_TABLE + ")", - null); - com.newsblur.util.Log.d(this, "cleaned up read stories: " + count); + synchronized (RW_MUTEX) { + int count = dbRW.delete(DatabaseConstants.STORY_TABLE, + DatabaseConstants.STORY_READ + " = 1" + + " AND " + DatabaseConstants.STORY_TEXT_STORY_HASH + " NOT IN " + + "( SELECT " + DatabaseConstants.READING_SESSION_STORY_HASH + " FROM " + DatabaseConstants.READING_SESSION_TABLE + ")", + null); + com.newsblur.util.Log.d(this, "cleaned up read stories: " + count); + } } public void cleanupStoryText() { @@ -163,37 +174,37 @@ public class BlurDatabaseHelper { " WHERE " + DatabaseConstants.STORY_TEXT_STORY_HASH + " NOT IN " + "( SELECT " + DatabaseConstants.STORY_HASH + " FROM " + DatabaseConstants.STORY_TABLE + ")"; - dbRW.execSQL(q); + synchronized (RW_MUTEX) {dbRW.execSQL(q);} } public void vacuum() { - dbRW.execSQL("VACUUM"); + synchronized (RW_MUTEX) {dbRW.execSQL("VACUUM");} } public void deleteFeed(String feedId) { String[] selArgs = new String[] {feedId}; - dbRW.delete(DatabaseConstants.FEED_TABLE, DatabaseConstants.FEED_ID + " = ?", selArgs); - dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs); + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.FEED_TABLE, DatabaseConstants.FEED_ID + " = ?", selArgs);} + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs);} } public void deleteSocialFeed(String userId) { String[] selArgs = new String[] {userId}; - dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, DatabaseConstants.SOCIAL_FEED_ID + " = ?", selArgs); - dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs); - dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, DatabaseConstants.SOCIALFEED_STORY_USER_ID + " = ?", selArgs); + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, DatabaseConstants.SOCIAL_FEED_ID + " = ?", selArgs);} + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs);} + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, DatabaseConstants.SOCIALFEED_STORY_USER_ID + " = ?", selArgs);} } public void deleteSavedSearch(String feedId, String query) { String q = "DELETE FROM " + DatabaseConstants.SAVED_SEARCH_TABLE + " WHERE " + DatabaseConstants.SAVED_SEARCH_FEED_ID + " = '" + feedId + "'" + " AND " + DatabaseConstants.SAVED_SEARCH_QUERY + " = '" + query + "'"; - dbRW.execSQL(q); + synchronized (RW_MUTEX) {dbRW.execSQL(q);} } public void deleteStories() { vacuum(); - dbRW.delete(DatabaseConstants.STORY_TABLE, null, null); - dbRW.delete(DatabaseConstants.STORY_TEXT_TABLE, null, null); + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TABLE, null, null);} + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TEXT_TABLE, null, null);} } public Feed getFeed(String feedId) { @@ -207,25 +218,29 @@ public class BlurDatabaseHelper { } public void updateFeed(Feed feed) { - dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, feed.getValues(), SQLiteDatabase.CONFLICT_REPLACE); + synchronized (RW_MUTEX) { + dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, feed.getValues(), SQLiteDatabase.CONFLICT_REPLACE); + } } private void bulkInsertValues(String table, List valuesList) { - if (valuesList.isEmpty()) return; - dbRW.beginTransaction(); - try { - for (ContentValues values : valuesList) { - dbRW.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); + if (valuesList.size() < 1) return; + synchronized (RW_MUTEX) { + dbRW.beginTransaction(); + try { + for (ContentValues values : valuesList) { + dbRW.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); } } // just like bulkInsertValues, but leaves sync/transactioning to the caller private void bulkInsertValuesExtSync(String table, List valuesList) { - if (valuesList.isEmpty()) return; + if (valuesList.size() < 1) return; for (ContentValues values : valuesList) { dbRW.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); } @@ -236,24 +251,26 @@ public class BlurDatabaseHelper { List socialFeedValues, List starredCountValues, List savedSearchValues) { - dbRW.beginTransaction(); - try { - dbRW.delete(DatabaseConstants.FEED_TABLE, null, null); - dbRW.delete(DatabaseConstants.FOLDER_TABLE, null, null); - dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, null, null); - dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, null, null); - dbRW.delete(DatabaseConstants.COMMENT_TABLE, null, null); - dbRW.delete(DatabaseConstants.REPLY_TABLE, null, null); - dbRW.delete(DatabaseConstants.STARREDCOUNTS_TABLE, null, null); - dbRW.delete(DatabaseConstants.SAVED_SEARCH_TABLE, null, null); - bulkInsertValuesExtSync(DatabaseConstants.FOLDER_TABLE, folderValues); - bulkInsertValuesExtSync(DatabaseConstants.FEED_TABLE, feedValues); - bulkInsertValuesExtSync(DatabaseConstants.SOCIALFEED_TABLE, socialFeedValues); - bulkInsertValuesExtSync(DatabaseConstants.STARREDCOUNTS_TABLE, starredCountValues); - bulkInsertValuesExtSync(DatabaseConstants.SAVED_SEARCH_TABLE, savedSearchValues); - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); + synchronized (RW_MUTEX) { + dbRW.beginTransaction(); + try { + dbRW.delete(DatabaseConstants.FEED_TABLE, null, null); + dbRW.delete(DatabaseConstants.FOLDER_TABLE, null, null); + dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, null, null); + dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, null, null); + dbRW.delete(DatabaseConstants.COMMENT_TABLE, null, null); + dbRW.delete(DatabaseConstants.REPLY_TABLE, null, null); + dbRW.delete(DatabaseConstants.STARREDCOUNTS_TABLE, null, null); + dbRW.delete(DatabaseConstants.SAVED_SEARCH_TABLE, null, null); + bulkInsertValuesExtSync(DatabaseConstants.FOLDER_TABLE, folderValues); + bulkInsertValuesExtSync(DatabaseConstants.FEED_TABLE, feedValues); + bulkInsertValuesExtSync(DatabaseConstants.SOCIALFEED_TABLE, socialFeedValues); + bulkInsertValuesExtSync(DatabaseConstants.STARREDCOUNTS_TABLE, starredCountValues); + bulkInsertValuesExtSync(DatabaseConstants.SAVED_SEARCH_TABLE, savedSearchValues); + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); + } } } @@ -309,95 +326,105 @@ public class BlurDatabaseHelper { } public void insertStories(StoriesResponse apiResponse, StateFilter stateFilter, boolean forImmediateReading) { - // do not attempt to use beginTransactionNonExclusive() to reduce lock time for this very heavy set - // of calls. most versions of Android incorrectly implement the underlying SQLite calls and will - // result in crashes that poison the DB beyond repair - dbRW.beginTransaction(); - try { - // to insert classifiers, we need to determine the feed ID of the stories in this - // response, so sniff one out. - String impliedFeedId = null; - // handle users - if (apiResponse.users != null) { - List userValues = new ArrayList(apiResponse.users.length); - for (UserProfile user : apiResponse.users) { - userValues.add(user.getValues()); - } - bulkInsertValuesExtSync(DatabaseConstants.USER_TABLE, userValues); - } - // handle supplemental feed data that may have been included (usually in social requests) - if (apiResponse.feeds != null) { - List feedValues = new ArrayList(apiResponse.feeds.size()); - for (Feed feed : apiResponse.feeds) { - feedValues.add(feed.getValues()); - } - bulkInsertValuesExtSync(DatabaseConstants.FEED_TABLE, feedValues); - } - // handle story content - if (apiResponse.stories != null) { - storiesloop: for (Story story : apiResponse.stories) { - if ((story.storyHash == null) || (story.storyHash.isEmpty())) { - // this is incredibly rare, but has been seen in crash reports at least twice. - com.newsblur.util.Log.e(this, "story received without story hash: " + story.id); - continue storiesloop; - } - insertSingleStoryExtSync(story); - // if the story is being fetched for the immediate session, also add the hash to the session table - if (forImmediateReading && story.isStoryVisibleInState(stateFilter)) { - ContentValues sessionHashValues = new ContentValues(); - sessionHashValues.put(DatabaseConstants.READING_SESSION_STORY_HASH, story.storyHash); - dbRW.insert(DatabaseConstants.READING_SESSION_TABLE, null, sessionHashValues); + synchronized (RW_MUTEX) { + // do not attempt to use beginTransactionNonExclusive() to reduce lock time for this very heavy set + // of calls. most versions of Android incorrectly implement the underlying SQLite calls and will + // result in crashes that poison the DB beyond repair + dbRW.beginTransaction(); + try { + + // to insert classifiers, we need to determine the feed ID of the stories in this + // response, so sniff one out. + String impliedFeedId = null; + + // handle users + if (apiResponse.users != null) { + List userValues = new ArrayList(apiResponse.users.length); + for (UserProfile user : apiResponse.users) { + userValues.add(user.getValues()); } - impliedFeedId = story.feedId; - } - } - if (apiResponse.story != null) { - if ((apiResponse.story.storyHash == null) || (apiResponse.story.storyHash.isEmpty())) { - com.newsblur.util.Log.e(this, "story received without story hash: " + apiResponse.story.id); - return; + bulkInsertValuesExtSync(DatabaseConstants.USER_TABLE, userValues); } - insertSingleStoryExtSync(apiResponse.story); - impliedFeedId = apiResponse.story.feedId; - } - // handle classifiers - if (apiResponse.classifiers != null) { - for (Map.Entry entry : apiResponse.classifiers.entrySet()) { - // the API might not have included a feed ID, in which case it deserialized as -1 and must be implied - String classifierFeedId = entry.getKey(); - if (classifierFeedId.equals("-1")) { - classifierFeedId = impliedFeedId; + + // handle supplemental feed data that may have been included (usually in social requests) + if (apiResponse.feeds != null) { + List feedValues = new ArrayList(apiResponse.feeds.size()); + for (Feed feed : apiResponse.feeds) { + feedValues.add(feed.getValues()); } - List classifierValues = entry.getValue().getContentValues(); - for (ContentValues values : classifierValues) { - values.put(DatabaseConstants.CLASSIFIER_ID, classifierFeedId); - } - dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", new String[] { classifierFeedId }); - bulkInsertValuesExtSync(DatabaseConstants.CLASSIFIER_TABLE, classifierValues); + bulkInsertValuesExtSync(DatabaseConstants.FEED_TABLE, feedValues); } - } - if (apiResponse.feedTags != null ) { - List feedTags = new ArrayList(apiResponse.feedTags.length); - for (String[] tuple : apiResponse.feedTags) { - // the API returns a list of lists, but all we care about is the tag name/id which is the first item in the tuple - if (tuple.length > 0) { - feedTags.add(tuple[0]); + + // handle story content + if (apiResponse.stories != null) { + storiesloop: for (Story story : apiResponse.stories) { + if ((story.storyHash == null) || (story.storyHash.length() < 1)) { + // this is incredibly rare, but has been seen in crash reports at least twice. + com.newsblur.util.Log.e(this, "story received without story hash: " + story.id); + continue storiesloop; + } + insertSingleStoryExtSync(story); + // if the story is being fetched for the immediate session, also add the hash to the session table + if (forImmediateReading && story.isStoryVisibleInState(stateFilter)) { + ContentValues sessionHashValues = new ContentValues(); + sessionHashValues.put(DatabaseConstants.READING_SESSION_STORY_HASH, story.storyHash); + dbRW.insert(DatabaseConstants.READING_SESSION_TABLE, null, sessionHashValues); + } + impliedFeedId = story.feedId; } } - putFeedTagsExtSync(impliedFeedId, feedTags); - } - if (apiResponse.feedAuthors != null ) { - List feedAuthors = new ArrayList(apiResponse.feedAuthors.length); - for (String[] tuple : apiResponse.feedAuthors) { - // the API returns a list of lists, but all we care about is the author name/id which is the first item in the tuple - if (tuple.length > 0) { - feedAuthors.add(tuple[0]); + if (apiResponse.story != null) { + if ((apiResponse.story.storyHash == null) || (apiResponse.story.storyHash.length() < 1)) { + com.newsblur.util.Log.e(this, "story received without story hash: " + apiResponse.story.id); + return; + } + insertSingleStoryExtSync(apiResponse.story); + impliedFeedId = apiResponse.story.feedId; + } + + // handle classifiers + if (apiResponse.classifiers != null) { + for (Map.Entry entry : apiResponse.classifiers.entrySet()) { + // the API might not have included a feed ID, in which case it deserialized as -1 and must be implied + String classifierFeedId = entry.getKey(); + if (classifierFeedId.equals("-1")) { + classifierFeedId = impliedFeedId; + } + List classifierValues = entry.getValue().getContentValues(); + for (ContentValues values : classifierValues) { + values.put(DatabaseConstants.CLASSIFIER_ID, classifierFeedId); + } + dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", new String[] { classifierFeedId }); + bulkInsertValuesExtSync(DatabaseConstants.CLASSIFIER_TABLE, classifierValues); } } - putFeedAuthorsExtSync(impliedFeedId, feedAuthors); + + if (apiResponse.feedTags != null ) { + List feedTags = new ArrayList(apiResponse.feedTags.length); + for (String[] tuple : apiResponse.feedTags) { + // the API returns a list of lists, but all we care about is the tag name/id which is the first item in the tuple + if (tuple.length > 0) { + feedTags.add(tuple[0]); + } + } + putFeedTagsExtSync(impliedFeedId, feedTags); + } + + if (apiResponse.feedAuthors != null ) { + List feedAuthors = new ArrayList(apiResponse.feedAuthors.length); + for (String[] tuple : apiResponse.feedAuthors) { + // the API returns a list of lists, but all we care about is the author name/id which is the first item in the tuple + if (tuple.length > 0) { + feedAuthors.add(tuple[0]); + } + } + putFeedAuthorsExtSync(impliedFeedId, feedAuthors); + } + + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); } } @@ -477,30 +504,33 @@ public class BlurDatabaseHelper { * API. Most social APIs vend an updated view that replaces any old or placeholder records. */ public void updateComment(CommentResponse apiResponse, String storyId) { - // comments often contain enclosed replies, so batch them. - dbRW.beginTransaction(); - try { - // the API might include new supplemental user metadata if new replies have shown up. - if (apiResponse.users != null) { - List userValues = new ArrayList(apiResponse.users.length); - for (UserProfile user : apiResponse.users) { - userValues.add(user.getValues()); + synchronized (RW_MUTEX) { + // comments often contain enclosed replies, so batch them. + dbRW.beginTransaction(); + try { + // the API might include new supplemental user metadata if new replies have shown up. + if (apiResponse.users != null) { + List userValues = new ArrayList(apiResponse.users.length); + for (UserProfile user : apiResponse.users) { + userValues.add(user.getValues()); + } + bulkInsertValuesExtSync(DatabaseConstants.USER_TABLE, userValues); } - bulkInsertValuesExtSync(DatabaseConstants.USER_TABLE, userValues); + + // we store all comments in the context of the associated story, but the social API doesn't + // reference the story when responding, so fix that from our context + apiResponse.comment.storyId = storyId; + insertSingleCommentExtSync(apiResponse.comment); + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } - // we store all comments in the context of the associated story, but the social API doesn't - // reference the story when responding, so fix that from our context - apiResponse.comment.storyId = storyId; - insertSingleCommentExtSync(apiResponse.comment); - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); } } public void fixMissingStoryFeeds(Story[] stories) { // start off with feeds mentioned by the set of stories - Set feedIds = new HashSet<>(); + Set feedIds = new HashSet(); for (Story story : stories) { feedIds.add(story.feedId); } @@ -513,7 +543,7 @@ public class BlurDatabaseHelper { } c.close(); // if any feeds are left, they are phantoms and need a fake entry - if (feedIds.isEmpty()) return; + if (feedIds.size() < 1) return; android.util.Log.i(this.getClass().getName(), "inserting missing metadata for " + feedIds.size() + " feeds used by new stories"); List feedValues = new ArrayList(feedIds.size()); for (String feedId : feedIds) { @@ -521,14 +551,16 @@ public class BlurDatabaseHelper { missingFeed.feedId = feedId; feedValues.add(missingFeed.getValues()); } - dbRW.beginTransaction(); - try { - for (ContentValues values : feedValues) { - dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, values, SQLiteDatabase.CONFLICT_IGNORE); + synchronized (RW_MUTEX) { + dbRW.beginTransaction(); + try { + for (ContentValues values : feedValues) { + dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); } } @@ -548,55 +580,61 @@ public class BlurDatabaseHelper { public void touchStory(String hash) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_LAST_READ_DATE, (new Date()).getTime()); - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_LAST_READ_DATE + " < 1 AND " + DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_LAST_READ_DATE + " < 1 AND " + DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} } public void markStoryHashesRead(Collection hashes) { - dbRW.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_READ, true); - for (String hash : hashes) { - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + synchronized (RW_MUTEX) { + dbRW.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_READ, true); + for (String hash : hashes) { + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); } } public void markStoryHashesStarred(Collection hashes, boolean isStarred) { - dbRW.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_STARRED, isStarred); - for (String hash : hashes) { - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + synchronized (RW_MUTEX) { + dbRW.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_STARRED, isStarred); + for (String hash : hashes) { + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); } } public void setFeedsActive(Set feedIds, boolean active) { - dbRW.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.FEED_ACTIVE, active); - for (String feedId : feedIds) { - dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); + synchronized (RW_MUTEX) { + dbRW.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.FEED_ACTIVE, active); + for (String feedId : feedIds) { + dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); + } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); } } public void setFeedFetchPending(String feedId) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.FEED_FETCH_PENDING, true); - dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} } public boolean isFeedSetFetchPending(FeedSet fs) { @@ -622,7 +660,7 @@ public class BlurDatabaseHelper { public void setStoryReadState(String hash, boolean read) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_READ, read); - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} } /** @@ -641,63 +679,65 @@ public class BlurDatabaseHelper { if (story.friendUserIds != null) { socialIds.addAll(Arrays.asList(story.friendUserIds)); } - if (!socialIds.isEmpty()) { + if (socialIds.size() > 0) { impactedFeeds.add(FeedSet.multipleSocialFeeds(socialIds)); } // check the story's starting state and the desired state and adjust it as an atom so we // know if it truly changed or not - dbRW.beginTransaction(); - try { - // get a fresh copy of the story from the DB so we know if it changed - Cursor c = dbRW.query(DatabaseConstants.STORY_TABLE, - new String[]{DatabaseConstants.STORY_READ}, - DatabaseConstants.STORY_HASH + " = ?", - new String[]{story.storyHash}, - null, null, null); - if (c.getCount() < 1) { - Log.w(this.getClass().getName(), "story removed before finishing mark-read"); - return impactedFeeds; - } - c.moveToFirst(); - boolean origState = (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.STORY_READ)) > 0); - c.close(); - // if there is nothing to be done, halt - if (origState == read) { - dbRW.setTransactionSuccessful(); - return impactedFeeds; - } - // update the story's read state - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_READ, read); - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{story.storyHash}); - // which column to inc/dec depends on story intel - String impactedCol; - String impactedSocialCol; - if (story.intelligence.calcTotalIntel() < 0) { - // negative stories don't affect counts - dbRW.setTransactionSuccessful(); - return impactedFeeds; - } else if (story.intelligence.calcTotalIntel() == 0 ) { - impactedCol = DatabaseConstants.FEED_NEUTRAL_COUNT; - impactedSocialCol = DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT; - } else { - impactedCol = DatabaseConstants.FEED_POSITIVE_COUNT; - impactedSocialCol = DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT; - } - String operator = (read ? " - 1" : " + 1"); - StringBuilder q = new StringBuilder("UPDATE " + DatabaseConstants.FEED_TABLE); - q.append(" SET ").append(impactedCol).append(" = ").append(impactedCol).append(operator); - q.append(" WHERE " + DatabaseConstants.FEED_ID + " = ").append(story.feedId); - dbRW.execSQL(q.toString()); - for (String socialId : socialIds) { - q = new StringBuilder("UPDATE " + DatabaseConstants.SOCIALFEED_TABLE); - q.append(" SET ").append(impactedSocialCol).append(" = ").append(impactedSocialCol).append(operator); - q.append(" WHERE " + DatabaseConstants.SOCIAL_FEED_ID + " = ").append(socialId); + synchronized (RW_MUTEX) { + dbRW.beginTransaction(); + try { + // get a fresh copy of the story from the DB so we know if it changed + Cursor c = dbRW.query(DatabaseConstants.STORY_TABLE, + new String[]{DatabaseConstants.STORY_READ}, + DatabaseConstants.STORY_HASH + " = ?", + new String[]{story.storyHash}, + null, null, null); + if (c.getCount() < 1) { + Log.w(this.getClass().getName(), "story removed before finishing mark-read"); + return impactedFeeds; + } + c.moveToFirst(); + boolean origState = (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.STORY_READ)) > 0); + c.close(); + // if there is nothing to be done, halt + if (origState == read) { + dbRW.setTransactionSuccessful(); + return impactedFeeds; + } + // update the story's read state + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_READ, read); + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{story.storyHash}); + // which column to inc/dec depends on story intel + String impactedCol; + String impactedSocialCol; + if (story.intelligence.calcTotalIntel() < 0) { + // negative stories don't affect counts + dbRW.setTransactionSuccessful(); + return impactedFeeds; + } else if (story.intelligence.calcTotalIntel() == 0 ) { + impactedCol = DatabaseConstants.FEED_NEUTRAL_COUNT; + impactedSocialCol = DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT; + } else { + impactedCol = DatabaseConstants.FEED_POSITIVE_COUNT; + impactedSocialCol = DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT; + } + String operator = (read ? " - 1" : " + 1"); + StringBuilder q = new StringBuilder("UPDATE " + DatabaseConstants.FEED_TABLE); + q.append(" SET ").append(impactedCol).append(" = ").append(impactedCol).append(operator); + q.append(" WHERE " + DatabaseConstants.FEED_ID + " = ").append(story.feedId); dbRW.execSQL(q.toString()); + for (String socialId : socialIds) { + q = new StringBuilder("UPDATE " + DatabaseConstants.SOCIALFEED_TABLE); + q.append(" SET ").append(impactedSocialCol).append(" = ").append(impactedSocialCol).append(operator); + q.append(" WHERE " + DatabaseConstants.SOCIAL_FEED_ID + " = ").append(socialId); + dbRW.execSQL(q.toString()); + } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); } return impactedFeeds; } @@ -728,7 +768,7 @@ public class BlurDatabaseHelper { } else { throw new IllegalStateException("Asked to mark stories for FeedSet of unknown type."); } - dbRW.update(DatabaseConstants.STORY_TABLE, values, conjoinSelections(feedSelection, rangeSelection), null); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, conjoinSelections(feedSelection, rangeSelection), null);} } /** @@ -790,11 +830,11 @@ public class BlurDatabaseHelper { } public void updateFeedCounts(String feedId, ContentValues values) { - dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} } public void updateSocialFeedCounts(String feedId, ContentValues values) { - dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{feedId}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{feedId});} } /** @@ -827,7 +867,7 @@ public class BlurDatabaseHelper { values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEG)); values.put(DatabaseConstants.FEED_NEUTRAL_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEUT)); values.put(DatabaseConstants.FEED_POSITIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.BEST)); - dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} } for (String socialId : socialFeedIds) { @@ -836,7 +876,7 @@ public class BlurDatabaseHelper { values.put(DatabaseConstants.SOCIAL_FEED_NEGATIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEG)); values.put(DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEUT)); values.put(DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.BEST)); - dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{socialId}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{socialId});} } } @@ -857,11 +897,11 @@ public class BlurDatabaseHelper { public void clearInfrequentSession() { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_INFREQUENT, false); - dbRW.update(DatabaseConstants.STORY_TABLE, values, null, null); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, null, null);} } public void enqueueAction(ReadingAction ra) { - dbRW.insertOrThrow(DatabaseConstants.ACTION_TABLE, null, ra.toContentValues()); + synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.ACTION_TABLE, null, ra.toContentValues());} } public Cursor getActions() { @@ -870,10 +910,12 @@ public class BlurDatabaseHelper { } public void incrementActionTried(String actionId) { - String q = "UPDATE " + DatabaseConstants.ACTION_TABLE + - " SET " + DatabaseConstants.ACTION_TRIED + " = " + DatabaseConstants.ACTION_TRIED + " + 1" + - " WHERE " + DatabaseConstants.ACTION_ID + " = ?"; - dbRW.execSQL(q, new String[]{actionId}); + synchronized (RW_MUTEX) { + String q = "UPDATE " + DatabaseConstants.ACTION_TABLE + + " SET " + DatabaseConstants.ACTION_TRIED + " = " + DatabaseConstants.ACTION_TRIED + " + 1" + + " WHERE " + DatabaseConstants.ACTION_ID + " = ?"; + dbRW.execSQL(q, new String[]{actionId}); + } } public int getUntriedActionCount() { @@ -885,52 +927,54 @@ public class BlurDatabaseHelper { } public void clearAction(String actionId) { - dbRW.delete(DatabaseConstants.ACTION_TABLE, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId}); + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.ACTION_TABLE, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId});} } public void setStoryStarred(String hash, @Nullable List userTags, boolean starred) { // check the story's starting state and the desired state and adjust it as an atom so we // know if it truly changed or not and thus whether to update counts - dbRW.beginTransaction(); - try { - // get a fresh copy of the story from the DB so we know if it changed - Cursor c = dbRW.query(DatabaseConstants.STORY_TABLE, - new String[]{DatabaseConstants.STORY_STARRED}, - DatabaseConstants.STORY_HASH + " = ?", - new String[]{hash}, - null, null, null); - if (c.getCount() < 1) { - Log.w(this.getClass().getName(), "story removed before finishing mark-starred"); - return; - } - c.moveToFirst(); - boolean origState = (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.STORY_STARRED)) > 0); - c.close(); - // if already stared, update user tags - if (origState == starred && starred && userTags != null) { + synchronized (RW_MUTEX) { + dbRW.beginTransaction(); + try { + // get a fresh copy of the story from the DB so we know if it changed + Cursor c = dbRW.query(DatabaseConstants.STORY_TABLE, + new String[]{DatabaseConstants.STORY_STARRED}, + DatabaseConstants.STORY_HASH + " = ?", + new String[]{hash}, + null, null, null); + if (c.getCount() < 1) { + Log.w(this.getClass().getName(), "story removed before finishing mark-starred"); + return; + } + c.moveToFirst(); + boolean origState = (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.STORY_STARRED)) > 0); + c.close(); + // if already stared, update user tags + if (origState == starred && starred && userTags != null) { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_USER_TAGS, TextUtils.join(",", userTags)); + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + return; + } + // if there is nothing to be done, halt + else if (origState == starred) { + return; + } + // fix the state ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_USER_TAGS, TextUtils.join(",", userTags)); + values.put(DatabaseConstants.STORY_STARRED, starred); dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); - return; + // adjust counts + String operator = (starred ? " + 1" : " - 1"); + StringBuilder q = new StringBuilder("UPDATE " + DatabaseConstants.STARREDCOUNTS_TABLE); + q.append(" SET " + DatabaseConstants.STARREDCOUNTS_COUNT + " = " + DatabaseConstants.STARREDCOUNTS_COUNT).append(operator); + q.append(" WHERE " + DatabaseConstants.STARREDCOUNTS_TAG + " = '" + StarredCount.TOTAL_STARRED + "'"); + // TODO: adjust counts per feed (and tags?) + dbRW.execSQL(q.toString()); + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); } - // if there is nothing to be done, halt - else if (origState == starred) { - return; - } - // fix the state - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_STARRED, starred); - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); - // adjust counts - String operator = (starred ? " + 1" : " - 1"); - StringBuilder q = new StringBuilder("UPDATE " + DatabaseConstants.STARREDCOUNTS_TABLE); - q.append(" SET " + DatabaseConstants.STARREDCOUNTS_COUNT + " = " + DatabaseConstants.STARREDCOUNTS_COUNT).append(operator); - q.append(" WHERE " + DatabaseConstants.STARREDCOUNTS_TAG + " = '" + StarredCount.TOTAL_STARRED + "'"); - // TODO: adjust counts per feed (and tags?) - dbRW.execSQL(q.toString()); - dbRW.setTransactionSuccessful(); - } finally { - dbRW.endTransaction(); } } @@ -960,7 +1004,7 @@ public class BlurDatabaseHelper { } ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_SHARED_USER_IDS, TextUtils.join(",", newIds)); - dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} } public String getStoryText(String hash) { @@ -1000,7 +1044,7 @@ public class BlurDatabaseHelper { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_TEXT_STORY_HASH, hash); values.put(DatabaseConstants.STORY_TEXT_STORY_TEXT, text); - dbRW.insertOrThrow(DatabaseConstants.STORY_TEXT_TABLE, null, values); + synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.STORY_TEXT_TABLE, null, values);} } public Cursor getSocialFeedsCursor(CancellationSignal cancellationSignal) { @@ -1130,7 +1174,7 @@ public class BlurDatabaseHelper { public void clearStorySession() { com.newsblur.util.Log.i(this, "reading session reset"); - dbRW.delete(DatabaseConstants.READING_SESSION_TABLE, null, null); + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.READING_SESSION_TABLE, null, null);} } /** @@ -1151,7 +1195,7 @@ public class BlurDatabaseHelper { q.append(" (" + DatabaseConstants.READING_SESSION_STORY_HASH + ") "); q.append(sel); - dbRW.execSQL(q.toString(), selArgs.toArray(new String[0])); + synchronized (RW_MUTEX) {dbRW.execSQL(q.toString(), selArgs.toArray(new String[0]));} } /** @@ -1241,17 +1285,17 @@ public class BlurDatabaseHelper { public void setSessionFeedSet(FeedSet fs) { if (fs == null) { - dbRW.delete(DatabaseConstants.SYNC_METADATA_TABLE, DatabaseConstants.SYNC_METADATA_KEY + " = ?", new String[] {DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET}); + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SYNC_METADATA_TABLE, DatabaseConstants.SYNC_METADATA_KEY + " = ?", new String[] {DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET});} } else { ContentValues values = new ContentValues(); values.put(DatabaseConstants.SYNC_METADATA_KEY, DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET); values.put(DatabaseConstants.SYNC_METADATA_VALUE, fs.toCompactSerial()); - dbRW.insertWithOnConflict(DatabaseConstants.SYNC_METADATA_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE); + synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.SYNC_METADATA_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);} } } public FeedSet getSessionFeedSet() { - FeedSet fs; + FeedSet fs = null; Cursor c = dbRO.query(DatabaseConstants.SYNC_METADATA_TABLE, null, DatabaseConstants.SYNC_METADATA_KEY + " = ?", new String[] {DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET}, null, null, null, null); if (c.getCount() < 1) return null; c.moveToFirst(); @@ -1266,7 +1310,7 @@ public class BlurDatabaseHelper { public void clearClassifiersForFeed(String feedId) { String[] selArgs = new String[] {feedId}; - dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs); + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs);} } public void insertClassifier(Classifier classifier) { @@ -1321,29 +1365,31 @@ public class BlurDatabaseHelper { if (TextUtils.isEmpty(commentText)) { comment.isPseudo = true; } - // in order to make this method idempotent (so it can be attempted before, during, or after - // the real comment is done, we have to check for a real one - if (getComment(storyId, userId) != null) { - com.newsblur.util.Log.i(this.getClass().getName(), "electing not to insert placeholder comment over live one"); - return; + synchronized (RW_MUTEX) { + // in order to make this method idempotent (so it can be attempted before, during, or after + // the real comment is done, we have to check for a real one + if (getComment(storyId, userId) != null) { + com.newsblur.util.Log.i(this.getClass().getName(), "electing not to insert placeholder comment over live one"); + return; + } + dbRW.insertWithOnConflict(DatabaseConstants.COMMENT_TABLE, null, comment.getValues(), SQLiteDatabase.CONFLICT_REPLACE); } - dbRW.insertWithOnConflict(DatabaseConstants.COMMENT_TABLE, null, comment.getValues(), SQLiteDatabase.CONFLICT_REPLACE); } public void editReply(String replyId, String replyText) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.REPLY_TEXT, replyText); - dbRW.update(DatabaseConstants.REPLY_TABLE, values, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.REPLY_TABLE, values, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId});} } public void deleteReply(String replyId) { - dbRW.delete(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId}); + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId});} } public void clearSelfComments(String storyId, @Nullable String userId) { - dbRW.delete(DatabaseConstants.COMMENT_TABLE, - DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?", - new String[]{storyId, userId}); + synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.COMMENT_TABLE, + DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?", + new String[]{storyId, userId});} } public void setCommentLiked(String storyId, String commentUserId, @Nullable String currentUserId, boolean liked) { @@ -1372,7 +1418,7 @@ public class BlurDatabaseHelper { } ContentValues values = new ContentValues(); values.put(DatabaseConstants.COMMENT_LIKING_USERS, TextUtils.join(",", newIds)); - dbRW.update(DatabaseConstants.COMMENT_TABLE, values, DatabaseConstants.COMMENT_ID + " = ?", new String[]{comment.id}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.COMMENT_TABLE, values, DatabaseConstants.COMMENT_ID + " = ?", new String[]{comment.id});} } public UserProfile getUserProfile(String userId) { @@ -1418,14 +1464,14 @@ public class BlurDatabaseHelper { reply.userId = userId; reply.date = new Date(); reply.id = Reply.PLACEHOLDER_COMMENT_ID + storyId + comment.id + reply.userId; - dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE); + synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE);} } public void putStoryDismissed(String storyHash) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.NOTIFY_DISMISS_STORY_HASH, storyHash); values.put(DatabaseConstants.NOTIFY_DISMISS_TIME, Calendar.getInstance().getTime().getTime()); - dbRW.insertOrThrow(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, values); + synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, values);} } public boolean isStoryDismissed(String storyHash) { @@ -1440,10 +1486,12 @@ public class BlurDatabaseHelper { public void cleanupDismissals() { Calendar cutoffDate = Calendar.getInstance(); cutoffDate.add(Calendar.MONTH, -1); - int count = dbRW.delete(DatabaseConstants.NOTIFY_DISMISS_TABLE, - DatabaseConstants.NOTIFY_DISMISS_TIME + " < ?", - new String[]{Long.toString(cutoffDate.getTime().getTime())}); - com.newsblur.util.Log.d(this.getClass().getName(), "cleaned up dismissals: " + count); + synchronized (RW_MUTEX) { + int count = dbRW.delete(DatabaseConstants.NOTIFY_DISMISS_TABLE, + DatabaseConstants.NOTIFY_DISMISS_TIME + " < ?", + new String[]{Long.toString(cutoffDate.getTime().getTime())}); + com.newsblur.util.Log.d(this.getClass().getName(), "cleaned up dismissals: " + count); + } } private void putFeedTagsExtSync(String feedId, Collection tags) { @@ -1513,7 +1561,7 @@ public class BlurDatabaseHelper { public void renameFeed(String feedId, String newFeedName) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.FEED_TITLE, newFeedName); - dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId}); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} } public static void closeQuietly(Cursor c) { From ca30fc88bcf543ab88a07ef5f57078a57ee8ae45 Mon Sep 17 00:00:00 2001 From: sictiru Date: Tue, 9 Jul 2024 13:44:41 -0700 Subject: [PATCH 14/22] Android v13.2.11 Revert db mutex changes --- clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index e6b30b17e..99f490af9 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 24 const val targetSdk = 34 - const val versionCode = 226 - const val versionName = "13.2.10" + const val versionCode = 227 + const val versionName = "13.2.11" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" From 749f842ccba3f0190915c4d68f9b1b36b5217767 Mon Sep 17 00:00:00 2001 From: sictiru Date: Thu, 11 Jul 2024 09:04:22 -0700 Subject: [PATCH 15/22] Android v13.3.0 --- .../main/java/com/newsblur/database/BlurDatabaseHelper.java | 4 ++-- clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java index 0bc123d80..bbffc2227 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java @@ -46,13 +46,13 @@ import java.util.concurrent.Executors; /** * Utility class for executing DB operations on the local, private NB database. - * * It is the intent of this class to be the single location of SQL executed on * our DB, replacing the deprecated ContentProvider access pattern. */ public class BlurDatabaseHelper { - // manual synchro isn't needed if you only use one DBHelper, but at present the app uses several + // Removing the manual synchro will cause ANRs + // because the db transactions are made on the main thread public final static Object RW_MUTEX = new Object(); private final BlurDatabase dbWrapper; diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index 99f490af9..912d7c0b4 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 24 const val targetSdk = 34 - const val versionCode = 227 - const val versionName = "13.2.11" + const val versionCode = 228 + const val versionName = "13.3.0" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" From 398fd91ad9c4e8bb32535e2a36d310f4def2e2b7 Mon Sep 17 00:00:00 2001 From: sictiru Date: Thu, 11 Jul 2024 09:08:57 -0700 Subject: [PATCH 16/22] Revert binding view destroy --- .../com/newsblur/fragment/ReadingItemFragment.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt index f64736d1f..8472c3572 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt @@ -122,10 +122,8 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { private var savedScrollPosRel = 0f private val webViewContentMutex = Any() - private var _binding: FragmentReadingitemBinding? = null - private var _readingItemActionsBinding: ReadingItemActionsBinding? = null - private val binding get() = _binding!! - private val readingItemActionsBinding get() = _readingItemActionsBinding!! + private lateinit var binding: FragmentReadingitemBinding + private lateinit var readingItemActionsBinding: ReadingItemActionsBinding private lateinit var markStoryReadBehavior: MarkStoryReadBehavior @@ -166,9 +164,6 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { override fun onDestroyView() { sampledQueue?.close() - binding.readingWebview.webChromeClient = null - _readingItemActionsBinding = null - _binding = null super.onDestroyView() } @@ -186,8 +181,8 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - _binding = FragmentReadingitemBinding.inflate(inflater, container, false) - _readingItemActionsBinding = ReadingItemActionsBinding.bind(binding.root) + binding = FragmentReadingitemBinding.inflate(inflater, container, false) + readingItemActionsBinding = ReadingItemActionsBinding.bind(binding.root) val readingActivity = requireActivity() as Reading fs = readingActivity.fs From ba1264f2b8d25fa17885aef19c7e21e6997b34b5 Mon Sep 17 00:00:00 2001 From: sictiru Date: Fri, 30 Aug 2024 14:40:49 -0700 Subject: [PATCH 17/22] Add nullability annotation to help with Kotlin conversion --- .../java/com/newsblur/activity/Reading.kt | 2 +- .../newsblur/database/BlurDatabaseHelper.java | 225 ++++++++++-------- .../newsblur/fragment/ReadingItemFragment.kt | 6 +- .../fragment/SetupCommentSectionTask.kt | 2 +- 4 files changed, 130 insertions(+), 105 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt index b92867283..68ec86296 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt @@ -388,7 +388,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene get() { // saved stories and global shared stories don't have unreads if (fs!!.isAllSaved || fs!!.isGlobalShared) return 0 - val result = dbHelper.getUnreadCount(fs, intelState) + val result = dbHelper.getUnreadCount(fs!!, intelState) return if (result < 0) 0 else result } diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java index bbffc2227..1ad6e3455 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java @@ -7,6 +7,8 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.CancellationSignal; + +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.text.TextUtils; @@ -85,6 +87,7 @@ public class BlurDatabaseHelper { com.newsblur.util.Log.i(this.getClass().getName(), ". . . tables recreated."); } + @Nullable public String getEngineVersion() { String engineVersion = ""; try { @@ -99,10 +102,12 @@ public class BlurDatabaseHelper { return engineVersion; } + @NonNull public Set getAllFeeds() { return getAllFeeds(false); } + @NonNull private Set getAllFeeds(boolean activeOnly) { String q1 = "SELECT " + DatabaseConstants.FEED_ID + " FROM " + DatabaseConstants.FEED_TABLE; @@ -118,10 +123,12 @@ public class BlurDatabaseHelper { return feedIds; } + @NonNull public Set getAllActiveFeeds() { return getAllFeeds(true); } + @NonNull private List getAllSocialFeeds() { String q1 = "SELECT " + DatabaseConstants.SOCIAL_FEED_ID + " FROM " + DatabaseConstants.SOCIALFEED_TABLE; @@ -181,20 +188,20 @@ public class BlurDatabaseHelper { synchronized (RW_MUTEX) {dbRW.execSQL("VACUUM");} } - public void deleteFeed(String feedId) { + public void deleteFeed(@Nullable String feedId) { String[] selArgs = new String[] {feedId}; synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.FEED_TABLE, DatabaseConstants.FEED_ID + " = ?", selArgs);} synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs);} } - public void deleteSocialFeed(String userId) { + public void deleteSocialFeed(@Nullable String userId) { String[] selArgs = new String[] {userId}; synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, DatabaseConstants.SOCIAL_FEED_ID + " = ?", selArgs);} synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs);} synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, DatabaseConstants.SOCIALFEED_STORY_USER_ID + " = ?", selArgs);} } - public void deleteSavedSearch(String feedId, String query) { + public void deleteSavedSearch(@Nullable String feedId, @Nullable String query) { String q = "DELETE FROM " + DatabaseConstants.SAVED_SEARCH_TABLE + " WHERE " + DatabaseConstants.SAVED_SEARCH_FEED_ID + " = '" + feedId + "'" + " AND " + DatabaseConstants.SAVED_SEARCH_QUERY + " = '" + query + "'"; @@ -207,7 +214,8 @@ public class BlurDatabaseHelper { synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TEXT_TABLE, null, null);} } - public Feed getFeed(String feedId) { + @Nullable + public Feed getFeed(@Nullable String feedId) { Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE, null, DatabaseConstants.FEED_ID + " = ?", new String[] {feedId}, null, null, null); Feed result = null; while (c.moveToNext()) { @@ -217,13 +225,13 @@ public class BlurDatabaseHelper { return result; } - public void updateFeed(Feed feed) { + public void updateFeed(@NonNull Feed feed) { synchronized (RW_MUTEX) { dbRW.insertWithOnConflict(DatabaseConstants.FEED_TABLE, null, feed.getValues(), SQLiteDatabase.CONFLICT_REPLACE); } } - private void bulkInsertValues(String table, List valuesList) { + private void bulkInsertValues(@NonNull String table, @NonNull List valuesList) { if (valuesList.size() < 1) return; synchronized (RW_MUTEX) { dbRW.beginTransaction(); @@ -239,18 +247,18 @@ public class BlurDatabaseHelper { } // just like bulkInsertValues, but leaves sync/transactioning to the caller - private void bulkInsertValuesExtSync(String table, List valuesList) { + private void bulkInsertValuesExtSync(@NonNull String table, @NonNull List valuesList) { if (valuesList.size() < 1) return; for (ContentValues values : valuesList) { dbRW.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); } } - public void setFeedsFolders(List folderValues, - List feedValues, - List socialFeedValues, - List starredCountValues, - List savedSearchValues) { + public void setFeedsFolders(@NonNull List folderValues, + @NonNull List feedValues, + @NonNull List socialFeedValues, + @NonNull List starredCountValues, + @NonNull List savedSearchValues) { synchronized (RW_MUTEX) { dbRW.beginTransaction(); try { @@ -276,6 +284,7 @@ public class BlurDatabaseHelper { // note method name: this gets a set rather than a list, in case the caller wants to // spend the up-front cost of hashing for better lookup speed rather than iteration! + @NonNull public Set getUnreadStoryHashesAsSet() { String q = "SELECT " + DatabaseConstants.STORY_HASH + " FROM " + DatabaseConstants.STORY_TABLE + @@ -289,6 +298,7 @@ public class BlurDatabaseHelper { return hashes; } + @NonNull public Set getStarredStoryHashes() { String q = "SELECT " + DatabaseConstants.STORY_HASH + " FROM " + DatabaseConstants.STORY_TABLE + @@ -302,6 +312,7 @@ public class BlurDatabaseHelper { return hashes; } + @NonNull public Set getAllStoryImages() { Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_IMAGE_URLS}, null, null, null, null, null); Set urls = new HashSet(c.getCount()); @@ -312,6 +323,7 @@ public class BlurDatabaseHelper { return urls; } + @NonNull public Set getAllStoryThumbnails() { Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_THUMBNAIL_URL}, null, null, null, null, null); Set urls = new HashSet(c.getCount()); @@ -325,7 +337,7 @@ public class BlurDatabaseHelper { return urls; } - public void insertStories(StoriesResponse apiResponse, StateFilter stateFilter, boolean forImmediateReading) { + public void insertStories(@NonNull StoriesResponse apiResponse, @NonNull StateFilter stateFilter, boolean forImmediateReading) { synchronized (RW_MUTEX) { // do not attempt to use beginTransactionNonExclusive() to reduce lock time for this very heavy set // of calls. most versions of Android incorrectly implement the underlying SQLite calls and will @@ -428,7 +440,7 @@ public class BlurDatabaseHelper { } } - private void insertSingleStoryExtSync(Story story) { + private void insertSingleStoryExtSync(@NonNull Story story) { // pick a thumbnail for the story story.thumbnailUrl = Story.guessStoryThumbnailURL(story); // insert the story data @@ -459,7 +471,7 @@ public class BlurDatabaseHelper { } } - private void insertSingleCommentExtSync(Comment comment) { + private void insertSingleCommentExtSync(@NonNull Comment comment) { // real comments replace placeholders int count = dbRW.delete(DatabaseConstants.COMMENT_TABLE, DatabaseConstants.COMMENT_ISPLACEHOLDER + " = ?", new String[]{"true"}); // comments always come with an updated set of replies, so remove old ones first @@ -477,7 +489,7 @@ public class BlurDatabaseHelper { * to reflect a social action, but that the new copy is missing some fields. Attempt to merge the * new story with the old one. */ - public void updateStory(StoriesResponse apiResponse, StateFilter stateFilter, boolean forImmediateReading) { + public void updateStory(@NonNull StoriesResponse apiResponse, @NonNull StateFilter stateFilter, boolean forImmediateReading) { if (apiResponse.story == null) { com.newsblur.util.Log.e(this, "updateStory called on response with missing single story"); return; @@ -503,7 +515,7 @@ public class BlurDatabaseHelper { * Update an existing comment and associated replies based upon a new copy received from a social * API. Most social APIs vend an updated view that replaces any old or placeholder records. */ - public void updateComment(CommentResponse apiResponse, String storyId) { + public void updateComment(@NonNull CommentResponse apiResponse, @Nullable String storyId) { synchronized (RW_MUTEX) { // comments often contain enclosed replies, so batch them. dbRW.beginTransaction(); @@ -528,7 +540,7 @@ public class BlurDatabaseHelper { } } - public void fixMissingStoryFeeds(Story[] stories) { + public void fixMissingStoryFeeds(@Nullable Story[] stories) { // start off with feeds mentioned by the set of stories Set feedIds = new HashSet(); for (Story story : stories) { @@ -564,7 +576,8 @@ public class BlurDatabaseHelper { } } - public Folder getFolder(String folderName) { + @NonNull + public Folder getFolder(@NonNull String folderName) { String[] selArgs = new String[] {folderName}; String selection = DatabaseConstants.FOLDER_NAME + " = ?"; Cursor c = dbRO.query(DatabaseConstants.FOLDER_TABLE, null, selection, selArgs, null, null, null); @@ -577,13 +590,13 @@ public class BlurDatabaseHelper { return folder; } - public void touchStory(String hash) { + public void touchStory(@Nullable String hash) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_LAST_READ_DATE, (new Date()).getTime()); synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_LAST_READ_DATE + " < 1 AND " + DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} } - public void markStoryHashesRead(Collection hashes) { + public void markStoryHashesRead(@NonNull Collection hashes) { synchronized (RW_MUTEX) { dbRW.beginTransaction(); try { @@ -599,7 +612,7 @@ public class BlurDatabaseHelper { } } - public void markStoryHashesStarred(Collection hashes, boolean isStarred) { + public void markStoryHashesStarred(@NonNull Collection hashes, boolean isStarred) { synchronized (RW_MUTEX) { dbRW.beginTransaction(); try { @@ -615,7 +628,7 @@ public class BlurDatabaseHelper { } } - public void setFeedsActive(Set feedIds, boolean active) { + public void setFeedsActive(@NonNull Set feedIds, boolean active) { synchronized (RW_MUTEX) { dbRW.beginTransaction(); try { @@ -631,13 +644,13 @@ public class BlurDatabaseHelper { } } - public void setFeedFetchPending(String feedId) { + public void setFeedFetchPending(@NonNull String feedId) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.FEED_FETCH_PENDING, true); synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} } - public boolean isFeedSetFetchPending(FeedSet fs) { + public boolean isFeedSetFetchPending(@NonNull FeedSet fs) { if (fs.getSingleFeed() != null) { String feedId = fs.getSingleFeed(); Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE, @@ -657,7 +670,7 @@ public class BlurDatabaseHelper { /** * Marks a story (un)read but does not adjust counts. Must stay idempotent an time-insensitive. */ - public void setStoryReadState(String hash, boolean read) { + public void setStoryReadState(@Nullable String hash, boolean read) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_READ, read); synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} @@ -668,7 +681,8 @@ public class BlurDatabaseHelper { * * @return the set of feed IDs that potentially have counts impacted by the mark. */ - public Set setStoryReadState(Story story, boolean read) { + @NonNull + public Set setStoryReadState(@NonNull Story story, boolean read) { // calculate the impact surface so the caller can re-check counts if needed Set impactedFeeds = new HashSet(); impactedFeeds.add(FeedSet.singleFeed(story.feedId)); @@ -746,7 +760,7 @@ public class BlurDatabaseHelper { * Marks a range of stories in a subset of feeds as read. Does not update unread counts; * the caller must use updateLocalFeedCounts() or the /reader/feed_unread_count API. */ - public void markStoriesRead(FeedSet fs, Long olderThan, Long newerThan) { + public void markStoriesRead(@NonNull FeedSet fs, @Nullable Long olderThan, @Nullable Long newerThan) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_READ, true); String rangeSelection = null; @@ -774,7 +788,7 @@ public class BlurDatabaseHelper { /** * Get the unread count for the given feedset based on the totals in the feeds table. */ - public int getUnreadCount(FeedSet fs, StateFilter stateFilter) { + public int getUnreadCount(@NonNull FeedSet fs, @NonNull StateFilter stateFilter) { // if reading in starred-only mode, there are no unreads, since stories vended as starred are never unread if (fs.isFilterSaved()) return 0; if (fs.isAllNormal()) { @@ -802,7 +816,7 @@ public class BlurDatabaseHelper { } } - private int getFeedsUnreadCount(StateFilter stateFilter, String selection, String[] selArgs) { + private int getFeedsUnreadCount(@NonNull StateFilter stateFilter, @Nullable String selection, @Nullable String[] selArgs) { int result = 0; Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE, null, selection, selArgs, null, null, null); while (c.moveToNext()) { @@ -816,7 +830,7 @@ public class BlurDatabaseHelper { return result; } - private int getSocialFeedsUnreadCount(StateFilter stateFilter, String selection, String[] selArgs) { + private int getSocialFeedsUnreadCount(@NonNull StateFilter stateFilter, @NonNull String selection, @Nullable String[] selArgs) { int result = 0; Cursor c = dbRO.query(DatabaseConstants.SOCIALFEED_TABLE, null, selection, selArgs, null, null, null); while (c.moveToNext()) { @@ -829,18 +843,18 @@ public class BlurDatabaseHelper { return result; } - public void updateFeedCounts(String feedId, ContentValues values) { + public void updateFeedCounts(@Nullable String feedId, @Nullable ContentValues values) { synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} } - public void updateSocialFeedCounts(String feedId, ContentValues values) { + public void updateSocialFeedCounts(@Nullable String feedId, @Nullable ContentValues values) { synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{feedId});} } /** * Refreshes the counts in the feeds/socialfeeds tables by counting stories in the story table. */ - public void updateLocalFeedCounts(FeedSet fs) { + public void updateLocalFeedCounts(@NonNull FeedSet fs) { // decompose the FeedSet into a list of single feeds that need to be recounted List feedIds = new ArrayList(); List socialFeedIds = new ArrayList(); @@ -883,7 +897,7 @@ public class BlurDatabaseHelper { /** * Get the unread count for the given feedset based on local story state. */ - public int getLocalUnreadCount(FeedSet fs, StateFilter stateFilter) { + public int getLocalUnreadCount(@NonNull FeedSet fs, @NonNull StateFilter stateFilter) { StringBuilder sel = new StringBuilder(); ArrayList selArgs = new ArrayList(); getLocalStorySelectionAndArgs(sel, selArgs, fs, stateFilter, ReadFilter.UNREAD); @@ -900,16 +914,17 @@ public class BlurDatabaseHelper { synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, null, null);} } - public void enqueueAction(ReadingAction ra) { + public void enqueueAction(@NonNull ReadingAction ra) { synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.ACTION_TABLE, null, ra.toContentValues());} } + @NonNull public Cursor getActions() { String q = "SELECT * FROM " + DatabaseConstants.ACTION_TABLE; return dbRO.rawQuery(q, null); } - public void incrementActionTried(String actionId) { + public void incrementActionTried(@Nullable String actionId) { synchronized (RW_MUTEX) { String q = "UPDATE " + DatabaseConstants.ACTION_TABLE + " SET " + DatabaseConstants.ACTION_TRIED + " = " + DatabaseConstants.ACTION_TRIED + " + 1" + @@ -926,7 +941,7 @@ public class BlurDatabaseHelper { return result; } - public void clearAction(String actionId) { + public void clearAction(@Nullable String actionId) { synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.ACTION_TABLE, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId});} } @@ -978,7 +993,7 @@ public class BlurDatabaseHelper { } } - public void setStoryShared(String hash, @Nullable String currentUserId, boolean shared) { + public void setStoryShared(@Nullable String hash, @Nullable String currentUserId, boolean shared) { // get a fresh copy of the story from the DB so we can append to the shared ID set Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_SHARED_USER_IDS}, @@ -1007,7 +1022,8 @@ public class BlurDatabaseHelper { synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} } - public String getStoryText(String hash) { + @Nullable + public String getStoryText(@Nullable String hash) { String q = "SELECT " + DatabaseConstants.STORY_TEXT_STORY_TEXT + " FROM " + DatabaseConstants.STORY_TEXT_TABLE + " WHERE " + DatabaseConstants.STORY_TEXT_STORY_HASH + " = ?"; @@ -1023,7 +1039,8 @@ public class BlurDatabaseHelper { } } - public String getStoryContent(String hash) { + @Nullable + public String getStoryContent(@Nullable String hash) { String q = "SELECT " + DatabaseConstants.STORY_CONTENT + " FROM " + DatabaseConstants.STORY_TABLE + " WHERE " + DatabaseConstants.STORY_HASH + " = ?"; @@ -1040,18 +1057,20 @@ public class BlurDatabaseHelper { } } - public void putStoryText(String hash, String text) { + public void putStoryText(@Nullable String hash, @NonNull String text) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_TEXT_STORY_HASH, hash); values.put(DatabaseConstants.STORY_TEXT_STORY_TEXT, text); synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.STORY_TEXT_TABLE, null, values);} } - public Cursor getSocialFeedsCursor(CancellationSignal cancellationSignal) { + @NonNull + public Cursor getSocialFeedsCursor(@NonNull CancellationSignal cancellationSignal) { return query(false, DatabaseConstants.SOCIALFEED_TABLE, null, null, null, null, null, "UPPER(" + DatabaseConstants.SOCIAL_FEED_TITLE + ") ASC", null, cancellationSignal); } - public SocialFeed getSocialFeed(String feedId) { + @Nullable + public SocialFeed getSocialFeed(@Nullable String feedId) { Cursor c = dbRO.query(DatabaseConstants.SOCIALFEED_TABLE, null, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[] {feedId}, null, null, null); SocialFeed result = null; while (c.moveToNext()) { @@ -1062,7 +1081,7 @@ public class BlurDatabaseHelper { } @Nullable - public StarredCount getStarredFeedByTag(String tag) { + public StarredCount getStarredFeedByTag(@NonNull String tag) { Cursor c = dbRO.query(DatabaseConstants.STARREDCOUNTS_TABLE, null, DatabaseConstants.STARREDCOUNTS_TAG + " = ?", new String[] {tag}, null, null, null); StarredCount result = null; while (c.moveToNext()) { @@ -1082,30 +1101,37 @@ public class BlurDatabaseHelper { return folders; } - public Cursor getFoldersCursor(CancellationSignal cancellationSignal) { + @NonNull + public Cursor getFoldersCursor(@Nullable CancellationSignal cancellationSignal) { return query(false, DatabaseConstants.FOLDER_TABLE, null, null, null, null, null, null, null, cancellationSignal); } - public Cursor getFeedsCursor(CancellationSignal cancellationSignal) { + @NonNull + public Cursor getFeedsCursor(@NonNull CancellationSignal cancellationSignal) { return query(false, DatabaseConstants.FEED_TABLE, null, null, null, null, null, "UPPER(" + DatabaseConstants.FEED_TITLE + ") ASC", null, cancellationSignal); } - public Cursor getSavedStoryCountsCursor(CancellationSignal cancellationSignal) { + @NonNull + public Cursor getSavedStoryCountsCursor(@NonNull CancellationSignal cancellationSignal) { return query(false, DatabaseConstants.STARREDCOUNTS_TABLE, null, null, null, null, null, null, null, cancellationSignal); } - public Cursor getSavedSearchCursor(CancellationSignal cancellationSignal) { + @NonNull + public Cursor getSavedSearchCursor(@NonNull CancellationSignal cancellationSignal) { return query(false, DatabaseConstants.SAVED_SEARCH_TABLE, null, null, null, null, null, null, null, cancellationSignal); } + @Nullable public Cursor getNotifyFocusStoriesCursor() { return rawQuery(DatabaseConstants.NOTIFY_FOCUS_STORY_QUERY, null, null); } + @Nullable public Cursor getNotifyUnreadStoriesCursor() { return rawQuery(DatabaseConstants.NOTIFY_UNREAD_STORY_QUERY, null, null); } + @NonNull public Set getNotifyFeeds() { String q = "SELECT " + DatabaseConstants.FEED_ID + " FROM " + DatabaseConstants.FEED_TABLE + " WHERE " + DatabaseConstants.FEED_NOTIFICATION_FILTER + " = '" + Feed.NOTIFY_FILTER_FOCUS + "'" + @@ -1122,25 +1148,8 @@ public class BlurDatabaseHelper { return feedIds; } - private Cursor getStoriesCursor(@Nullable FeedSet fs, CancellationSignal cancellationSignal) { - StringBuilder q = new StringBuilder(DatabaseConstants.STORY_QUERY_BASE_0); - - if (fs != null && !TextUtils.isEmpty(fs.getSingleFeed())) { - q.append(DatabaseConstants.STORY_FEED_ID); - q.append(" = "); - q.append(fs.getSingleFeed()); - } else { - q.append(DatabaseConstants.FEED_ACTIVE); - q.append(" = 1"); - } - - q.append(" ORDER BY "); - q.append(DatabaseConstants.STORY_TIMESTAMP); - q.append(" DESC LIMIT 20"); - return rawQuery(q.toString(), null, cancellationSignal); - } - - public Cursor getActiveStoriesCursor(FeedSet fs, CursorFilters cursorFilters, CancellationSignal cancellationSignal) { + @NonNull + public Cursor getActiveStoriesCursor(@NonNull FeedSet fs, @NonNull CursorFilters cursorFilters, @NonNull CancellationSignal cancellationSignal) { // get the stories for this FS Cursor result = getActiveStoriesCursorNoPrep(fs, cursorFilters.getStoryOrder(), cancellationSignal); // if the result is blank, try to prime the session table with existing stories, in case we @@ -1153,8 +1162,9 @@ public class BlurDatabaseHelper { } return result; } - - private Cursor getActiveStoriesCursorNoPrep(FeedSet fs, StoryOrder order, CancellationSignal cancellationSignal) { + + @NonNull + private Cursor getActiveStoriesCursorNoPrep(@NonNull FeedSet fs, @NonNull StoryOrder order, @NonNull CancellationSignal cancellationSignal) { // stories aren't actually queried directly via the FeedSet and filters set in the UI. rather, // those filters are use to push live or cached story hashes into the reading session table, and // those hashes are used to pull story data from the story table @@ -1182,7 +1192,7 @@ public class BlurDatabaseHelper { * criteria for the given FeedSet and filters; these hashes will be supplemented by hashes * fetched via the API and used to actually select story data when rendering story lists. */ - public void prepareReadingSession(FeedSet fs, StateFilter stateFilter, ReadFilter readFilter) { + public void prepareReadingSession(@NonNull FeedSet fs, @NonNull StateFilter stateFilter, @NonNull ReadFilter readFilter) { // a selection filter that will be used to pull active story hashes from the stories table into the reading session table StringBuilder sel = new StringBuilder(); // any selection args that need to be used within the inner select statement @@ -1202,7 +1212,7 @@ public class BlurDatabaseHelper { * Gets hashes of already-fetched stories that satisfy the given FeedSet and filters. Can be used * both to populate a reading session or to count local unreads. */ - private void getLocalStorySelectionAndArgs(StringBuilder sel, List selArgs, FeedSet fs, StateFilter stateFilter, ReadFilter readFilter) { + private void getLocalStorySelectionAndArgs(@NonNull StringBuilder sel, @NonNull List selArgs, @NonNull FeedSet fs, @NonNull StateFilter stateFilter, @NonNull ReadFilter readFilter) { // if the user has requested saved stories, ignore the unreads filter, as saveds do not have this state if (fs.isFilterSaved()) { readFilter = ReadFilter.ALL; @@ -1283,7 +1293,7 @@ public class BlurDatabaseHelper { } } - public void setSessionFeedSet(FeedSet fs) { + public void setSessionFeedSet(@Nullable FeedSet fs) { if (fs == null) { synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SYNC_METADATA_TABLE, DatabaseConstants.SYNC_METADATA_KEY + " = ?", new String[] {DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET});} } else { @@ -1293,9 +1303,10 @@ public class BlurDatabaseHelper { synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.SYNC_METADATA_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);} } } - + + @Nullable public FeedSet getSessionFeedSet() { - FeedSet fs = null; + FeedSet fs; Cursor c = dbRO.query(DatabaseConstants.SYNC_METADATA_TABLE, null, DatabaseConstants.SYNC_METADATA_KEY + " = ?", new String[] {DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET}, null, null, null, null); if (c.getCount() < 1) return null; c.moveToFirst(); @@ -1304,20 +1315,21 @@ public class BlurDatabaseHelper { return fs; } - public boolean isFeedSetReady(FeedSet fs) { + public boolean isFeedSetReady(@Nullable FeedSet fs) { return fs.equals(getSessionFeedSet()); } - public void clearClassifiersForFeed(String feedId) { + public void clearClassifiersForFeed(@Nullable String feedId) { String[] selArgs = new String[] {feedId}; synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs);} } - public void insertClassifier(Classifier classifier) { + public void insertClassifier(@NonNull Classifier classifier) { bulkInsertValues(DatabaseConstants.CLASSIFIER_TABLE, classifier.getContentValues()); } - public Classifier getClassifierForFeed(String feedId) { + @NonNull + public Classifier getClassifierForFeed(@Nullable String feedId) { String[] selArgs = new String[] {feedId}; Cursor c = dbRO.query(DatabaseConstants.CLASSIFIER_TABLE, null, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs, null, null, null); Classifier classifier = Classifier.fromCursor(c); @@ -1326,7 +1338,8 @@ public class BlurDatabaseHelper { return classifier; } - public List getComments(String storyId) { + @NonNull + public List getComments(@NonNull String storyId) { String[] selArgs = new String[] {storyId}; String selection = DatabaseConstants.COMMENT_STORYID + " = ?"; Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, null, selection, selArgs, null, null, null); @@ -1338,7 +1351,8 @@ public class BlurDatabaseHelper { return comments; } - public Comment getComment(String storyId, String userId) { + @Nullable + public Comment getComment(@Nullable String storyId, @Nullable String userId) { String selection = DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?"; String[] selArgs = new String[] {storyId, userId}; Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, null, selection, selArgs, null, null, null); @@ -1354,7 +1368,7 @@ public class BlurDatabaseHelper { * will show up in the UI with reduced functionality until the server gets back to us with * an ID at which time the placeholder will be removed. */ - public void insertCommentPlaceholder(String storyId, @Nullable String userId, String commentText) { + public void insertCommentPlaceholder(@Nullable String storyId, @Nullable String userId, @Nullable String commentText) { Comment comment = new Comment(); comment.isPlaceholder = true; comment.id = Comment.PLACEHOLDER_COMMENT_ID + storyId + userId; @@ -1376,23 +1390,23 @@ public class BlurDatabaseHelper { } } - public void editReply(String replyId, String replyText) { + public void editReply(@Nullable String replyId, @Nullable String replyText) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.REPLY_TEXT, replyText); synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.REPLY_TABLE, values, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId});} } - public void deleteReply(String replyId) { + public void deleteReply(@Nullable String replyId) { synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_ID + " = ?", new String[]{replyId});} } - public void clearSelfComments(String storyId, @Nullable String userId) { + public void clearSelfComments(@Nullable String storyId, @Nullable String userId) { synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.COMMENT_TABLE, DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?", new String[]{storyId, userId});} } - public void setCommentLiked(String storyId, String commentUserId, @Nullable String currentUserId, boolean liked) { + public void setCommentLiked(@Nullable String storyId, @Nullable String commentUserId, @Nullable String currentUserId, boolean liked) { // get a fresh copy of the story from the DB so we can append to the shared ID set Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, null, @@ -1421,7 +1435,8 @@ public class BlurDatabaseHelper { synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.COMMENT_TABLE, values, DatabaseConstants.COMMENT_ID + " = ?", new String[]{comment.id});} } - public UserProfile getUserProfile(String userId) { + @Nullable + public UserProfile getUserProfile(@Nullable String userId) { String[] selArgs = new String[] {userId}; String selection = DatabaseConstants.USER_USERID + " = ?"; Cursor c = dbRO.query(DatabaseConstants.USER_TABLE, null, selection, selArgs, null, null, null); @@ -1430,7 +1445,8 @@ public class BlurDatabaseHelper { return profile; } - public List getCommentReplies(String commentId) { + @NonNull + public List getCommentReplies(@Nullable String commentId) { String[] selArgs = new String[] {commentId}; String selection = DatabaseConstants.REPLY_COMMENTID+ " = ?"; Cursor c = dbRO.query(DatabaseConstants.REPLY_TABLE, null, selection, selArgs, null, null, DatabaseConstants.REPLY_DATE + " ASC"); @@ -1442,7 +1458,7 @@ public class BlurDatabaseHelper { return replies; } - public void insertReplyPlaceholder(String storyId, @Nullable String userId, String commentUserId, String replyText) { + public void insertReplyPlaceholder(@Nullable String storyId, @Nullable String userId, @Nullable String commentUserId, @Nullable String replyText) { // get a fresh copy of the comment so we can discover the ID Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, null, @@ -1467,14 +1483,14 @@ public class BlurDatabaseHelper { synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE);} } - public void putStoryDismissed(String storyHash) { + public void putStoryDismissed(@Nullable String storyHash) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.NOTIFY_DISMISS_STORY_HASH, storyHash); values.put(DatabaseConstants.NOTIFY_DISMISS_TIME, Calendar.getInstance().getTime().getTime()); synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, values);} } - public boolean isStoryDismissed(String storyHash) { + public boolean isStoryDismissed(@Nullable String storyHash) { String[] selArgs = new String[] {storyHash}; String selection = DatabaseConstants.NOTIFY_DISMISS_STORY_HASH + " = ?"; Cursor c = dbRO.query(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, selection, selArgs, null, null, null); @@ -1494,7 +1510,7 @@ public class BlurDatabaseHelper { } } - private void putFeedTagsExtSync(String feedId, Collection tags) { + private void putFeedTagsExtSync(@Nullable String feedId, @NonNull Collection tags) { dbRW.delete(DatabaseConstants.FEED_TAGS_TABLE, DatabaseConstants.FEED_TAGS_FEEDID + " = ?", new String[]{feedId} @@ -1509,7 +1525,8 @@ public class BlurDatabaseHelper { bulkInsertValuesExtSync(DatabaseConstants.FEED_TAGS_TABLE, valuesList); } - public List getTagsForFeed(String feedId) { + @NonNull + public List getTagsForFeed(@Nullable String feedId) { Cursor c = dbRO.query(DatabaseConstants.FEED_TAGS_TABLE, new String[]{DatabaseConstants.FEED_TAGS_TAG}, DatabaseConstants.FEED_TAGS_FEEDID + " = ?", @@ -1526,7 +1543,7 @@ public class BlurDatabaseHelper { return result; } - private void putFeedAuthorsExtSync(String feedId, Collection authors) { + private void putFeedAuthorsExtSync(@Nullable String feedId, @NonNull Collection authors) { dbRW.delete(DatabaseConstants.FEED_AUTHORS_TABLE, DatabaseConstants.FEED_AUTHORS_FEEDID + " = ?", new String[]{feedId} @@ -1541,7 +1558,8 @@ public class BlurDatabaseHelper { bulkInsertValuesExtSync(DatabaseConstants.FEED_AUTHORS_TABLE, valuesList); } - public List getAuthorsForFeed(String feedId) { + @NonNull + public List getAuthorsForFeed(@Nullable String feedId) { Cursor c = dbRO.query(DatabaseConstants.FEED_AUTHORS_TABLE, new String[]{DatabaseConstants.FEED_AUTHORS_AUTHOR}, DatabaseConstants.FEED_AUTHORS_FEEDID + " = ?", @@ -1558,18 +1576,19 @@ public class BlurDatabaseHelper { return result; } - public void renameFeed(String feedId, String newFeedName) { + public void renameFeed(@Nullable String feedId, @Nullable String newFeedName) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.FEED_TITLE, newFeedName); synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} } - public static void closeQuietly(Cursor c) { + public static void closeQuietly(@Nullable Cursor c) { if (c == null) return; try {c.close();} catch (Exception e) { } } + @Nullable private static String conjoinSelections(CharSequence... args) { StringBuilder s = null; for (CharSequence c : args) { @@ -1589,7 +1608,8 @@ public class BlurDatabaseHelper { * Invoke the rawQuery() method on our read-only SQLiteDatabase memeber using the provided CancellationSignal * only if the device's platform provides support. */ - private Cursor rawQuery(String sql, String[] selectionArgs, CancellationSignal cancellationSignal) { + @Nullable + private Cursor rawQuery(@NonNull String sql, @Nullable String[] selectionArgs, @Nullable CancellationSignal cancellationSignal) { if (AppConstants.VERBOSE_LOG_DB) { Log.d(this.getClass().getName(), String.format("DB rawQuery: '%s' with args: %s", sql, java.util.Arrays.toString(selectionArgs))); } @@ -1600,15 +1620,18 @@ public class BlurDatabaseHelper { * Invoke the query() method on our read-only SQLiteDatabase memeber using the provided CancellationSignal * only if the device's platform provides support. */ - private Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit, CancellationSignal cancellationSignal) { + @NonNull + private Cursor query(boolean distinct, @NonNull String table, @Nullable String[] columns, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String groupBy, @Nullable String having, @Nullable String orderBy, @Nullable String limit, @NonNull CancellationSignal cancellationSignal) { return dbRO.query(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit, cancellationSignal); } - public FeedSet feedSetFromFolderName(String folderName) { + @NonNull + public FeedSet feedSetFromFolderName(@NonNull String folderName) { return FeedSet.folder(folderName, getFeedIdsRecursive(folderName)); } - private Set getFeedIdsRecursive(String folderName) { + @NonNull + private Set getFeedIdsRecursive(@NonNull String folderName) { Folder folder = getFolder(folderName); if (folder == null) return emptySet(); Set feedIds = new HashSet<>(folder.feedIds); diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt index 8472c3572..058297714 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt @@ -434,8 +434,10 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { } R.id.menu_go_to_feed -> { val feed = dbHelper.getFeed(story!!.feedId) - val fs = FeedSet.singleFeed(feed.feedId) - FeedItemsList.startActivity(requireContext(), fs, feed, null, null) + feed?.let { + val fs = FeedSet.singleFeed(it.feedId) + FeedItemsList.startActivity(requireContext(), fs, it, null, null) + } true } else -> { diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/SetupCommentSectionTask.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/SetupCommentSectionTask.kt index 48910e510..8ddf87c49 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/SetupCommentSectionTask.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/SetupCommentSectionTask.kt @@ -56,7 +56,7 @@ class SetupCommentSectionTask(private val fragment: ReadingItemFragment, view: V private fun doInBackground() { if (context == null || story == null || story.id.isNullOrEmpty()) return - comments.addAll(fragment.dbHelper.getComments(story.id)) + comments.addAll(fragment.dbHelper.getComments(story.id!!)) // users by whom we saw non-pseudo comments val commentingUserIds: MutableSet = HashSet() From e0eb72977694109051a6523c7192bf88685ec16f Mon Sep 17 00:00:00 2001 From: sictiru Date: Tue, 17 Sep 2024 11:35:53 -0700 Subject: [PATCH 18/22] #1891 Handle Android 9 and below for OPML file export --- .../NewsBlur/app/src/main/AndroidManifest.xml | 215 +++++++++--------- .../newsblur/activity/ImportExportActivity.kt | 34 ++- .../app/src/main/res/values/strings.xml | 4 +- 3 files changed, 149 insertions(+), 104 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/AndroidManifest.xml b/clients/android/NewsBlur/app/src/main/AndroidManifest.xml index 1f5645a03..f8dcce021 100644 --- a/clients/android/NewsBlur/app/src/main/AndroidManifest.xml +++ b/clients/android/NewsBlur/app/src/main/AndroidManifest.xml @@ -1,22 +1,25 @@ - + - + + + android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"> + android:theme="@style/splashScreen"> - @@ -48,143 +52,139 @@ + android:label="@string/get_started" + android:noHistory="true" /> + android:alwaysRetainTaskState="true" + android:launchMode="singleTask" /> + android:label="@string/profile" /> + android:label="@string/settings" /> + android:label="@string/menu_widget" + android:launchMode="singleTask" /> - + + android:launchMode="singleTask" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + android:label="@string/mute_sites" + android:launchMode="singleTask" /> - + - + android:exported="false" + android:permission="android.permission.BIND_JOB_SERVICE" /> - - - - - - - + + + + - - - - + + - + android:exported="false"> + - + - - - - - - - - + + + + + + + + + + + + - - - - + + + + + - - + + + + - - - - - - - - + + + + + + + + + - + android:theme="@style/Theme.Translucent" + android:windowSoftInputMode="adjustResize"> diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/ImportExportActivity.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/ImportExportActivity.kt index 3f1b2c4c7..2c92973fd 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/ImportExportActivity.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/ImportExportActivity.kt @@ -1,12 +1,16 @@ package com.newsblur.activity +import android.Manifest import android.app.Activity import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri +import android.os.Build import android.os.Bundle import android.webkit.MimeTypeMap import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat import com.google.android.material.snackbar.Snackbar import com.newsblur.R import com.newsblur.databinding.ActivityImportExportBinding @@ -40,6 +44,16 @@ class ImportExportActivity : NbActivity() { } } + // used for Android 9 and below + private val requestWriteStoragePermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + exportOpmlFile() + } else { + Toast.makeText(this, R.string.write_storage_permission_opml, Toast.LENGTH_LONG).show() + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityImportExportBinding.inflate(layoutInflater) @@ -55,7 +69,13 @@ class ImportExportActivity : NbActivity() { private fun setupListeners() { binding.btnUpload.setOnClickListener { pickOpmlFile() } - binding.btnDownload.setOnClickListener { exportOpmlFile() } + binding.btnDownload.setOnClickListener { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + checkAndRequestWriteStoragePermission() + } else { + exportOpmlFile() + } + } } private fun pickOpmlFile() { @@ -131,4 +151,16 @@ class ImportExportActivity : NbActivity() { override fun handleUpdate(updateType: Int) { // ignore } + + // Android 9 and below + private fun checkAndRequestWriteStoragePermission() { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED) { + exportOpmlFile() + } else { + requestWriteStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } } \ No newline at end of file diff --git a/clients/android/NewsBlur/app/src/main/res/values/strings.xml b/clients/android/NewsBlur/app/src/main/res/values/strings.xml index 6eda5fa25..48387ba01 100644 --- a/clients/android/NewsBlur/app/src/main/res/values/strings.xml +++ b/clients/android/NewsBlur/app/src/main/res/values/strings.xml @@ -745,7 +745,9 @@ Permissions is required for posting notifications Notifications permission must be added manually in the app\'s settings before trying again to enable notifications - + + Write storage permission is required for OPML export + Story marked as saved Story marked as unsaved Story marked as read From 64b12bd4a7435bb79304040b37c6a13a0d77477a Mon Sep 17 00:00:00 2001 From: sictiru Date: Tue, 17 Sep 2024 14:10:52 -0700 Subject: [PATCH 19/22] Android 13.3.1 229 --- clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index 912d7c0b4..28ac9aa3b 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 24 const val targetSdk = 34 - const val versionCode = 228 - const val versionName = "13.3.0" + const val versionCode = 229 + const val versionName = "13.3.1" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" From adfdb55741124f8a034ba778e16b0f4d6ab23102 Mon Sep 17 00:00:00 2001 From: sictiru Date: Wed, 16 Oct 2024 10:36:21 -0700 Subject: [PATCH 20/22] Add nullability annotations to ReadingAction --- .../newsblur/database/BlurDatabaseHelper.java | 1 - .../java/com/newsblur/util/ReadingAction.java | 47 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java index 1ad6e3455..99d39a531 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/database/BlurDatabaseHelper.java @@ -576,7 +576,6 @@ public class BlurDatabaseHelper { } } - @NonNull public Folder getFolder(@NonNull String folderName) { String[] selArgs = new String[] {folderName}; String selection = DatabaseConstants.FOLDER_NAME + " = ?"; diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/ReadingAction.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/ReadingAction.java index 3377357af..648bc9f8f 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/ReadingAction.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/ReadingAction.java @@ -9,6 +9,9 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.io.Serializable; import com.newsblur.database.BlurDatabaseHelper; @@ -88,21 +91,21 @@ public class ReadingAction implements Serializable { return tried; } - public static ReadingAction markStoryRead(String hash) { + public static ReadingAction markStoryRead(@Nullable String hash) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.MARK_READ; ra.storyHash = hash; return ra; } - public static ReadingAction markStoryUnread(String hash) { + public static ReadingAction markStoryUnread(@Nullable String hash) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.MARK_UNREAD; ra.storyHash = hash; return ra; } - public static ReadingAction saveStory(String hash, List userTags) { + public static ReadingAction saveStory(@Nullable String hash, @Nullable List userTags) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.SAVE; ra.storyHash = hash; @@ -114,14 +117,14 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction unsaveStory(String hash) { + public static ReadingAction unsaveStory(@Nullable String hash) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.UNSAVE; ra.storyHash = hash; return ra; } - public static ReadingAction markFeedRead(FeedSet fs, Long olderThan, Long newerThan) { + public static ReadingAction markFeedRead(@NonNull FeedSet fs, @Nullable Long olderThan, @Nullable Long newerThan) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.MARK_READ; ra.feedSet = fs; @@ -130,7 +133,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction shareStory(String hash, String storyId, String feedId, String sourceUserId, String commentReplyText) { + public static ReadingAction shareStory(@Nullable String hash, @Nullable String storyId, @Nullable String feedId, @Nullable String sourceUserId, @Nullable String commentReplyText) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.SHARE; ra.storyHash = hash; @@ -141,7 +144,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction unshareStory(String hash, String storyId, String feedId) { + public static ReadingAction unshareStory(@Nullable String hash, @Nullable String storyId, @Nullable String feedId) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.UNSHARE; ra.storyHash = hash; @@ -150,7 +153,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction likeComment(String storyId, String commentUserId, String feedId) { + public static ReadingAction likeComment(@Nullable String storyId, @Nullable String commentUserId, @Nullable String feedId) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.LIKE_COMMENT; ra.storyId = storyId; @@ -159,7 +162,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction unlikeComment(String storyId, String commentUserId, String feedId) { + public static ReadingAction unlikeComment(@Nullable String storyId, @Nullable String commentUserId, @Nullable String feedId) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.UNLIKE_COMMENT; ra.storyId = storyId; @@ -168,7 +171,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction replyToComment(String storyId, String feedId, String commentUserId, String commentReplyText) { + public static ReadingAction replyToComment(@Nullable String storyId, @Nullable String feedId, @Nullable String commentUserId, @Nullable String commentReplyText) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.REPLY; ra.storyId = storyId; @@ -178,7 +181,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction updateReply(String storyId, String feedId, String commentUserId, String replyId, String commentReplyText) { + public static ReadingAction updateReply(@Nullable String storyId, @Nullable String feedId, @Nullable String commentUserId, @Nullable String replyId, @Nullable String commentReplyText) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.EDIT_REPLY; ra.storyId = storyId; @@ -189,7 +192,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction deleteReply(String storyId, String feedId, String commentUserId, String replyId) { + public static ReadingAction deleteReply(@Nullable String storyId, @Nullable String feedId, @Nullable String commentUserId, @Nullable String replyId) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.DELETE_REPLY; ra.storyId = storyId; @@ -199,7 +202,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction muteFeeds(Set activeFeedIds, Set modifiedFeedIds) { + public static ReadingAction muteFeeds(@NonNull Set activeFeedIds, @NonNull Set modifiedFeedIds) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.MUTE_FEEDS; ra.activeFeedIds = activeFeedIds; @@ -207,7 +210,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction unmuteFeeds(Set activeFeedIds, Set modifiedFeedIds) { + public static ReadingAction unmuteFeeds(@NonNull Set activeFeedIds, @NonNull Set modifiedFeedIds) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.UNMUTE_FEEDS; ra.activeFeedIds = activeFeedIds; @@ -215,7 +218,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction setNotify(String feedId, List notifyTypes, String notifyFilter) { + public static ReadingAction setNotify(@Nullable String feedId, @Nullable List notifyTypes, @Nullable String notifyFilter) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.SET_NOTIFY; ra.feedId = feedId; @@ -228,14 +231,14 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction instaFetch(String feedId) { + public static ReadingAction instaFetch(@Nullable String feedId) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.INSTA_FETCH; ra.feedId = feedId; return ra; } - public static ReadingAction updateIntel(String feedId, Classifier classifier, FeedSet fs) { + public static ReadingAction updateIntel(@Nullable String feedId, @Nullable Classifier classifier, @Nullable FeedSet fs) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.UPDATE_INTEL; ra.feedId = feedId; @@ -244,7 +247,7 @@ public class ReadingAction implements Serializable { return ra; } - public static ReadingAction renameFeed(String feedId, String newFeedName) { + public static ReadingAction renameFeed(@Nullable String feedId, @Nullable String newFeedName) { ReadingAction ra = new ReadingAction(); ra.type = ActionType.RENAME_FEED; ra.feedId = feedId; @@ -265,7 +268,7 @@ public class ReadingAction implements Serializable { return values; } - public static ReadingAction fromCursor(Cursor c) { + public static ReadingAction fromCursor(@NonNull Cursor c) { long time = c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TIME)); int tried = c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TRIED)); String params = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_PARAMS)); @@ -278,7 +281,7 @@ public class ReadingAction implements Serializable { /** * Execute this action remotely via the API. */ - public NewsBlurResponse doRemote(APIManager apiManager, BlurDatabaseHelper dbHelper, StateFilter stateFilter) { + public NewsBlurResponse doRemote(@NonNull APIManager apiManager, @NonNull BlurDatabaseHelper dbHelper, @NonNull StateFilter stateFilter) { // generic response to return NewsBlurResponse result = null; // optional specific responses that are locally actionable @@ -392,7 +395,7 @@ public class ReadingAction implements Serializable { return result; } - public int doLocal(Context context, BlurDatabaseHelper dbHelper) { + public int doLocal(@NonNull Context context, @NonNull BlurDatabaseHelper dbHelper) { return doLocal(context, dbHelper, false); } @@ -403,7 +406,7 @@ public class ReadingAction implements Serializable { * * @return the union of update impact flags that resulted from this action. */ - public int doLocal(Context context, BlurDatabaseHelper dbHelper, boolean isFollowup) { + public int doLocal(@NonNull Context context, @NonNull BlurDatabaseHelper dbHelper, boolean isFollowup) { String userId = PrefsUtils.getUserId(context); int impact = 0; switch (type) { From 80fb506e6ad1d691fea8072ac3308bcaf569fcf2 Mon Sep 17 00:00:00 2001 From: sictiru Date: Mon, 11 Nov 2024 20:56:47 -0800 Subject: [PATCH 21/22] #1886 Support for themed icons --- .../NewsBlur/app/src/main/AndroidManifest.xml | 2 +- .../com/newsblur/util/NotificationUtils.java | 2 +- .../main/res/drawable/ic_logo_background.xml | 33 ++++++++++++ .../main/res/drawable/ic_logo_foreground.xml | 51 +++++++++++++++++++ .../main/res/drawable/ic_logo_monochrome.xml | 10 ++++ .../res/mipmap-anydpi-v33/ic_launcher.xml | 6 +++ .../main/res/mipmap-anydpi/ic_launcher.xml | 5 ++ .../NewsBlur/buildSrc/src/main/java/Config.kt | 4 +- 8 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_background.xml create mode 100644 clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_foreground.xml create mode 100644 clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_monochrome.xml create mode 100644 clients/android/NewsBlur/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml create mode 100644 clients/android/NewsBlur/app/src/main/res/mipmap-anydpi/ic_launcher.xml diff --git a/clients/android/NewsBlur/app/src/main/AndroidManifest.xml b/clients/android/NewsBlur/app/src/main/AndroidManifest.xml index f8dcce021..5ee3b86de 100644 --- a/clients/android/NewsBlur/app/src/main/AndroidManifest.xml +++ b/clients/android/NewsBlur/app/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backupscheme" android:fullBackupOnly="true" - android:icon="@drawable/logo" + android:icon="@mipmap/ic_launcher" android:label="@string/newsblur" android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"> diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/NotificationUtils.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/NotificationUtils.java index 3de853349..51eb809fd 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/NotificationUtils.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/NotificationUtils.java @@ -144,7 +144,7 @@ public class NotificationUtils { NotificationCompat.Builder nb = new NotificationCompat.Builder(context, context.getString(R.string.story_notification_channel_id)) .setContentTitle(title.toString()) .setContentText(story.shortContent) - .setSmallIcon(R.drawable.logo_monochrome) + .setSmallIcon(R.drawable.ic_logo_monochrome) .setContentIntent(pendingIntent) .setDeleteIntent(dismissPendingIntent) .setAutoCancel(true) diff --git a/clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_background.xml b/clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_background.xml new file mode 100644 index 000000000..e1c056d5d --- /dev/null +++ b/clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_background.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + diff --git a/clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_foreground.xml b/clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_foreground.xml new file mode 100644 index 000000000..ff9a3dadd --- /dev/null +++ b/clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_foreground.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_monochrome.xml b/clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_monochrome.xml new file mode 100644 index 000000000..550341ee9 --- /dev/null +++ b/clients/android/NewsBlur/app/src/main/res/drawable/ic_logo_monochrome.xml @@ -0,0 +1,10 @@ + + + diff --git a/clients/android/NewsBlur/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/clients/android/NewsBlur/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml new file mode 100644 index 000000000..b30f81359 --- /dev/null +++ b/clients/android/NewsBlur/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/clients/android/NewsBlur/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/clients/android/NewsBlur/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 000000000..80ac01975 --- /dev/null +++ b/clients/android/NewsBlur/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index 28ac9aa3b..dc442716f 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -3,12 +3,12 @@ import org.gradle.api.JavaVersion object Config { const val compileSdk = 34 - const val minSdk = 24 + const val minSdk = 26 const val targetSdk = 34 const val versionCode = 229 const val versionName = "13.3.1" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" - val javaVersion = JavaVersion.VERSION_17 + val javaVersion = JavaVersion.VERSION_21 } \ No newline at end of file From 94fdf776134142796568be30e980c41a0ecc4bfd Mon Sep 17 00:00:00 2001 From: sictiru Date: Mon, 11 Nov 2024 20:57:53 -0800 Subject: [PATCH 22/22] Android 13.3.2 230 --- clients/android/NewsBlur/buildSrc/src/main/java/Config.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt index dc442716f..9a8adb605 100644 --- a/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt +++ b/clients/android/NewsBlur/buildSrc/src/main/java/Config.kt @@ -5,8 +5,8 @@ object Config { const val compileSdk = 34 const val minSdk = 26 const val targetSdk = 34 - const val versionCode = 229 - const val versionName = "13.3.1" + const val versionCode = 230 + const val versionName = "13.3.2" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner"