Merge branch 'sictiru'

* sictiru: (22 commits)
  Android 13.3.2 230
  #1886 Support for themed icons
  Add nullability annotations to ReadingAction
  Android 13.3.1 229
  #1891 Handle Android 9 and below for OPML file export
  Add nullability annotation to help with Kotlin conversion
  Revert binding view destroy
  Android v13.3.0
  Android v13.2.11 Revert db mutex changes
  Revert "#1853 Remove read write db mutex"
  Android v13.2.10. Clear web chrome client on web view destroy
  Android v13.2.9. Destroy webview on fragment view destroy
  Android v13.2.8
  Cleanup web view binding on destroy view
  Android v13.2.7
  Remove play core in favor or play review. Update other dependencies
  Android v13.2.6
  Catch OperationCanceledException exception when loading active stories
  #1853 Remove read write db mutex
  Android v13.2.5
  ...
This commit is contained in:
Samuel Clay 2024-11-12 17:09:17 -08:00
commit 5af380136c
26 changed files with 540 additions and 282 deletions

View file

@ -1,6 +1,7 @@
package com.newsblur.benchmark 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.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule import org.junit.Rule
@ -10,7 +11,7 @@ import org.junit.runner.RunWith
/** /**
* Runs in its own process * Runs in its own process
*/ */
@OptIn(ExperimentalBaselineProfilesApi::class) @RequiresApi(Build.VERSION_CODES.P)
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator { class BaselineProfileGenerator {
@ -19,7 +20,7 @@ class BaselineProfileGenerator {
@Test @Test
fun generateSimpleStartupProfile() { fun generateSimpleStartupProfile() {
rule.collectBaselineProfile(packageName = "com.newsblur") { rule.collect(packageName = "com.newsblur") {
pressHome() pressHome()
startActivityAndWait() startActivityAndWait()
} }
@ -28,7 +29,7 @@ class BaselineProfileGenerator {
@Test @Test
fun generateUserJourneyProfile() { fun generateUserJourneyProfile() {
var needsLogin = true var needsLogin = true
rule.collectBaselineProfile(packageName = "com.newsblur") { rule.collect(packageName = "com.newsblur") {
pressHome() pressHome()
startActivityAndWait() startActivityAndWait()

View file

@ -57,7 +57,7 @@ dependencies {
implementation(Dependencies.okHttp) implementation(Dependencies.okHttp)
implementation(Dependencies.gson) implementation(Dependencies.gson)
implementation(Dependencies.billing) implementation(Dependencies.billing)
implementation(Dependencies.playCore) implementation(Dependencies.playReview)
implementation(Dependencies.material) implementation(Dependencies.material)
implementation(Dependencies.preference) implementation(Dependencies.preference)
implementation(Dependencies.browser) implementation(Dependencies.browser)

View file

@ -1,22 +1,25 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application <application
android:icon="@drawable/logo"
android:label="@string/newsblur"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"
android:fullBackupContent="@xml/backupscheme"
android:name=".NbApplication" android:name=".NbApplication"
android:dataExtractionRules="@xml/data_extraction_rules"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupOnly="true"> android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backupscheme"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/newsblur"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<profileable <profileable
android:shell="true" android:shell="true"
@ -24,15 +27,16 @@
<activity <activity
android:name=".activity.InitActivity" android:name=".activity.InitActivity"
android:theme="@style/splashScreen" android:exported="true"
android:noHistory="true" android:noHistory="true"
android:exported="true"> android:theme="@style/splashScreen">
<intent-filter> <intent-filter>
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts" <meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity> </activity>
@ -48,143 +52,139 @@
<activity <activity
android:name=".activity.RegisterProgress" android:name=".activity.RegisterProgress"
android:noHistory="true" android:label="@string/get_started"
android:label="@string/get_started" /> android:noHistory="true" />
<activity <activity
android:name=".activity.Main" android:name=".activity.Main"
android:launchMode="singleTask" android:alwaysRetainTaskState="true"
android:alwaysRetainTaskState="true" /> android:launchMode="singleTask" />
<activity <activity
android:name=".activity.Profile" android:name=".activity.Profile"
android:label="@string/profile"/> android:label="@string/profile" />
<activity <activity
android:name=".activity.Settings" android:name=".activity.Settings"
android:label="@string/settings"/> android:label="@string/settings" />
<activity android:name=".activity.ImportExportActivity" /> <activity android:name=".activity.ImportExportActivity" />
<activity android:name=".activity.NotificationsActivity" /> <activity android:name=".activity.NotificationsActivity" />
<activity <activity
android:name=".activity.WidgetConfig" android:name=".activity.WidgetConfig"
android:launchMode="singleTask" android:label="@string/menu_widget"
android:label="@string/menu_widget" /> android:launchMode="singleTask" />
<activity <activity android:name=".activity.FeedItemsList" />
android:name=".activity.FeedItemsList" />
<activity <activity
android:name=".activity.AllStoriesItemsList" android:name=".activity.AllStoriesItemsList"
android:launchMode="singleTask"/> android:launchMode="singleTask" />
<activity <activity android:name=".activity.InfrequentItemsList" />
android:name=".activity.InfrequentItemsList" />
<activity <activity android:name=".activity.ReadStoriesItemsList" />
android:name=".activity.ReadStoriesItemsList" />
<activity <activity android:name=".activity.SavedStoriesItemsList" />
android:name=".activity.SavedStoriesItemsList" />
<activity <activity android:name=".activity.AllSharedStoriesItemsList" />
android:name=".activity.AllSharedStoriesItemsList" />
<activity <activity android:name=".activity.GlobalSharedStoriesItemsList" />
android:name=".activity.GlobalSharedStoriesItemsList" />
<activity <activity android:name=".activity.FolderItemsList" />
android:name=".activity.FolderItemsList" />
<activity <activity android:name=".activity.SocialFeedItemsList" />
android:name=".activity.SocialFeedItemsList" />
<activity <activity android:name=".activity.FeedReading" />
android:name=".activity.FeedReading"/>
<activity <activity android:name=".activity.AllStoriesReading" />
android:name=".activity.AllStoriesReading"/>
<activity <activity android:name=".activity.InfrequentReading" />
android:name=".activity.InfrequentReading"/>
<activity <activity android:name=".activity.ReadStoriesReading" />
android:name=".activity.ReadStoriesReading"/>
<activity <activity android:name=".activity.SavedStoriesReading" />
android:name=".activity.SavedStoriesReading"/>
<activity <activity android:name=".activity.AllSharedStoriesReading" />
android:name=".activity.AllSharedStoriesReading"/>
<activity <activity android:name=".activity.GlobalSharedStoriesReading" />
android:name=".activity.GlobalSharedStoriesReading"/>
<activity <activity android:name=".activity.FolderReading" />
android:name=".activity.FolderReading"/>
<activity <activity android:name=".activity.SubscriptionActivity" />
android:name=".activity.SubscriptionActivity" />
<activity <activity
android:name=".activity.MuteConfig" android:name=".activity.MuteConfig"
android:launchMode="singleTask" android:label="@string/mute_sites"
android:label="@string/mute_sites"/> android:launchMode="singleTask" />
<activity <activity
android:name=".activity.FeedSearchActivity" android:name=".activity.FeedSearchActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<activity <activity android:name=".activity.SocialFeedReading" />
android:name=".activity.SocialFeedReading"/>
<service <service
android:name=".service.NBSyncService" android:name=".service.NBSyncService"
android:permission="android.permission.BIND_JOB_SERVICE" /> android:permission="android.permission.BIND_JOB_SERVICE" />
<service android:name=".widget.WidgetRemoteViewsService" <service
android:name=".widget.WidgetRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
<service <service
android:name=".service.SubscriptionSyncService" android:name=".service.SubscriptionSyncService"
android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false"
android:exported="false" /> android:permission="android.permission.BIND_JOB_SERVICE" />
<receiver android:name=".service.BootReceiver" <receiver
android:name=".service.BootReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".util.DownloadCompleteReceiver" <receiver
android:name=".util.DownloadCompleteReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" /> <action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".util.NotifyDismissReceiver" android:exported="false" /> <receiver
<receiver android:name=".util.NotifySaveReceiver" android:exported="false" /> android:name=".util.NotifyDismissReceiver"
<receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" /> android:exported="false" />
<receiver android:name=".util.NotifyShareReceiver" android:exported="false" /> <receiver
<receiver android:name=".widget.WidgetProvider" android:name=".util.NotifySaveReceiver"
android:exported="false" />
<receiver
android:name=".util.NotifyMarkreadReceiver"
android:exported="false" />
<receiver
android:name=".util.NotifyShareReceiver"
android:exported="false" />
<receiver
android:name=".widget.WidgetProvider"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter> </intent-filter>
<meta-data android:name="android.appwidget.provider" <meta-data
android:name="android.appwidget.provider"
android:resource="@xml/newsblur_appwidget_info" /> android:resource="@xml/newsblur_appwidget_info" />
</receiver> </receiver>
<receiver android:name=".service.TimeChangeReceiver" <receiver
android:name=".service.TimeChangeReceiver"
android:exported="true"> android:exported="true">
<intent-filter > <intent-filter>
<action android:name="android.intent.action.TIME_SET"/> <action android:name="android.intent.action.TIME_SET" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver <receiver
android:name=".widget.WidgetUpdateReceiver" android:name=".widget.WidgetUpdateReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false"></receiver>
</receiver>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="com.newsblur.fileprovider" android:authorities="com.newsblur.fileprovider"
@ -195,59 +195,70 @@
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<activity android:name=".activity.AddFeedExternal" <activity
android:name=".activity.AddFeedExternal"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/>
<data android:scheme="https"/> <data android:scheme="http" />
<data android:host="*"/> <data android:scheme="https" />
<data android:pathPattern=".*xml"/> <data android:host="*" />
<data android:pathPattern=".*rss"/> <data android:pathPattern=".*xml" />
<data android:pathPattern=".*atom"/> <data android:pathPattern=".*rss" />
<data android:pathPattern=".*json"/> <data android:pathPattern=".*atom" />
<data android:pathPattern=".*/feed.*"/> <data android:pathPattern=".*json" />
<data android:pathPattern=".*/feed.*" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="feed" /> <data android:scheme="feed" />
<data android:scheme="rss" /> <data android:scheme="rss" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/>
<data android:scheme="https"/> <data android:scheme="http" />
<data android:host="feeds.feedburner.com"/> <data android:scheme="https" />
<data android:host="feeds2.feedburner.com"/> <data android:host="feeds.feedburner.com" />
<data android:host="feeds2.feedburner.com" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<data android:scheme="http"/> <data android:scheme="http" />
<data android:scheme="https"/> <data android:scheme="https" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="text/xml"/>
<data android:mimeType="application/rss+xml"/> <data android:mimeType="text/xml" />
<data android:mimeType="application/atom+xml"/> <data android:mimeType="application/rss+xml" />
<data android:mimeType="application/xml"/> <data android:mimeType="application/atom+xml" />
<data android:mimeType="application/json"/> <data android:mimeType="application/xml" />
<data android:mimeType="application/feed+json"/> <data android:mimeType="application/json" />
<data android:scheme="http"/> <data android:mimeType="application/feed+json" />
<data android:scheme="https"/> <data android:scheme="http" />
<data android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".activity.ShareExternalStoryActivity" <activity
android:theme="@style/Theme.Translucent" android:name=".activity.ShareExternalStoryActivity"
android:windowSoftInputMode="adjustResize" android:exported="true"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:exported="true"> android:theme="@style/Theme.Translucent"
android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />

View file

@ -10,10 +10,10 @@ import androidx.fragment.app.DialogFragment;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; 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.ReviewInfo;
import com.google.android.play.core.review.ReviewManager; import com.google.android.play.core.review.ReviewManager;
import com.google.android.play.core.review.ReviewManagerFactory; import com.google.android.play.core.review.ReviewManagerFactory;
import com.google.android.play.core.tasks.Task;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.di.IconLoader; import com.newsblur.di.IconLoader;
import com.newsblur.domain.Feed; import com.newsblur.domain.Feed;

View file

@ -1,12 +1,16 @@
package com.newsblur.activity package com.newsblur.activity
import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.newsblur.R import com.newsblur.R
import com.newsblur.databinding.ActivityImportExportBinding 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityImportExportBinding.inflate(layoutInflater) binding = ActivityImportExportBinding.inflate(layoutInflater)
@ -55,7 +69,13 @@ class ImportExportActivity : NbActivity() {
private fun setupListeners() { private fun setupListeners() {
binding.btnUpload.setOnClickListener { pickOpmlFile() } 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() { private fun pickOpmlFile() {
@ -131,4 +151,16 @@ class ImportExportActivity : NbActivity() {
override fun handleUpdate(updateType: Int) { override fun handleUpdate(updateType: Int) {
// ignore // 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)
}
}
} }

View file

@ -35,6 +35,7 @@ import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
import com.newsblur.util.Log; import com.newsblur.util.Log;
import com.newsblur.util.PendingTransitionUtils;
import com.newsblur.util.ReadingActionListener; import com.newsblur.util.ReadingActionListener;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
import com.newsblur.util.Session; import com.newsblur.util.Session;
@ -82,7 +83,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe
Trace.beginSection("ItemsListOnCreate"); Trace.beginSection("ItemsListOnCreate");
super.onCreate(bundle); super.onCreate(bundle);
overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left); PendingTransitionUtils.overrideEnterTransition(this);
contextMenuDelegate = new ItemListContextMenuDelegateImpl(this, feedUtils); contextMenuDelegate = new ItemListContextMenuDelegateImpl(this, feedUtils);
viewModel = new ViewModelProvider(this).get(ItemListViewModel.class); viewModel = new ViewModelProvider(this).get(ItemListViewModel.class);
@ -313,13 +314,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe
@Override @Override
public void finish() { public void finish() {
super.finish(); super.finish();
/* PendingTransitionUtils.overrideExitTransition(this);
* 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);
} }
abstract String getSaveSearchFeedId(); abstract String getSaveSearchFeedId();

View file

@ -65,7 +65,7 @@ open class NbActivity : AppCompatActivity() {
// Facilitates the db updates by the sync service on the UI // Facilitates the db updates by the sync service on the UI
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { launch {
NbSyncManager.state.collectLatest { NbSyncManager.state.collectLatest {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View file

@ -388,7 +388,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
get() { get() {
// saved stories and global shared stories don't have unreads // saved stories and global shared stories don't have unreads
if (fs!!.isAllSaved || fs!!.isGlobalShared) return 0 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 return if (result < 0) 0 else result
} }

View file

@ -7,6 +7,8 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.os.CancellationSignal; import android.os.CancellationSignal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
@ -46,13 +48,13 @@ import java.util.concurrent.Executors;
/** /**
* Utility class for executing DB operations on the local, private NB database. * 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 * It is the intent of this class to be the single location of SQL executed on
* our DB, replacing the deprecated ContentProvider access pattern. * our DB, replacing the deprecated ContentProvider access pattern.
*/ */
public class BlurDatabaseHelper { 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(); public final static Object RW_MUTEX = new Object();
private final BlurDatabase dbWrapper; private final BlurDatabase dbWrapper;
@ -85,6 +87,7 @@ public class BlurDatabaseHelper {
com.newsblur.util.Log.i(this.getClass().getName(), ". . . tables recreated."); com.newsblur.util.Log.i(this.getClass().getName(), ". . . tables recreated.");
} }
@Nullable
public String getEngineVersion() { public String getEngineVersion() {
String engineVersion = ""; String engineVersion = "";
try { try {
@ -99,10 +102,12 @@ public class BlurDatabaseHelper {
return engineVersion; return engineVersion;
} }
@NonNull
public Set<String> getAllFeeds() { public Set<String> getAllFeeds() {
return getAllFeeds(false); return getAllFeeds(false);
} }
@NonNull
private Set<String> getAllFeeds(boolean activeOnly) { private Set<String> getAllFeeds(boolean activeOnly) {
String q1 = "SELECT " + DatabaseConstants.FEED_ID + String q1 = "SELECT " + DatabaseConstants.FEED_ID +
" FROM " + DatabaseConstants.FEED_TABLE; " FROM " + DatabaseConstants.FEED_TABLE;
@ -118,10 +123,12 @@ public class BlurDatabaseHelper {
return feedIds; return feedIds;
} }
@NonNull
public Set<String> getAllActiveFeeds() { public Set<String> getAllActiveFeeds() {
return getAllFeeds(true); return getAllFeeds(true);
} }
@NonNull
private List<String> getAllSocialFeeds() { private List<String> getAllSocialFeeds() {
String q1 = "SELECT " + DatabaseConstants.SOCIAL_FEED_ID + String q1 = "SELECT " + DatabaseConstants.SOCIAL_FEED_ID +
" FROM " + DatabaseConstants.SOCIALFEED_TABLE; " FROM " + DatabaseConstants.SOCIALFEED_TABLE;
@ -181,20 +188,20 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {dbRW.execSQL("VACUUM");} synchronized (RW_MUTEX) {dbRW.execSQL("VACUUM");}
} }
public void deleteFeed(String feedId) { public void deleteFeed(@Nullable String feedId) {
String[] selArgs = new 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.FEED_TABLE, DatabaseConstants.FEED_ID + " = ?", selArgs);}
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TABLE, DatabaseConstants.STORY_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}; String[] selArgs = new String[] {userId};
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SOCIALFEED_TABLE, DatabaseConstants.SOCIAL_FEED_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.STORY_TABLE, DatabaseConstants.STORY_FEED_ID + " = ?", selArgs);}
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, DatabaseConstants.SOCIALFEED_STORY_USER_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 + String q = "DELETE FROM " + DatabaseConstants.SAVED_SEARCH_TABLE +
" WHERE " + DatabaseConstants.SAVED_SEARCH_FEED_ID + " = '" + feedId + "'" + " WHERE " + DatabaseConstants.SAVED_SEARCH_FEED_ID + " = '" + feedId + "'" +
" AND " + DatabaseConstants.SAVED_SEARCH_QUERY + " = '" + query + "'"; " AND " + DatabaseConstants.SAVED_SEARCH_QUERY + " = '" + query + "'";
@ -207,7 +214,8 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.STORY_TEXT_TABLE, null, null);} 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); Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE, null, DatabaseConstants.FEED_ID + " = ?", new String[] {feedId}, null, null, null);
Feed result = null; Feed result = null;
while (c.moveToNext()) { while (c.moveToNext()) {
@ -217,13 +225,13 @@ public class BlurDatabaseHelper {
return result; return result;
} }
public void updateFeed(Feed feed) { public void updateFeed(@NonNull Feed feed) {
synchronized (RW_MUTEX) { 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<ContentValues> valuesList) { private void bulkInsertValues(@NonNull String table, @NonNull List<ContentValues> valuesList) {
if (valuesList.size() < 1) return; if (valuesList.size() < 1) return;
synchronized (RW_MUTEX) { synchronized (RW_MUTEX) {
dbRW.beginTransaction(); dbRW.beginTransaction();
@ -239,18 +247,18 @@ public class BlurDatabaseHelper {
} }
// just like bulkInsertValues, but leaves sync/transactioning to the caller // just like bulkInsertValues, but leaves sync/transactioning to the caller
private void bulkInsertValuesExtSync(String table, List<ContentValues> valuesList) { private void bulkInsertValuesExtSync(@NonNull String table, @NonNull List<ContentValues> valuesList) {
if (valuesList.size() < 1) return; if (valuesList.size() < 1) return;
for (ContentValues values : valuesList) { for (ContentValues values : valuesList) {
dbRW.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); dbRW.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
} }
} }
public void setFeedsFolders(List<ContentValues> folderValues, public void setFeedsFolders(@NonNull List<ContentValues> folderValues,
List<ContentValues> feedValues, @NonNull List<ContentValues> feedValues,
List<ContentValues> socialFeedValues, @NonNull List<ContentValues> socialFeedValues,
List<ContentValues> starredCountValues, @NonNull List<ContentValues> starredCountValues,
List<ContentValues> savedSearchValues) { @NonNull List<ContentValues> savedSearchValues) {
synchronized (RW_MUTEX) { synchronized (RW_MUTEX) {
dbRW.beginTransaction(); dbRW.beginTransaction();
try { 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 // 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! // spend the up-front cost of hashing for better lookup speed rather than iteration!
@NonNull
public Set<String> getUnreadStoryHashesAsSet() { public Set<String> getUnreadStoryHashesAsSet() {
String q = "SELECT " + DatabaseConstants.STORY_HASH + String q = "SELECT " + DatabaseConstants.STORY_HASH +
" FROM " + DatabaseConstants.STORY_TABLE + " FROM " + DatabaseConstants.STORY_TABLE +
@ -289,6 +298,7 @@ public class BlurDatabaseHelper {
return hashes; return hashes;
} }
@NonNull
public Set<String> getStarredStoryHashes() { public Set<String> getStarredStoryHashes() {
String q = "SELECT " + DatabaseConstants.STORY_HASH + String q = "SELECT " + DatabaseConstants.STORY_HASH +
" FROM " + DatabaseConstants.STORY_TABLE + " FROM " + DatabaseConstants.STORY_TABLE +
@ -302,6 +312,7 @@ public class BlurDatabaseHelper {
return hashes; return hashes;
} }
@NonNull
public Set<String> getAllStoryImages() { public Set<String> getAllStoryImages() {
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_IMAGE_URLS}, null, null, null, null, null); Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_IMAGE_URLS}, null, null, null, null, null);
Set<String> urls = new HashSet<String>(c.getCount()); Set<String> urls = new HashSet<String>(c.getCount());
@ -312,6 +323,7 @@ public class BlurDatabaseHelper {
return urls; return urls;
} }
@NonNull
public Set<String> getAllStoryThumbnails() { public Set<String> getAllStoryThumbnails() {
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_THUMBNAIL_URL}, null, null, null, null, null); Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_THUMBNAIL_URL}, null, null, null, null, null);
Set<String> urls = new HashSet<String>(c.getCount()); Set<String> urls = new HashSet<String>(c.getCount());
@ -325,7 +337,7 @@ public class BlurDatabaseHelper {
return urls; 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) { synchronized (RW_MUTEX) {
// do not attempt to use beginTransactionNonExclusive() to reduce lock time for this very heavy set // 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 // 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 // pick a thumbnail for the story
story.thumbnailUrl = Story.guessStoryThumbnailURL(story); story.thumbnailUrl = Story.guessStoryThumbnailURL(story);
// insert the story data // 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 // real comments replace placeholders
int count = dbRW.delete(DatabaseConstants.COMMENT_TABLE, DatabaseConstants.COMMENT_ISPLACEHOLDER + " = ?", new String[]{"true"}); 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 // 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 * to reflect a social action, but that the new copy is missing some fields. Attempt to merge the
* new story with the old one. * 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) { if (apiResponse.story == null) {
com.newsblur.util.Log.e(this, "updateStory called on response with missing single story"); com.newsblur.util.Log.e(this, "updateStory called on response with missing single story");
return; return;
@ -503,7 +515,7 @@ public class BlurDatabaseHelper {
* Update an existing comment and associated replies based upon a new copy received from a social * 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. * 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) { synchronized (RW_MUTEX) {
// comments often contain enclosed replies, so batch them. // comments often contain enclosed replies, so batch them.
dbRW.beginTransaction(); 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 // start off with feeds mentioned by the set of stories
Set<String> feedIds = new HashSet<String>(); Set<String> feedIds = new HashSet<String>();
for (Story story : stories) { for (Story story : stories) {
@ -564,7 +576,7 @@ public class BlurDatabaseHelper {
} }
} }
public Folder getFolder(String folderName) { public Folder getFolder(@NonNull String folderName) {
String[] selArgs = new String[] {folderName}; String[] selArgs = new String[] {folderName};
String selection = DatabaseConstants.FOLDER_NAME + " = ?"; String selection = DatabaseConstants.FOLDER_NAME + " = ?";
Cursor c = dbRO.query(DatabaseConstants.FOLDER_TABLE, null, selection, selArgs, null, null, null); Cursor c = dbRO.query(DatabaseConstants.FOLDER_TABLE, null, selection, selArgs, null, null, null);
@ -577,13 +589,13 @@ public class BlurDatabaseHelper {
return folder; return folder;
} }
public void touchStory(String hash) { public void touchStory(@Nullable String hash) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_LAST_READ_DATE, (new Date()).getTime()); 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});} 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<String> hashes) { public void markStoryHashesRead(@NonNull Collection<String> hashes) {
synchronized (RW_MUTEX) { synchronized (RW_MUTEX) {
dbRW.beginTransaction(); dbRW.beginTransaction();
try { try {
@ -599,7 +611,7 @@ public class BlurDatabaseHelper {
} }
} }
public void markStoryHashesStarred(Collection<String> hashes, boolean isStarred) { public void markStoryHashesStarred(@NonNull Collection<String> hashes, boolean isStarred) {
synchronized (RW_MUTEX) { synchronized (RW_MUTEX) {
dbRW.beginTransaction(); dbRW.beginTransaction();
try { try {
@ -615,7 +627,7 @@ public class BlurDatabaseHelper {
} }
} }
public void setFeedsActive(Set<String> feedIds, boolean active) { public void setFeedsActive(@NonNull Set<String> feedIds, boolean active) {
synchronized (RW_MUTEX) { synchronized (RW_MUTEX) {
dbRW.beginTransaction(); dbRW.beginTransaction();
try { try {
@ -631,13 +643,13 @@ public class BlurDatabaseHelper {
} }
} }
public void setFeedFetchPending(String feedId) { public void setFeedFetchPending(@NonNull String feedId) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.FEED_FETCH_PENDING, true); values.put(DatabaseConstants.FEED_FETCH_PENDING, true);
synchronized (RW_MUTEX) {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) { public boolean isFeedSetFetchPending(@NonNull FeedSet fs) {
if (fs.getSingleFeed() != null) { if (fs.getSingleFeed() != null) {
String feedId = fs.getSingleFeed(); String feedId = fs.getSingleFeed();
Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE, Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE,
@ -657,7 +669,7 @@ public class BlurDatabaseHelper {
/** /**
* Marks a story (un)read but does not adjust counts. Must stay idempotent an time-insensitive. * 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(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_READ, read); values.put(DatabaseConstants.STORY_READ, read);
synchronized (RW_MUTEX) {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});}
@ -668,7 +680,8 @@ public class BlurDatabaseHelper {
* *
* @return the set of feed IDs that potentially have counts impacted by the mark. * @return the set of feed IDs that potentially have counts impacted by the mark.
*/ */
public Set<FeedSet> setStoryReadState(Story story, boolean read) { @NonNull
public Set<FeedSet> setStoryReadState(@NonNull Story story, boolean read) {
// calculate the impact surface so the caller can re-check counts if needed // calculate the impact surface so the caller can re-check counts if needed
Set<FeedSet> impactedFeeds = new HashSet<FeedSet>(); Set<FeedSet> impactedFeeds = new HashSet<FeedSet>();
impactedFeeds.add(FeedSet.singleFeed(story.feedId)); impactedFeeds.add(FeedSet.singleFeed(story.feedId));
@ -746,7 +759,7 @@ public class BlurDatabaseHelper {
* Marks a range of stories in a subset of feeds as read. Does not update unread counts; * 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. * 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(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_READ, true); values.put(DatabaseConstants.STORY_READ, true);
String rangeSelection = null; String rangeSelection = null;
@ -774,7 +787,7 @@ public class BlurDatabaseHelper {
/** /**
* Get the unread count for the given feedset based on the totals in the feeds table. * 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 reading in starred-only mode, there are no unreads, since stories vended as starred are never unread
if (fs.isFilterSaved()) return 0; if (fs.isFilterSaved()) return 0;
if (fs.isAllNormal()) { if (fs.isAllNormal()) {
@ -802,7 +815,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; int result = 0;
Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE, null, selection, selArgs, null, null, null); Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE, null, selection, selArgs, null, null, null);
while (c.moveToNext()) { while (c.moveToNext()) {
@ -816,7 +829,7 @@ public class BlurDatabaseHelper {
return result; 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; int result = 0;
Cursor c = dbRO.query(DatabaseConstants.SOCIALFEED_TABLE, null, selection, selArgs, null, null, null); Cursor c = dbRO.query(DatabaseConstants.SOCIALFEED_TABLE, null, selection, selArgs, null, null, null);
while (c.moveToNext()) { while (c.moveToNext()) {
@ -829,18 +842,18 @@ public class BlurDatabaseHelper {
return result; 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});} 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});} 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. * 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 // decompose the FeedSet into a list of single feeds that need to be recounted
List<String> feedIds = new ArrayList<String>(); List<String> feedIds = new ArrayList<String>();
List<String> socialFeedIds = new ArrayList<String>(); List<String> socialFeedIds = new ArrayList<String>();
@ -883,7 +896,7 @@ public class BlurDatabaseHelper {
/** /**
* Get the unread count for the given feedset based on local story state. * 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(); StringBuilder sel = new StringBuilder();
ArrayList<String> selArgs = new ArrayList<String>(); ArrayList<String> selArgs = new ArrayList<String>();
getLocalStorySelectionAndArgs(sel, selArgs, fs, stateFilter, ReadFilter.UNREAD); getLocalStorySelectionAndArgs(sel, selArgs, fs, stateFilter, ReadFilter.UNREAD);
@ -900,16 +913,17 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, null, null);} 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());} synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.ACTION_TABLE, null, ra.toContentValues());}
} }
@NonNull
public Cursor getActions() { public Cursor getActions() {
String q = "SELECT * FROM " + DatabaseConstants.ACTION_TABLE; String q = "SELECT * FROM " + DatabaseConstants.ACTION_TABLE;
return dbRO.rawQuery(q, null); return dbRO.rawQuery(q, null);
} }
public void incrementActionTried(String actionId) { public void incrementActionTried(@Nullable String actionId) {
synchronized (RW_MUTEX) { synchronized (RW_MUTEX) {
String q = "UPDATE " + DatabaseConstants.ACTION_TABLE + String q = "UPDATE " + DatabaseConstants.ACTION_TABLE +
" SET " + DatabaseConstants.ACTION_TRIED + " = " + DatabaseConstants.ACTION_TRIED + " + 1" + " SET " + DatabaseConstants.ACTION_TRIED + " = " + DatabaseConstants.ACTION_TRIED + " + 1" +
@ -926,7 +940,7 @@ public class BlurDatabaseHelper {
return result; 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});} synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.ACTION_TABLE, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId});}
} }
@ -978,7 +992,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 // 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, Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE,
new String[]{DatabaseConstants.STORY_SHARED_USER_IDS}, new String[]{DatabaseConstants.STORY_SHARED_USER_IDS},
@ -1007,7 +1021,8 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {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) { @Nullable
public String getStoryText(@Nullable String hash) {
String q = "SELECT " + DatabaseConstants.STORY_TEXT_STORY_TEXT + String q = "SELECT " + DatabaseConstants.STORY_TEXT_STORY_TEXT +
" FROM " + DatabaseConstants.STORY_TEXT_TABLE + " FROM " + DatabaseConstants.STORY_TEXT_TABLE +
" WHERE " + DatabaseConstants.STORY_TEXT_STORY_HASH + " = ?"; " WHERE " + DatabaseConstants.STORY_TEXT_STORY_HASH + " = ?";
@ -1023,7 +1038,8 @@ public class BlurDatabaseHelper {
} }
} }
public String getStoryContent(String hash) { @Nullable
public String getStoryContent(@Nullable String hash) {
String q = "SELECT " + DatabaseConstants.STORY_CONTENT + String q = "SELECT " + DatabaseConstants.STORY_CONTENT +
" FROM " + DatabaseConstants.STORY_TABLE + " FROM " + DatabaseConstants.STORY_TABLE +
" WHERE " + DatabaseConstants.STORY_HASH + " = ?"; " WHERE " + DatabaseConstants.STORY_HASH + " = ?";
@ -1040,18 +1056,20 @@ public class BlurDatabaseHelper {
} }
} }
public void putStoryText(String hash, String text) { public void putStoryText(@Nullable String hash, @NonNull String text) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_TEXT_STORY_HASH, hash); values.put(DatabaseConstants.STORY_TEXT_STORY_HASH, hash);
values.put(DatabaseConstants.STORY_TEXT_STORY_TEXT, text); values.put(DatabaseConstants.STORY_TEXT_STORY_TEXT, text);
synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.STORY_TEXT_TABLE, null, values);} 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); 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); Cursor c = dbRO.query(DatabaseConstants.SOCIALFEED_TABLE, null, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[] {feedId}, null, null, null);
SocialFeed result = null; SocialFeed result = null;
while (c.moveToNext()) { while (c.moveToNext()) {
@ -1062,7 +1080,7 @@ public class BlurDatabaseHelper {
} }
@Nullable @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); Cursor c = dbRO.query(DatabaseConstants.STARREDCOUNTS_TABLE, null, DatabaseConstants.STARREDCOUNTS_TAG + " = ?", new String[] {tag}, null, null, null);
StarredCount result = null; StarredCount result = null;
while (c.moveToNext()) { while (c.moveToNext()) {
@ -1082,30 +1100,37 @@ public class BlurDatabaseHelper {
return folders; 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); 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); 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); 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); return query(false, DatabaseConstants.SAVED_SEARCH_TABLE, null, null, null, null, null, null, null, cancellationSignal);
} }
@Nullable
public Cursor getNotifyFocusStoriesCursor() { public Cursor getNotifyFocusStoriesCursor() {
return rawQuery(DatabaseConstants.NOTIFY_FOCUS_STORY_QUERY, null, null); return rawQuery(DatabaseConstants.NOTIFY_FOCUS_STORY_QUERY, null, null);
} }
@Nullable
public Cursor getNotifyUnreadStoriesCursor() { public Cursor getNotifyUnreadStoriesCursor() {
return rawQuery(DatabaseConstants.NOTIFY_UNREAD_STORY_QUERY, null, null); return rawQuery(DatabaseConstants.NOTIFY_UNREAD_STORY_QUERY, null, null);
} }
@NonNull
public Set<String> getNotifyFeeds() { public Set<String> getNotifyFeeds() {
String q = "SELECT " + DatabaseConstants.FEED_ID + " FROM " + DatabaseConstants.FEED_TABLE + String q = "SELECT " + DatabaseConstants.FEED_ID + " FROM " + DatabaseConstants.FEED_TABLE +
" WHERE " + DatabaseConstants.FEED_NOTIFICATION_FILTER + " = '" + Feed.NOTIFY_FILTER_FOCUS + "'" + " WHERE " + DatabaseConstants.FEED_NOTIFICATION_FILTER + " = '" + Feed.NOTIFY_FILTER_FOCUS + "'" +
@ -1122,25 +1147,8 @@ public class BlurDatabaseHelper {
return feedIds; return feedIds;
} }
private Cursor getStoriesCursor(@Nullable FeedSet fs, CancellationSignal cancellationSignal) { @NonNull
StringBuilder q = new StringBuilder(DatabaseConstants.STORY_QUERY_BASE_0); public Cursor getActiveStoriesCursor(@NonNull FeedSet fs, @NonNull CursorFilters cursorFilters, @NonNull CancellationSignal cancellationSignal) {
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) {
// get the stories for this FS // get the stories for this FS
Cursor result = getActiveStoriesCursorNoPrep(fs, cursorFilters.getStoryOrder(), cancellationSignal); Cursor result = getActiveStoriesCursorNoPrep(fs, cursorFilters.getStoryOrder(), cancellationSignal);
// if the result is blank, try to prime the session table with existing stories, in case we // if the result is blank, try to prime the session table with existing stories, in case we
@ -1153,8 +1161,9 @@ public class BlurDatabaseHelper {
} }
return result; 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, // 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 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 // those hashes are used to pull story data from the story table
@ -1182,7 +1191,7 @@ public class BlurDatabaseHelper {
* criteria for the given FeedSet and filters; these hashes will be supplemented by hashes * 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. * 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 // 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(); StringBuilder sel = new StringBuilder();
// any selection args that need to be used within the inner select statement // any selection args that need to be used within the inner select statement
@ -1202,7 +1211,7 @@ public class BlurDatabaseHelper {
* Gets hashes of already-fetched stories that satisfy the given FeedSet and filters. Can be used * 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. * both to populate a reading session or to count local unreads.
*/ */
private void getLocalStorySelectionAndArgs(StringBuilder sel, List<String> selArgs, FeedSet fs, StateFilter stateFilter, ReadFilter readFilter) { private void getLocalStorySelectionAndArgs(@NonNull StringBuilder sel, @NonNull List<String> 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 the user has requested saved stories, ignore the unreads filter, as saveds do not have this state
if (fs.isFilterSaved()) { if (fs.isFilterSaved()) {
readFilter = ReadFilter.ALL; readFilter = ReadFilter.ALL;
@ -1283,7 +1292,7 @@ public class BlurDatabaseHelper {
} }
} }
public void setSessionFeedSet(FeedSet fs) { public void setSessionFeedSet(@Nullable FeedSet fs) {
if (fs == null) { 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});} synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.SYNC_METADATA_TABLE, DatabaseConstants.SYNC_METADATA_KEY + " = ?", new String[] {DatabaseConstants.SYNC_METADATA_KEY_SESSION_FEED_SET});}
} else { } else {
@ -1293,9 +1302,10 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {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);}
} }
} }
@Nullable
public FeedSet getSessionFeedSet() { 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); 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; if (c.getCount() < 1) return null;
c.moveToFirst(); c.moveToFirst();
@ -1304,20 +1314,21 @@ public class BlurDatabaseHelper {
return fs; return fs;
} }
public boolean isFeedSetReady(FeedSet fs) { public boolean isFeedSetReady(@Nullable FeedSet fs) {
return fs.equals(getSessionFeedSet()); return fs.equals(getSessionFeedSet());
} }
public void clearClassifiersForFeed(String feedId) { public void clearClassifiersForFeed(@Nullable String feedId) {
String[] selArgs = new String[] {feedId}; String[] selArgs = new String[] {feedId};
synchronized (RW_MUTEX) {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) { public void insertClassifier(@NonNull Classifier classifier) {
bulkInsertValues(DatabaseConstants.CLASSIFIER_TABLE, classifier.getContentValues()); bulkInsertValues(DatabaseConstants.CLASSIFIER_TABLE, classifier.getContentValues());
} }
public Classifier getClassifierForFeed(String feedId) { @NonNull
public Classifier getClassifierForFeed(@Nullable String feedId) {
String[] selArgs = new String[] {feedId}; String[] selArgs = new String[] {feedId};
Cursor c = dbRO.query(DatabaseConstants.CLASSIFIER_TABLE, null, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs, null, null, null); Cursor c = dbRO.query(DatabaseConstants.CLASSIFIER_TABLE, null, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs, null, null, null);
Classifier classifier = Classifier.fromCursor(c); Classifier classifier = Classifier.fromCursor(c);
@ -1326,7 +1337,8 @@ public class BlurDatabaseHelper {
return classifier; return classifier;
} }
public List<Comment> getComments(String storyId) { @NonNull
public List<Comment> getComments(@NonNull String storyId) {
String[] selArgs = new String[] {storyId}; String[] selArgs = new String[] {storyId};
String selection = DatabaseConstants.COMMENT_STORYID + " = ?"; String selection = DatabaseConstants.COMMENT_STORYID + " = ?";
Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, null, selection, selArgs, null, null, null); Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, null, selection, selArgs, null, null, null);
@ -1338,7 +1350,8 @@ public class BlurDatabaseHelper {
return comments; 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 selection = DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?";
String[] selArgs = new String[] {storyId, userId}; String[] selArgs = new String[] {storyId, userId};
Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, null, selection, selArgs, null, null, null); Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, null, selection, selArgs, null, null, null);
@ -1354,7 +1367,7 @@ public class BlurDatabaseHelper {
* will show up in the UI with reduced functionality until the server gets back to us with * 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. * 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 comment = new Comment();
comment.isPlaceholder = true; comment.isPlaceholder = true;
comment.id = Comment.PLACEHOLDER_COMMENT_ID + storyId + userId; comment.id = Comment.PLACEHOLDER_COMMENT_ID + storyId + userId;
@ -1376,23 +1389,23 @@ public class BlurDatabaseHelper {
} }
} }
public void editReply(String replyId, String replyText) { public void editReply(@Nullable String replyId, @Nullable String replyText) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.REPLY_TEXT, replyText); values.put(DatabaseConstants.REPLY_TEXT, replyText);
synchronized (RW_MUTEX) {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) { public void deleteReply(@Nullable String replyId) {
synchronized (RW_MUTEX) {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) { public void clearSelfComments(@Nullable String storyId, @Nullable String userId) {
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.COMMENT_TABLE, synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.COMMENT_TABLE,
DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?", DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?",
new String[]{storyId, 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 // 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, Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE,
null, null,
@ -1421,7 +1434,8 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {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) { @Nullable
public UserProfile getUserProfile(@Nullable String userId) {
String[] selArgs = new String[] {userId}; String[] selArgs = new String[] {userId};
String selection = DatabaseConstants.USER_USERID + " = ?"; String selection = DatabaseConstants.USER_USERID + " = ?";
Cursor c = dbRO.query(DatabaseConstants.USER_TABLE, null, selection, selArgs, null, null, null); Cursor c = dbRO.query(DatabaseConstants.USER_TABLE, null, selection, selArgs, null, null, null);
@ -1430,7 +1444,8 @@ public class BlurDatabaseHelper {
return profile; return profile;
} }
public List<Reply> getCommentReplies(String commentId) { @NonNull
public List<Reply> getCommentReplies(@Nullable String commentId) {
String[] selArgs = new String[] {commentId}; String[] selArgs = new String[] {commentId};
String selection = DatabaseConstants.REPLY_COMMENTID+ " = ?"; String selection = DatabaseConstants.REPLY_COMMENTID+ " = ?";
Cursor c = dbRO.query(DatabaseConstants.REPLY_TABLE, null, selection, selArgs, null, null, DatabaseConstants.REPLY_DATE + " ASC"); Cursor c = dbRO.query(DatabaseConstants.REPLY_TABLE, null, selection, selArgs, null, null, DatabaseConstants.REPLY_DATE + " ASC");
@ -1442,7 +1457,7 @@ public class BlurDatabaseHelper {
return replies; 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 // get a fresh copy of the comment so we can discover the ID
Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE,
null, null,
@ -1467,14 +1482,14 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {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) { public void putStoryDismissed(@Nullable String storyHash) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.NOTIFY_DISMISS_STORY_HASH, storyHash); values.put(DatabaseConstants.NOTIFY_DISMISS_STORY_HASH, storyHash);
values.put(DatabaseConstants.NOTIFY_DISMISS_TIME, Calendar.getInstance().getTime().getTime()); values.put(DatabaseConstants.NOTIFY_DISMISS_TIME, Calendar.getInstance().getTime().getTime());
synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, values);} 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[] selArgs = new String[] {storyHash};
String selection = DatabaseConstants.NOTIFY_DISMISS_STORY_HASH + " = ?"; String selection = DatabaseConstants.NOTIFY_DISMISS_STORY_HASH + " = ?";
Cursor c = dbRO.query(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, selection, selArgs, null, null, null); Cursor c = dbRO.query(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, selection, selArgs, null, null, null);
@ -1494,7 +1509,7 @@ public class BlurDatabaseHelper {
} }
} }
private void putFeedTagsExtSync(String feedId, Collection<String> tags) { private void putFeedTagsExtSync(@Nullable String feedId, @NonNull Collection<String> tags) {
dbRW.delete(DatabaseConstants.FEED_TAGS_TABLE, dbRW.delete(DatabaseConstants.FEED_TAGS_TABLE,
DatabaseConstants.FEED_TAGS_FEEDID + " = ?", DatabaseConstants.FEED_TAGS_FEEDID + " = ?",
new String[]{feedId} new String[]{feedId}
@ -1509,7 +1524,8 @@ public class BlurDatabaseHelper {
bulkInsertValuesExtSync(DatabaseConstants.FEED_TAGS_TABLE, valuesList); bulkInsertValuesExtSync(DatabaseConstants.FEED_TAGS_TABLE, valuesList);
} }
public List<String> getTagsForFeed(String feedId) { @NonNull
public List<String> getTagsForFeed(@Nullable String feedId) {
Cursor c = dbRO.query(DatabaseConstants.FEED_TAGS_TABLE, Cursor c = dbRO.query(DatabaseConstants.FEED_TAGS_TABLE,
new String[]{DatabaseConstants.FEED_TAGS_TAG}, new String[]{DatabaseConstants.FEED_TAGS_TAG},
DatabaseConstants.FEED_TAGS_FEEDID + " = ?", DatabaseConstants.FEED_TAGS_FEEDID + " = ?",
@ -1526,7 +1542,7 @@ public class BlurDatabaseHelper {
return result; return result;
} }
private void putFeedAuthorsExtSync(String feedId, Collection<String> authors) { private void putFeedAuthorsExtSync(@Nullable String feedId, @NonNull Collection<String> authors) {
dbRW.delete(DatabaseConstants.FEED_AUTHORS_TABLE, dbRW.delete(DatabaseConstants.FEED_AUTHORS_TABLE,
DatabaseConstants.FEED_AUTHORS_FEEDID + " = ?", DatabaseConstants.FEED_AUTHORS_FEEDID + " = ?",
new String[]{feedId} new String[]{feedId}
@ -1541,7 +1557,8 @@ public class BlurDatabaseHelper {
bulkInsertValuesExtSync(DatabaseConstants.FEED_AUTHORS_TABLE, valuesList); bulkInsertValuesExtSync(DatabaseConstants.FEED_AUTHORS_TABLE, valuesList);
} }
public List<String> getAuthorsForFeed(String feedId) { @NonNull
public List<String> getAuthorsForFeed(@Nullable String feedId) {
Cursor c = dbRO.query(DatabaseConstants.FEED_AUTHORS_TABLE, Cursor c = dbRO.query(DatabaseConstants.FEED_AUTHORS_TABLE,
new String[]{DatabaseConstants.FEED_AUTHORS_AUTHOR}, new String[]{DatabaseConstants.FEED_AUTHORS_AUTHOR},
DatabaseConstants.FEED_AUTHORS_FEEDID + " = ?", DatabaseConstants.FEED_AUTHORS_FEEDID + " = ?",
@ -1558,18 +1575,19 @@ public class BlurDatabaseHelper {
return result; return result;
} }
public void renameFeed(String feedId, String newFeedName) { public void renameFeed(@Nullable String feedId, @Nullable String newFeedName) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.FEED_TITLE, newFeedName); values.put(DatabaseConstants.FEED_TITLE, newFeedName);
synchronized (RW_MUTEX) {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) { public static void closeQuietly(@Nullable Cursor c) {
if (c == null) return; if (c == null) return;
try {c.close();} catch (Exception e) { try {c.close();} catch (Exception e) {
} }
} }
@Nullable
private static String conjoinSelections(CharSequence... args) { private static String conjoinSelections(CharSequence... args) {
StringBuilder s = null; StringBuilder s = null;
for (CharSequence c : args) { for (CharSequence c : args) {
@ -1589,7 +1607,8 @@ public class BlurDatabaseHelper {
* Invoke the rawQuery() method on our read-only SQLiteDatabase memeber using the provided CancellationSignal * Invoke the rawQuery() method on our read-only SQLiteDatabase memeber using the provided CancellationSignal
* only if the device's platform provides support. * 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) { if (AppConstants.VERBOSE_LOG_DB) {
Log.d(this.getClass().getName(), String.format("DB rawQuery: '%s' with args: %s", sql, java.util.Arrays.toString(selectionArgs))); Log.d(this.getClass().getName(), String.format("DB rawQuery: '%s' with args: %s", sql, java.util.Arrays.toString(selectionArgs)));
} }
@ -1600,15 +1619,18 @@ public class BlurDatabaseHelper {
* Invoke the query() method on our read-only SQLiteDatabase memeber using the provided CancellationSignal * Invoke the query() method on our read-only SQLiteDatabase memeber using the provided CancellationSignal
* only if the device's platform provides support. * 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); 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)); return FeedSet.folder(folderName, getFeedIdsRecursive(folderName));
} }
private Set<String> getFeedIdsRecursive(String folderName) { @NonNull
private Set<String> getFeedIdsRecursive(@NonNull String folderName) {
Folder folder = getFolder(folderName); Folder folder = getFolder(folderName);
if (folder == null) return emptySet(); if (folder == null) return emptySet();
Set<String> feedIds = new HashSet<>(folder.feedIds); Set<String> feedIds = new HashSet<>(folder.feedIds);

View file

@ -275,7 +275,7 @@ public class ItemSetFragment extends NbFragment {
} }
protected FeedSet getFeedSet() { protected FeedSet getFeedSet() {
return ((ItemsList) getActivity()).getFeedSet(); return ((ItemsList) requireActivity()).getFeedSet();
} }
public void hasUpdated() { public void hasUpdated() {
@ -299,7 +299,7 @@ public class ItemSetFragment extends NbFragment {
ensureSufficientStories(); ensureSufficientStories();
} }
private void setCursor(Cursor cursor) { private void setCursor(@Nullable Cursor cursor) {
if (cursor != null) { if (cursor != null) {
if (!dbHelper.isFeedSetReady(getFeedSet())) { if (!dbHelper.isFeedSetReady(getFeedSet())) {
// the DB hasn't caught up yet from the last story list; don't display stale stories. // the DB hasn't caught up yet from the last story list; don't display stale stories.

View file

@ -1,6 +1,8 @@
package com.newsblur.fragment 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.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
@ -9,8 +11,12 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.view.* import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo 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 android.webkit.WebView.HitTestResult
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu 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_STORY
import com.newsblur.service.NbSyncManager.UPDATE_TEXT import com.newsblur.service.NbSyncManager.UPDATE_TEXT
import com.newsblur.service.OriginalTextService 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.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 dagger.hilt.android.AndroidEntryPoint
import java.util.regex.Pattern import java.util.regex.Pattern
import javax.inject.Inject import javax.inject.Inject
@ -107,6 +125,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
private lateinit var binding: FragmentReadingitemBinding private lateinit var binding: FragmentReadingitemBinding
private lateinit var readingItemActionsBinding: ReadingItemActionsBinding private lateinit var readingItemActionsBinding: ReadingItemActionsBinding
private lateinit var markStoryReadBehavior: MarkStoryReadBehavior private lateinit var markStoryReadBehavior: MarkStoryReadBehavior
private var sampledQueue: SampledQueue? = null private var sampledQueue: SampledQueue? = null
@ -148,13 +167,6 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
super.onDestroyView() 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 // WebViews don't automatically pause content like audio and video when they lose focus. Chain our own
// state into the webview so it behaves. // state into the webview so it behaves.
override fun onPause() { override fun onPause() {
@ -169,8 +181,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_readingitem, container, false) binding = FragmentReadingitemBinding.inflate(inflater, container, false)
binding = FragmentReadingitemBinding.bind(view)
readingItemActionsBinding = ReadingItemActionsBinding.bind(binding.root) readingItemActionsBinding = ReadingItemActionsBinding.bind(binding.root)
val readingActivity = requireActivity() as Reading val readingActivity = requireActivity() as Reading
@ -194,7 +205,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
binding.readingScrollview.registerScrollChangeListener(readingActivity) binding.readingScrollview.registerScrollChangeListener(readingActivity)
return view return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -423,8 +434,10 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
} }
R.id.menu_go_to_feed -> { R.id.menu_go_to_feed -> {
val feed = dbHelper.getFeed(story!!.feedId) val feed = dbHelper.getFeed(story!!.feedId)
val fs = FeedSet.singleFeed(feed.feedId) feed?.let {
FeedItemsList.startActivity(requireContext(), fs, feed, null, null) val fs = FeedSet.singleFeed(it.feedId)
FeedItemsList.startActivity(requireContext(), fs, it, null, null)
}
true true
} }
else -> { else -> {

View file

@ -56,7 +56,7 @@ class SetupCommentSectionTask(private val fragment: ReadingItemFragment, view: V
private fun doInBackground() { private fun doInBackground() {
if (context == null || story == null || story.id.isNullOrEmpty()) return 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 // users by whom we saw non-pseudo comments
val commentingUserIds: MutableSet<String> = HashSet() val commentingUserIds: MutableSet<String> = HashSet()

View file

@ -144,7 +144,7 @@ public class NotificationUtils {
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, context.getString(R.string.story_notification_channel_id)) NotificationCompat.Builder nb = new NotificationCompat.Builder(context, context.getString(R.string.story_notification_channel_id))
.setContentTitle(title.toString()) .setContentTitle(title.toString())
.setContentText(story.shortContent) .setContentText(story.shortContent)
.setSmallIcon(R.drawable.logo_monochrome) .setSmallIcon(R.drawable.ic_logo_monochrome)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setDeleteIntent(dismissPendingIntent) .setDeleteIntent(dismissPendingIntent)
.setAutoCancel(true) .setAutoCancel(true)

View file

@ -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)
}
}
}

View file

@ -9,6 +9,9 @@ import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.Serializable; import java.io.Serializable;
import com.newsblur.database.BlurDatabaseHelper; import com.newsblur.database.BlurDatabaseHelper;
@ -88,21 +91,21 @@ public class ReadingAction implements Serializable {
return tried; return tried;
} }
public static ReadingAction markStoryRead(String hash) { public static ReadingAction markStoryRead(@Nullable String hash) {
ReadingAction ra = new ReadingAction(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.MARK_READ; ra.type = ActionType.MARK_READ;
ra.storyHash = hash; ra.storyHash = hash;
return ra; return ra;
} }
public static ReadingAction markStoryUnread(String hash) { public static ReadingAction markStoryUnread(@Nullable String hash) {
ReadingAction ra = new ReadingAction(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.MARK_UNREAD; ra.type = ActionType.MARK_UNREAD;
ra.storyHash = hash; ra.storyHash = hash;
return ra; return ra;
} }
public static ReadingAction saveStory(String hash, List<String> userTags) { public static ReadingAction saveStory(@Nullable String hash, @Nullable List<String> userTags) {
ReadingAction ra = new ReadingAction(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.SAVE; ra.type = ActionType.SAVE;
ra.storyHash = hash; ra.storyHash = hash;
@ -114,14 +117,14 @@ public class ReadingAction implements Serializable {
return ra; return ra;
} }
public static ReadingAction unsaveStory(String hash) { public static ReadingAction unsaveStory(@Nullable String hash) {
ReadingAction ra = new ReadingAction(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.UNSAVE; ra.type = ActionType.UNSAVE;
ra.storyHash = hash; ra.storyHash = hash;
return ra; 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(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.MARK_READ; ra.type = ActionType.MARK_READ;
ra.feedSet = fs; ra.feedSet = fs;
@ -130,7 +133,7 @@ public class ReadingAction implements Serializable {
return ra; 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(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.SHARE; ra.type = ActionType.SHARE;
ra.storyHash = hash; ra.storyHash = hash;
@ -141,7 +144,7 @@ public class ReadingAction implements Serializable {
return ra; 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(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.UNSHARE; ra.type = ActionType.UNSHARE;
ra.storyHash = hash; ra.storyHash = hash;
@ -150,7 +153,7 @@ public class ReadingAction implements Serializable {
return ra; 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(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.LIKE_COMMENT; ra.type = ActionType.LIKE_COMMENT;
ra.storyId = storyId; ra.storyId = storyId;
@ -159,7 +162,7 @@ public class ReadingAction implements Serializable {
return ra; 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(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.UNLIKE_COMMENT; ra.type = ActionType.UNLIKE_COMMENT;
ra.storyId = storyId; ra.storyId = storyId;
@ -168,7 +171,7 @@ public class ReadingAction implements Serializable {
return ra; 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(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.REPLY; ra.type = ActionType.REPLY;
ra.storyId = storyId; ra.storyId = storyId;
@ -178,7 +181,7 @@ public class ReadingAction implements Serializable {
return ra; 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(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.EDIT_REPLY; ra.type = ActionType.EDIT_REPLY;
ra.storyId = storyId; ra.storyId = storyId;
@ -189,7 +192,7 @@ public class ReadingAction implements Serializable {
return ra; 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(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.DELETE_REPLY; ra.type = ActionType.DELETE_REPLY;
ra.storyId = storyId; ra.storyId = storyId;
@ -199,7 +202,7 @@ public class ReadingAction implements Serializable {
return ra; return ra;
} }
public static ReadingAction muteFeeds(Set<String> activeFeedIds, Set<String> modifiedFeedIds) { public static ReadingAction muteFeeds(@NonNull Set<String> activeFeedIds, @NonNull Set<String> modifiedFeedIds) {
ReadingAction ra = new ReadingAction(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.MUTE_FEEDS; ra.type = ActionType.MUTE_FEEDS;
ra.activeFeedIds = activeFeedIds; ra.activeFeedIds = activeFeedIds;
@ -207,7 +210,7 @@ public class ReadingAction implements Serializable {
return ra; return ra;
} }
public static ReadingAction unmuteFeeds(Set<String> activeFeedIds, Set<String> modifiedFeedIds) { public static ReadingAction unmuteFeeds(@NonNull Set<String> activeFeedIds, @NonNull Set<String> modifiedFeedIds) {
ReadingAction ra = new ReadingAction(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.UNMUTE_FEEDS; ra.type = ActionType.UNMUTE_FEEDS;
ra.activeFeedIds = activeFeedIds; ra.activeFeedIds = activeFeedIds;
@ -215,7 +218,7 @@ public class ReadingAction implements Serializable {
return ra; return ra;
} }
public static ReadingAction setNotify(String feedId, List<String> notifyTypes, String notifyFilter) { public static ReadingAction setNotify(@Nullable String feedId, @Nullable List<String> notifyTypes, @Nullable String notifyFilter) {
ReadingAction ra = new ReadingAction(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.SET_NOTIFY; ra.type = ActionType.SET_NOTIFY;
ra.feedId = feedId; ra.feedId = feedId;
@ -228,14 +231,14 @@ public class ReadingAction implements Serializable {
return ra; return ra;
} }
public static ReadingAction instaFetch(String feedId) { public static ReadingAction instaFetch(@Nullable String feedId) {
ReadingAction ra = new ReadingAction(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.INSTA_FETCH; ra.type = ActionType.INSTA_FETCH;
ra.feedId = feedId; ra.feedId = feedId;
return ra; 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(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.UPDATE_INTEL; ra.type = ActionType.UPDATE_INTEL;
ra.feedId = feedId; ra.feedId = feedId;
@ -244,7 +247,7 @@ public class ReadingAction implements Serializable {
return ra; return ra;
} }
public static ReadingAction renameFeed(String feedId, String newFeedName) { public static ReadingAction renameFeed(@Nullable String feedId, @Nullable String newFeedName) {
ReadingAction ra = new ReadingAction(); ReadingAction ra = new ReadingAction();
ra.type = ActionType.RENAME_FEED; ra.type = ActionType.RENAME_FEED;
ra.feedId = feedId; ra.feedId = feedId;
@ -265,7 +268,7 @@ public class ReadingAction implements Serializable {
return values; return values;
} }
public static ReadingAction fromCursor(Cursor c) { public static ReadingAction fromCursor(@NonNull Cursor c) {
long time = c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TIME)); long time = c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TIME));
int tried = c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TRIED)); int tried = c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TRIED));
String params = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_PARAMS)); 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. * 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 // generic response to return
NewsBlurResponse result = null; NewsBlurResponse result = null;
// optional specific responses that are locally actionable // optional specific responses that are locally actionable
@ -392,7 +395,7 @@ public class ReadingAction implements Serializable {
return result; return result;
} }
public int doLocal(Context context, BlurDatabaseHelper dbHelper) { public int doLocal(@NonNull Context context, @NonNull BlurDatabaseHelper dbHelper) {
return doLocal(context, dbHelper, false); 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. * @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); String userId = PrefsUtils.getUserId(context);
int impact = 0; int impact = 0;
switch (type) { switch (type) {

View file

@ -232,10 +232,10 @@ public class UIUtils {
public void run() { public void run() {
Intent intent = activity.getIntent(); Intent intent = activity.getIntent();
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); 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.finish();
activity.overridePendingTransition(0, 0); PendingTransitionUtils.overrideNoEnterTransition(activity);
activity.startActivity(intent); activity.startActivity(intent);
} }
}); });

View file

@ -2,6 +2,7 @@ package com.newsblur.viewModel
import android.database.Cursor import android.database.Cursor
import android.os.CancellationSignal import android.os.CancellationSignal
import android.os.OperationCanceledException
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -9,6 +10,7 @@ import androidx.lifecycle.viewModelScope
import com.newsblur.database.BlurDatabaseHelper import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.util.CursorFilters import com.newsblur.util.CursorFilters
import com.newsblur.util.FeedSet import com.newsblur.util.FeedSet
import com.newsblur.util.Log
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -24,8 +26,12 @@ class StoriesViewModel
fun getActiveStories(fs: FeedSet, cursorFilters: CursorFilters) { fun getActiveStories(fs: FeedSet, cursorFilters: CursorFilters) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
dbHelper.getActiveStoriesCursor(fs, cursorFilters, cancellationSignal).let { try {
_activeStoriesLiveData.postValue(it) dbHelper.getActiveStoriesCursor(fs, cursorFilters, cancellationSignal).let {
_activeStoriesLiveData.postValue(it)
}
} catch (e: OperationCanceledException) {
Log.e(this.javaClass.name, "Caught ${e.javaClass.name} in getActiveStories.")
} }
} }
} }

View file

@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="48dp"
android:height="48dp"
android:viewportWidth="432"
android:viewportHeight="432">
<path android:pathData="M0,0h432v432h-432z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="216"
android:endY="432"
android:startX="216"
android:startY="0"
android:type="linear">
<item
android:color="#FFE6A33F"
android:offset="0" />
<item
android:color="#FFC35F28"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
</vector>

View file

@ -0,0 +1,51 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="48dp"
android:height="48dp"
android:viewportWidth="432"
android:viewportHeight="432">
<path
android:fillType="evenOdd"
android:pathData="M237.9,135.78L243.89,148.68L257.15,143.83C270.23,151.34 281.11,162.28 288.54,175.42L283.74,188.53L296.37,194.4C298.31,201.45 299.34,208.88 299.34,216.55C299.34,224.35 298.27,231.9 296.27,239.07L283.74,244.89L288.46,257.8C280.97,271 270,281.97 256.8,289.46L243.89,284.74L238.07,297.27C230.9,299.27 223.35,300.34 215.55,300.34C207.88,300.34 200.45,299.31 193.39,297.37L187.53,284.74L174.42,289.54C161.28,282.11 150.34,271.23 142.83,258.15L147.68,244.89L134.78,238.9C132.8,231.78 131.75,224.28 131.75,216.55C131.75,208.94 132.77,201.58 134.67,194.57L147.68,188.53L142.75,175.07C150.2,162.05 161.04,151.2 174.07,143.75L187.53,148.68L193.57,135.67C200.57,133.77 207.94,132.75 215.55,132.75C223.28,132.75 230.78,133.8 237.9,135.78ZM263.87,141.37C274.76,148.4 284.04,157.73 291.01,168.67L295.09,157.49C289.4,149.84 282.62,143.03 275,137.3L263.87,141.37ZM302.88,197.42L313.65,202.42C314.31,207.03 314.65,211.75 314.65,216.55C314.65,221.46 314.29,226.3 313.6,231.02L302.8,236.04C304.2,229.76 304.94,223.24 304.94,216.55C304.94,209.98 304.23,203.58 302.88,197.42ZM321.27,216.55C321.27,220.32 321.07,224.06 320.68,227.74L330.49,223.18C330.61,220.99 330.68,218.77 330.68,216.55C330.68,214.43 330.62,212.33 330.51,210.25L320.71,205.7C321.08,209.27 321.27,212.88 321.27,216.55ZM304.48,131.84L306.72,125.7L300.59,127.94C301.2,128.52 301.8,129.11 302.39,129.7C303.1,130.41 303.79,131.12 304.48,131.84ZM218.39,93.76L215.71,88L213.04,93.75C213.87,93.74 214.71,93.73 215.55,93.73C216.49,93.73 217.44,93.74 218.39,93.76ZM209.4,101.58C211.44,101.47 213.49,101.42 215.55,101.42C217.72,101.42 219.88,101.47 222.02,101.6L226.57,111.4C222.95,111.02 219.27,110.82 215.55,110.82C211.94,110.82 208.38,111.01 204.86,111.36L209.4,101.58ZM138.75,130.84L148.89,134.54C143.27,139.12 138.12,144.27 133.54,149.89L129.84,139.75C132.65,136.62 135.62,133.65 138.75,130.84ZM130.59,127.85L124.7,125.7L126.85,131.59C127.46,130.95 128.08,130.32 128.7,129.7C129.32,129.08 129.96,128.46 130.59,127.85ZM292.43,130.92C295.62,133.78 298.64,136.82 301.49,140.01L297.77,150.17C293.17,144.47 287.98,139.26 282.31,134.63L292.43,130.92ZM338.33,213.88C338.35,214.77 338.36,215.66 338.36,216.55C338.36,217.55 338.35,218.54 338.33,219.54L344.42,216.71L338.33,213.88ZM301.4,293.17L297.69,283.03C293.03,288.78 287.78,294.03 282.03,298.69L292.17,302.4C295.42,299.5 298.5,296.42 301.4,293.17ZM300.35,305.39C301.03,304.73 301.71,304.07 302.39,303.39C303.07,302.71 303.73,302.03 304.39,301.35L306.72,307.72L300.35,305.39ZM222.18,331.49L226.74,321.68C223.06,322.07 219.32,322.27 215.55,322.27C211.88,322.27 208.27,322.08 204.7,321.71L209.25,331.51C211.33,331.62 213.43,331.68 215.55,331.68C217.77,331.68 219.99,331.61 222.18,331.49ZM212.88,339.33C213.77,339.35 214.66,339.36 215.55,339.36C216.55,339.36 217.54,339.35 218.54,339.33L215.71,345.42L212.88,339.33ZM139.01,302.49L149.17,298.77C143.47,294.17 138.26,288.98 133.63,283.31L129.92,293.43C132.78,296.62 135.82,299.64 139.01,302.49ZM100.6,223.02C100.48,220.88 100.42,218.72 100.42,216.55C100.42,214.49 100.47,212.44 100.58,210.4L110.36,205.86C110.01,209.38 109.82,212.94 109.82,216.55C109.82,220.27 110.02,223.95 110.4,227.58L100.6,223.02ZM92.75,214.04L87,216.71L92.76,219.39C92.74,218.44 92.73,217.49 92.73,216.54C92.73,215.71 92.74,214.87 92.75,214.04ZM126.94,301.59C127.52,302.2 128.11,302.8 128.7,303.39C129.41,304.1 130.12,304.79 130.84,305.48L124.7,307.72L126.94,301.59ZM290.93,264.54L295.01,275.7C289.26,283.41 282.41,290.26 274.7,296.01L263.54,291.93C274.54,284.9 283.9,275.54 290.93,264.54ZM235.04,303.79L230.02,314.6C225.3,315.29 220.46,315.65 215.55,315.65C210.75,315.65 206.03,315.31 201.42,314.65L196.41,303.88C202.58,305.22 208.98,305.94 215.55,305.94C222.24,305.94 228.76,305.2 235.04,303.79ZM167.67,292.01L156.5,296.09C148.84,290.4 142.03,283.62 136.3,276L140.37,264.87C147.4,275.76 156.73,285.04 167.67,292.01ZM128.26,235.87L117.47,230.86C116.79,226.19 116.44,221.41 116.44,216.55C116.44,211.81 116.78,207.15 117.42,202.58L128.18,197.59C126.85,203.7 126.16,210.04 126.16,216.55C126.16,223.18 126.88,229.65 128.26,235.87ZM167.34,141.29L156.2,137.22C148.63,142.9 141.9,149.63 136.22,157.2L140.29,168.34C147.26,157.51 156.51,148.26 167.34,141.29ZM215.55,117.44C210.81,117.44 206.15,117.78 201.58,118.42L196.59,129.18C202.7,127.85 209.04,127.16 215.55,127.16C222.18,127.16 228.65,127.88 234.87,129.26L229.86,118.47C225.19,117.79 220.41,117.44 215.55,117.44ZM215.55,283.71C178.51,283.71 148.38,253.58 148.38,216.55C148.38,179.51 178.51,149.38 215.55,149.38C252.58,149.38 282.71,179.51 282.71,216.55C282.71,253.58 252.58,283.71 215.55,283.71ZM215.55,160.48C184.63,160.48 159.48,185.63 159.48,216.55C159.48,247.46 184.63,272.61 215.55,272.61C246.46,272.61 271.61,247.46 271.61,216.55C271.61,185.63 246.46,160.48 215.55,160.48Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="124.5"
android:endY="52"
android:startX="252.5"
android:startY="276.5"
android:type="linear">
<item
android:color="#FFEEDA5C"
android:offset="0" />
<item
android:color="#FFF0DE81"
android:offset="0.23" />
<item
android:color="#FFF2E19C"
android:offset="0.45" />
<item
android:color="#FFF3E3B0"
android:offset="0.66" />
<item
android:color="#FFF4E5BC"
android:offset="0.85" />
<item
android:color="#FFF3E3AA"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="432"
android:viewportHeight="432">
<path
android:fillColor="#ffffff"
android:fillType="evenOdd"
android:pathData="M237.9,135.78L243.89,148.68L257.15,143.83C270.23,151.34 281.11,162.28 288.54,175.42L283.74,188.53L296.37,194.4C298.31,201.45 299.34,208.88 299.34,216.55C299.34,224.35 298.27,231.9 296.27,239.07L283.74,244.89L288.46,257.8C280.97,271 270,281.97 256.8,289.46L243.89,284.74L238.07,297.27C230.9,299.27 223.35,300.34 215.55,300.34C207.88,300.34 200.45,299.31 193.39,297.37L187.53,284.74L174.42,289.54C161.28,282.11 150.34,271.23 142.83,258.15L147.68,244.89L134.78,238.9C132.8,231.78 131.75,224.28 131.75,216.55C131.75,208.94 132.77,201.58 134.67,194.57L147.68,188.53L142.75,175.07C150.2,162.05 161.04,151.2 174.07,143.75L187.53,148.68L193.57,135.67C200.57,133.77 207.94,132.75 215.55,132.75C223.28,132.75 230.78,133.8 237.9,135.78ZM263.87,141.37C274.76,148.4 284.04,157.73 291.01,168.67L295.09,157.49C289.4,149.84 282.62,143.03 275,137.3L263.87,141.37ZM302.88,197.42L313.65,202.42C314.31,207.03 314.65,211.75 314.65,216.55C314.65,221.46 314.29,226.3 313.6,231.02L302.8,236.04C304.2,229.76 304.94,223.24 304.94,216.55C304.94,209.98 304.23,203.58 302.88,197.42ZM321.27,216.55C321.27,220.32 321.07,224.06 320.68,227.74L330.49,223.18C330.61,220.99 330.68,218.77 330.68,216.55C330.68,214.43 330.62,212.33 330.51,210.25L320.71,205.7C321.08,209.27 321.27,212.88 321.27,216.55ZM304.48,131.84L306.72,125.7L300.59,127.94C301.2,128.52 301.8,129.11 302.39,129.7C303.1,130.41 303.79,131.12 304.48,131.84ZM218.39,93.76L215.71,88L213.04,93.75C213.87,93.74 214.71,93.73 215.55,93.73C216.49,93.73 217.44,93.74 218.39,93.76ZM209.4,101.58C211.44,101.47 213.49,101.42 215.55,101.42C217.72,101.42 219.88,101.47 222.02,101.6L226.57,111.4C222.95,111.02 219.27,110.82 215.55,110.82C211.94,110.82 208.38,111.01 204.86,111.36L209.4,101.58ZM138.75,130.84L148.89,134.54C143.27,139.12 138.12,144.27 133.54,149.89L129.84,139.75C132.65,136.62 135.62,133.65 138.75,130.84ZM130.59,127.85L124.7,125.7L126.85,131.59C127.46,130.95 128.08,130.32 128.7,129.7C129.32,129.08 129.96,128.46 130.59,127.85ZM292.43,130.92C295.62,133.78 298.64,136.82 301.49,140.01L297.77,150.17C293.17,144.47 287.98,139.26 282.31,134.63L292.43,130.92ZM338.33,213.88C338.35,214.77 338.36,215.66 338.36,216.55C338.36,217.55 338.35,218.54 338.33,219.54L344.42,216.71L338.33,213.88ZM301.4,293.17L297.69,283.03C293.03,288.78 287.78,294.03 282.03,298.69L292.17,302.4C295.42,299.5 298.5,296.42 301.4,293.17ZM300.35,305.39C301.03,304.73 301.71,304.07 302.39,303.39C303.07,302.71 303.73,302.03 304.39,301.35L306.72,307.72L300.35,305.39ZM222.18,331.49L226.74,321.68C223.06,322.07 219.32,322.27 215.55,322.27C211.88,322.27 208.27,322.08 204.7,321.71L209.25,331.51C211.33,331.62 213.43,331.68 215.55,331.68C217.77,331.68 219.99,331.61 222.18,331.49ZM212.88,339.33C213.77,339.35 214.66,339.36 215.55,339.36C216.55,339.36 217.54,339.35 218.54,339.33L215.71,345.42L212.88,339.33ZM139.01,302.49L149.17,298.77C143.47,294.17 138.26,288.98 133.63,283.31L129.92,293.43C132.78,296.62 135.82,299.64 139.01,302.49ZM100.6,223.02C100.48,220.88 100.42,218.72 100.42,216.55C100.42,214.49 100.47,212.44 100.58,210.4L110.36,205.86C110.01,209.38 109.82,212.94 109.82,216.55C109.82,220.27 110.02,223.95 110.4,227.58L100.6,223.02ZM92.75,214.04L87,216.71L92.76,219.39C92.74,218.44 92.73,217.49 92.73,216.54C92.73,215.71 92.74,214.87 92.75,214.04ZM126.94,301.59C127.52,302.2 128.11,302.8 128.7,303.39C129.41,304.1 130.12,304.79 130.84,305.48L124.7,307.72L126.94,301.59ZM290.93,264.54L295.01,275.7C289.26,283.41 282.41,290.26 274.7,296.01L263.54,291.93C274.54,284.9 283.9,275.54 290.93,264.54ZM235.04,303.79L230.02,314.6C225.3,315.29 220.46,315.65 215.55,315.65C210.75,315.65 206.03,315.31 201.42,314.65L196.41,303.88C202.58,305.22 208.98,305.94 215.55,305.94C222.24,305.94 228.76,305.2 235.04,303.79ZM167.67,292.01L156.5,296.09C148.84,290.4 142.03,283.62 136.3,276L140.37,264.87C147.4,275.76 156.73,285.04 167.67,292.01ZM128.26,235.87L117.47,230.86C116.79,226.19 116.44,221.41 116.44,216.55C116.44,211.81 116.78,207.15 117.42,202.58L128.18,197.59C126.85,203.7 126.16,210.04 126.16,216.55C126.16,223.18 126.88,229.65 128.26,235.87ZM167.34,141.29L156.2,137.22C148.63,142.9 141.9,149.63 136.22,157.2L140.29,168.34C147.26,157.51 156.51,148.26 167.34,141.29ZM215.55,117.44C210.81,117.44 206.15,117.78 201.58,118.42L196.59,129.18C202.7,127.85 209.04,127.16 215.55,127.16C222.18,127.16 228.65,127.88 234.87,129.26L229.86,118.47C225.19,117.79 220.41,117.44 215.55,117.44ZM215.55,283.71C178.51,283.71 148.38,253.58 148.38,216.55C148.38,179.51 178.51,149.38 215.55,149.38C252.58,149.38 282.71,179.51 282.71,216.55C282.71,253.58 252.58,283.71 215.55,283.71ZM215.55,160.48C184.63,160.48 159.48,185.63 159.48,216.55C159.48,247.46 184.63,272.61 215.55,272.61C246.46,272.61 271.61,247.46 271.61,216.55C271.61,185.63 246.46,160.48 215.55,160.48Z" />
</vector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_logo_background" />
<foreground android:drawable="@drawable/ic_logo_foreground" />
<monochrome android:drawable="@drawable/ic_logo_monochrome" />
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_logo_background" />
<foreground android:drawable="@drawable/ic_logo_foreground" />
</adaptive-icon>

View file

@ -745,7 +745,9 @@
<string name="notification_permissions_context">Permissions is required for posting notifications</string> <string name="notification_permissions_context">Permissions is required for posting notifications</string>
<string name="notification_permissions_rationale">Notifications permission must be added manually in the app\'s settings before trying again to enable notifications</string> <string name="notification_permissions_rationale">Notifications permission must be added manually in the app\'s settings before trying again to enable notifications</string>
<string name="write_storage_permission_opml">Write storage permission is required for OPML export</string>
<string name="story_saved">Story marked as saved</string> <string name="story_saved">Story marked as saved</string>
<string name="story_unsaved">Story marked as unsaved</string> <string name="story_unsaved">Story marked as unsaved</string>
<string name="story_read">Story marked as read</string> <string name="story_read">Story marked as read</string>

View file

@ -3,12 +3,12 @@ import org.gradle.api.JavaVersion
object Config { object Config {
const val compileSdk = 34 const val compileSdk = 34
const val minSdk = 24 const val minSdk = 26
const val targetSdk = 34 const val targetSdk = 34
const val versionCode = 220 const val versionCode = 230
const val versionName = "13.2.4" const val versionName = "13.3.2"
const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner"
val javaVersion = JavaVersion.VERSION_17 val javaVersion = JavaVersion.VERSION_21
} }

View file

@ -6,7 +6,6 @@ object Dependencies {
const val okHttp = "com.squareup.okhttp3:okhttp:${Version.okHttp}" const val okHttp = "com.squareup.okhttp3:okhttp:${Version.okHttp}"
const val gson = "com.google.code.gson:gson:${Version.gson}" const val gson = "com.google.code.gson:gson:${Version.gson}"
const val billing = "com.android.billingclient:billing:${Version.billing}" 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 material = "com.google.android.material:material:${Version.material}"
const val preference = "androidx.preference:preference-ktx:${Version.preference}" const val preference = "androidx.preference:preference-ktx:${Version.preference}"
const val browser = "androidx.browser:browser:${Version.browser}" 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 hiltAndroid = "com.google.dagger:hilt-android:${Version.hilt}"
const val hiltCompiler = "com.google.dagger:hilt-compiler:${Version.hilt}" const val hiltCompiler = "com.google.dagger:hilt-compiler:${Version.hilt}"
const val profileInstaller = "androidx.profileinstaller:profileinstaller:${Version.profileInstaller}" const val profileInstaller = "androidx.profileinstaller:profileinstaller:${Version.profileInstaller}"
const val playReview = "com.google.android.play:review:${Version.playReview}"
// test // test
const val junit = "junit:junit:${Version.junit}" const val junit = "junit:junit:${Version.junit}"

View file

@ -1,35 +1,35 @@
object Version { object Version {
const val android = "8.3.1" const val android = "8.5.0"
const val kotlin = "1.9.23" 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 recyclerView = "1.3.2"
const val swipeRefreshLayout = "1.1.0" const val swipeRefreshLayout = "1.1.0"
const val okHttp = "4.12.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 billing = "6.2.0"
const val playCore = "1.10.3" const val playReview = "2.0.1"
const val material = "1.11.0" const val material = "1.12.0"
const val preference = "1.2.1" const val preference = "1.2.1"
const val browser = "1.8.0" 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 splashScreen = "1.0.1"
const val hilt = "2.51" const val hilt = "2.51.1"
const val profileInstaller = "1.3.1" const val profileInstaller = "1.3.1"
const val junit = "4.13.2" 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 junitExt = "1.1.5"
const val espresso = "3.5.1" const val espresso = "3.5.1"
const val uiAutomator = "2.3.0" const val uiAutomator = "2.3.0"
const val benchmarkMacroJunit4 = "1.2.3" const val benchmarkMacroJunit4 = "1.2.4"
const val benManesVersions = "0.51.0" const val benManesVersions = "0.51.0"
} }