Merge branch 'sictiru'

* sictiru: (31 commits)
  Android v13.1.0
  #1811 Verify error handling and show loading indicator to the user when adding a feed.
  Fix broken swipe to refresh dependency
  Use plugin information from buildSrc
  Use dependencies and constants from buildSrc
  Add buildSrc for dependency management
  Convert Groovy to Kotlin. Update dependencies
  Convert Groovy to Kotlin
  Update sub service to cancel job when requested. Execute on the IO dispatcher.
  Await for sub service termination
  Use coroutines in sub services
  Update proguard rules
  Enable nonFinalResIds
  Enable non transitive R classes
  Update java version for the android GHA
  Upgrade dependencies
  #1794 Add auto fill hints for easier login
  Import
  Remove context from blur db helper
  Intel dialog row tweak
  ...
This commit is contained in:
Samuel Clay 2023-08-20 16:03:15 -04:00
commit 25a5929f56
81 changed files with 2090 additions and 708 deletions

View file

@ -19,7 +19,7 @@ jobs:
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '11' java-version: '17'
- name: Unit Test - name: Unit Test
run: ./gradlew -Pci --console=plain :app:testDebugUnitTest run: ./gradlew -Pci --console=plain :app:testDebugUnitTest

View file

@ -1,52 +0,0 @@
plugins {
id 'com.android.test'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.newsblur.benchmark'
compileSdk 33
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
defaultConfig {
minSdk 23
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
// This benchmark buildType is used for benchmarking, and should function like your
// release build (for example, with minification on). It's signed with a debug key
// for easy local/CI testing.
benchmark {
debuggable = true
signingConfig = debug.signingConfig
matchingFallbacks = ["release"]
}
}
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
dependencies {
implementation 'androidx.test.ext:junit:1.1.3'
implementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'androidx.test.uiautomator:uiautomator:2.2.0'
implementation 'androidx.benchmark:benchmark-macro-junit4:1.1.0'
}
androidComponents {
beforeVariants(selector().all()) {
enabled = buildType == "benchmark"
}
}

View file

@ -0,0 +1,49 @@
plugins {
id(Plugins.androidTest)
kotlin(Plugins.kotlinAndroid)
}
android {
namespace = Const.namespaceBenchmark
compileSdk = Config.compileSdk
compileOptions {
sourceCompatibility = Config.javaVersion
targetCompatibility = Config.javaVersion
}
defaultConfig {
minSdk = Config.minSdk
targetSdk = Config.targetSdk
testInstrumentationRunner = Config.androidTestInstrumentation
}
buildTypes {
// This benchmark buildType is used for benchmarking, and should function like your
// release build (for example, with minification on). It's signed with a debug key
// for easy local/CI testing.
maybeCreate(Const.benchmark)
getByName(Const.benchmark) {
isDebuggable = true
signingConfig = signingConfigs.getByName(Const.debug)
matchingFallbacks += listOf(Const.release)
}
}
targetProjectPath = ":app"
experimentalProperties[Const.selfInstrumenting] = true
}
dependencies {
implementation(Dependencies.junitExt)
implementation(Dependencies.espressoCore)
implementation(Dependencies.uiAutomator)
implementation(Dependencies.benchmarkMacroJunit4)
}
androidComponents {
beforeVariants(selector().all()) {
it.enabled = it.buildType == Const.benchmark
}
}

View file

@ -1,74 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.kapt'
id 'com.google.dagger.hilt.android'
}
android {
namespace 'com.newsblur'
compileSdk 33
defaultConfig {
applicationId "com.newsblur"
minSdk 23
targetSdk 33
versionCode 212
versionName "13.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
minifyEnabled false
shrinkResources false
}
benchmark {
signingConfig signingConfigs.debug
matchingFallbacks = ['release']
debuggable false
proguardFiles('benchmark-rules.pro')
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
resources.excludes.add("META-INF/*")
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures.viewBinding = true
}
dependencies {
implementation 'androidx.fragment:fragment-ktx:1.5.5'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.android.billingclient:billing:5.1.0'
implementation 'com.google.android.play:core:1.10.3'
implementation "com.google.android.material:material:1.7.0"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.browser:browser:1.4.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "com.google.dagger:hilt-android:2.44.2"
kapt "com.google.dagger:hilt-compiler:2.44.2"
implementation "androidx.profileinstaller:profileinstaller:1.2.2"
testImplementation "junit:junit:4.13.2"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

View file

@ -0,0 +1,75 @@
plugins {
id(Plugins.androidApplication)
kotlin(Plugins.kotlinAndroid)
kotlin(Plugins.kotlinKapt)
id(Plugins.hiltAndroid)
}
android {
namespace = Const.namespace
compileSdk = Config.compileSdk
defaultConfig {
applicationId = Const.namespace
minSdk = Config.minSdk
targetSdk = Config.targetSdk
versionCode = Config.versionCode
versionName = Config.versionName
testInstrumentationRunner = Config.androidTestInstrumentation
}
buildTypes {
getByName(Const.debug) {
isMinifyEnabled = false
isShrinkResources = false
}
maybeCreate(Const.benchmark)
getByName(Const.benchmark) {
signingConfig = signingConfigs.getByName(Const.debug)
matchingFallbacks += listOf(Const.release)
isDebuggable = false
proguardFiles(Const.benchmarkProguard)
}
getByName(Const.release) {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile(Const.defaultProguard), Const.appProguard)
}
}
packaging {
resources.excludes.add("META-INF/*")
}
compileOptions {
sourceCompatibility = Config.javaVersion
targetCompatibility = Config.javaVersion
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(Dependencies.fragment)
implementation(Dependencies.recyclerView)
implementation(Dependencies.swipeRefreshLayout)
implementation(Dependencies.okHttp)
implementation(Dependencies.gson)
implementation(Dependencies.billing)
implementation(Dependencies.playCore)
implementation(Dependencies.material)
implementation(Dependencies.preference)
implementation(Dependencies.browser)
implementation(Dependencies.lifecycleRuntime)
implementation(Dependencies.lifecycleProcess)
implementation(Dependencies.splashScreen)
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltCompiler)
implementation(Dependencies.profileInstaller)
testImplementation(Dependencies.junit)
testImplementation(Dependencies.mockk)
androidTestImplementation(Dependencies.junitExt)
androidTestImplementation(Dependencies.espressoCore)
}

View file

@ -1,6 +1,6 @@
# Add project specific ProGuard rules here. # Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the # You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle. # proguardFiles setting in build.gradle.kts.
# #
# For more details, see # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html
@ -41,3 +41,7 @@
# can be commented out to help diagnose shrinkage errors. # can be commented out to help diagnose shrinkage errors.
-dontwarn ** -dontwarn **
-dontnote ** -dontnote **
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep, allowobfuscation, allowshrinking class com.google.gson.reflect.TypeToken
-keep, allowobfuscation, allowshrinking class * extends com.google.gson.reflect.TypeToken

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="com.newsblur">
<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" />

View file

@ -101,44 +101,44 @@ abstract public class FeedChooser extends NbActivity {
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { if (item.getItemId() == android.R.id.home) {
case android.R.id.home: finish();
finish(); return true;
return true; } else if (item.getItemId() == R.id.menu_sort_order_ascending) {
case R.id.menu_sort_order_ascending: replaceListOrderFilter(ListOrderFilter.ASCENDING);
replaceListOrderFilter(ListOrderFilter.ASCENDING); return true;
return true; } else if (item.getItemId() == R.id.menu_sort_order_descending) {
case R.id.menu_sort_order_descending: replaceListOrderFilter(ListOrderFilter.DESCENDING);
replaceListOrderFilter(ListOrderFilter.DESCENDING); return true;
return true; } else if (item.getItemId() == R.id.menu_sort_by_name) {
case R.id.menu_sort_by_name: replaceFeedOrderFilter(FeedOrderFilter.NAME);
replaceFeedOrderFilter(FeedOrderFilter.NAME); return true;
return true; } else if (item.getItemId() == R.id.menu_sort_by_subs) {
case R.id.menu_sort_by_subs: replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS);
replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS); return true;
return true; } else if (item.getItemId() == R.id.menu_sort_by_recent_story) {
case R.id.menu_sort_by_recent_story: replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY);
replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY); return true;
return true; } else if (item.getItemId() == R.id.menu_sort_by_stories_month) {
case R.id.menu_sort_by_stories_month: replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH);
replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH); return true;
return true; } else if (item.getItemId() == R.id.menu_sort_by_number_opens) {
case R.id.menu_sort_by_number_opens: replaceFeedOrderFilter(FeedOrderFilter.OPENS);
replaceFeedOrderFilter(FeedOrderFilter.OPENS); return true;
return true; } else if (item.getItemId() == R.id.menu_folder_view_nested) {
case R.id.menu_folder_view_nested: replaceFolderView(FolderViewFilter.NESTED);
replaceFolderView(FolderViewFilter.NESTED); return true;
return true; } else if (item.getItemId() == R.id.menu_folder_view_flat) {
case R.id.menu_folder_view_flat: replaceFolderView(FolderViewFilter.FLAT);
replaceFolderView(FolderViewFilter.FLAT); return true;
return true; } else if (item.getItemId() == R.id.menu_widget_background_default) {
case R.id.menu_widget_background_default: setWidgetBackground(WidgetBackground.DEFAULT);
setWidgetBackground(WidgetBackground.DEFAULT); return true;
return true; } else if (item.getItemId() == R.id.menu_widget_background_transparent) {
case R.id.menu_widget_background_transparent: setWidgetBackground(WidgetBackground.TRANSPARENT);
setWidgetBackground(WidgetBackground.TRANSPARENT); return true;
default: } else {
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
} }

View file

@ -81,7 +81,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe
// this is not strictly necessary, since our first refresh with the fs will swap in // this is not strictly necessary, since our first refresh with the fs will swap in
// the correct session, but that can be delayed by sync backup, so we try here to // the correct session, but that can be delayed by sync backup, so we try here to
// reduce UI lag, or in case somehow we got redisplayed in a zero-story state // reduce UI lag, or in case somehow we got redisplayed in a zero-story state
feedUtils.prepareReadingSession(fs, false); feedUtils.prepareReadingSession(this, fs, false);
if (getIntent().getBooleanExtra(EXTRA_WIDGET_STORY, false)) { if (getIntent().getBooleanExtra(EXTRA_WIDGET_STORY, false)) {
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH); String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
UIUtils.startReadingActivity(fs, hash, this); UIUtils.startReadingActivity(fs, hash, this);
@ -206,7 +206,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe
if (session != null) { if (session != null) {
// set the next session on the parent activity // set the next session on the parent activity
fs = session.getFeedSet(); fs = session.getFeedSet();
feedUtils.prepareReadingSession(fs, false); feedUtils.prepareReadingSession(this, fs, false);
triggerSync(); triggerSync();
// set the next session on the child activity // set the next session on the child activity
@ -248,7 +248,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe
String oldQuery = fs.getSearchQuery(); String oldQuery = fs.getSearchQuery();
fs.setSearchQuery(q); fs.setSearchQuery(q);
if (!TextUtils.equals(q, oldQuery)) { if (!TextUtils.equals(q, oldQuery)) {
feedUtils.prepareReadingSession(fs, true); feedUtils.prepareReadingSession(this, fs, true);
triggerSync(); triggerSync();
itemSetFragment.resetEmptyState(); itemSetFragment.resetEmptyState();
itemSetFragment.hasUpdated(); itemSetFragment.hasUpdated();
@ -278,7 +278,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe
protected void restartReadingSession() { protected void restartReadingSession() {
NBSyncService.resetFetchState(fs); NBSyncService.resetFetchState(fs);
feedUtils.prepareReadingSession(fs, true); feedUtils.prepareReadingSession(this, fs, true);
triggerSync(); triggerSync();
itemSetFragment.resetEmptyState(); itemSetFragment.resetEmptyState();
itemSetFragment.hasUpdated(); itemSetFragment.hasUpdated();

View file

@ -10,10 +10,6 @@ import android.graphics.Bitmap;
import android.os.Bundle; import android.os.Bundle;
import android.os.Trace; import android.os.Trace;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import androidx.appcompat.widget.PopupMenu;
import androidx.fragment.app.FragmentManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.view.KeyEvent; import android.view.KeyEvent;
@ -22,13 +18,23 @@ import android.view.View;
import android.view.View.OnKeyListener; import android.view.View.OnKeyListener;
import android.widget.AbsListView; import android.widget.AbsListView;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.PopupMenu;
import androidx.fragment.app.FragmentManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.database.BlurDatabaseHelper; import com.newsblur.database.BlurDatabaseHelper;
import com.newsblur.databinding.ActivityMainBinding; import com.newsblur.databinding.ActivityMainBinding;
import com.newsblur.delegate.MainContextMenuDelegate; import com.newsblur.delegate.MainContextMenuDelegate;
import com.newsblur.delegate.MainContextMenuDelegateImpl; import com.newsblur.delegate.MainContextMenuDelegateImpl;
import com.newsblur.fragment.FeedIntelligenceSelectorFragment; import com.newsblur.fragment.FeedSelectorFragment;
import com.newsblur.fragment.FeedsShortcutFragment;
import com.newsblur.fragment.FolderListFragment; import com.newsblur.fragment.FolderListFragment;
import com.newsblur.keyboard.KeyboardEvent;
import com.newsblur.keyboard.KeyboardListener;
import com.newsblur.keyboard.KeyboardManager;
import com.newsblur.service.BootReceiver; import com.newsblur.service.BootReceiver;
import com.newsblur.service.NBSyncService; import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
@ -45,7 +51,7 @@ import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
public class Main extends NbActivity implements StateChangedListener, SwipeRefreshLayout.OnRefreshListener, AbsListView.OnScrollListener, PopupMenu.OnMenuItemClickListener { public class Main extends NbActivity implements StateChangedListener, SwipeRefreshLayout.OnRefreshListener, AbsListView.OnScrollListener, PopupMenu.OnMenuItemClickListener, KeyboardListener {
@Inject @Inject
FeedUtils feedUtils; FeedUtils feedUtils;
@ -55,10 +61,12 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
public static final String EXTRA_FORCE_SHOW_FEED_ID = "force_show_feed_id"; public static final String EXTRA_FORCE_SHOW_FEED_ID = "force_show_feed_id";
private FolderListFragment folderFeedList; private FolderListFragment folderFeedList;
private FeedSelectorFragment feedSelectorFragment;
private boolean wasSwipeEnabled = false; private boolean wasSwipeEnabled = false;
private ActivityMainBinding binding; private ActivityMainBinding binding;
private MainContextMenuDelegate contextMenuDelegate; private MainContextMenuDelegate contextMenuDelegate;
private KeyboardManager keyboardManager;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -69,7 +77,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
getWindow().setBackgroundDrawableResource(android.R.color.transparent); getWindow().setBackgroundDrawableResource(android.R.color.transparent);
binding = ActivityMainBinding.inflate(getLayoutInflater()); binding = ActivityMainBinding.inflate(getLayoutInflater());
contextMenuDelegate = new MainContextMenuDelegateImpl(this, dbHelper); contextMenuDelegate = new MainContextMenuDelegateImpl(this, dbHelper);
setContentView(binding.getRoot()); keyboardManager = new KeyboardManager();
setContentView(binding.getRoot());
// set the status bar to an generic loading message when the activity is first created so // set the status bar to an generic loading message when the activity is first created so
// that something is displayed while the service warms up // that something is displayed while the service warms up
@ -82,7 +91,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
FragmentManager fragmentManager = getSupportFragmentManager(); FragmentManager fragmentManager = getSupportFragmentManager();
folderFeedList = (FolderListFragment) fragmentManager.findFragmentByTag("folderFeedListFragment"); folderFeedList = (FolderListFragment) fragmentManager.findFragmentByTag("folderFeedListFragment");
((FeedIntelligenceSelectorFragment) fragmentManager.findFragmentByTag("feedIntelligenceSelector")).setState(folderFeedList.currentState); feedSelectorFragment = ((FeedSelectorFragment) fragmentManager.findFragmentByTag("feedIntelligenceSelector"));
feedSelectorFragment.setState(folderFeedList.currentState);
// make sure the interval sync is scheduled, since we are the root Activity // make sure the interval sync is scheduled, since we are the root Activity
BootReceiver.scheduleSyncService(this); BootReceiver.scheduleSyncService(this);
@ -127,12 +137,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
// Check whether it's a shortcut intent // Check whether it's a shortcut intent
String shortcutExtra = getIntent().getStringExtra(ShortcutUtils.SHORTCUT_EXTRA); String shortcutExtra = getIntent().getStringExtra(ShortcutUtils.SHORTCUT_EXTRA);
if (shortcutExtra != null && shortcutExtra.startsWith(ShortcutUtils.SHORTCUT_ALL_STORIES)) { if (shortcutExtra != null && shortcutExtra.startsWith(ShortcutUtils.SHORTCUT_ALL_STORIES)) {
Intent intent = new Intent(this, AllStoriesItemsList.class); boolean isAllStoriesSearch = shortcutExtra.equals(ShortcutUtils.SHORTCUT_ALL_STORIES_SEARCH);
intent.putExtra(ItemsList.EXTRA_FEED_SET, FeedSet.allFeeds()); openAllStories(isAllStoriesSearch);
if (shortcutExtra.equals(ShortcutUtils.SHORTCUT_ALL_STORIES_SEARCH)) {
intent.putExtra(ItemsList.EXTRA_VISIBLE_SEARCH, true);
}
startActivity(intent);
} }
Trace.endSection(); Trace.endSection();
@ -177,10 +183,17 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
updateStatusIndicators(); updateStatusIndicators();
folderFeedList.pushUnreadCounts(); folderFeedList.pushUnreadCounts();
folderFeedList.checkOpenFolderPreferences(); folderFeedList.checkOpenFolderPreferences();
keyboardManager.addListener(this);
triggerSync(); triggerSync();
} }
@Override @Override
protected void onPause() {
keyboardManager.removeListener();
super.onPause();
}
@Override
public void changedState(StateFilter state) { public void changedState(StateFilter state) {
if ( !( (state == StateFilter.ALL) || if ( !( (state == StateFilter.ALL) ||
(state == StateFilter.SOME) || (state == StateFilter.SOME) ||
@ -211,7 +224,27 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
if ((updateType & UPDATE_METADATA) != 0) { if ((updateType & UPDATE_METADATA) != 0) {
folderFeedList.hasUpdated(); folderFeedList.hasUpdated();
} }
} }
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (KeyboardManager.hasHardwareKeyboard(this)) {
boolean isKnownKeyCode = keyboardManager.isKnownKeyCode(keyCode);
if (isKnownKeyCode) return true;
else return super.onKeyDown(keyCode, event);
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (KeyboardManager.hasHardwareKeyboard(this)) {
boolean handledKeyCode = keyboardManager.onKeyUp(keyCode, event);
if (handledKeyCode) return true;
else return super.onKeyUp(keyCode, event);
}
return super.onKeyUp(keyCode, event);
}
public void updateUnreadCounts(int neutCount, int posiCount) { public void updateUnreadCounts(int neutCount, int posiCount) {
binding.mainUnreadCountNeutText.setText(Integer.toString(neutCount)); binding.mainUnreadCountNeutText.setText(Integer.toString(neutCount));
@ -325,4 +358,58 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
} }
folderFeedList.setSearchQuery(q); folderFeedList.setSearchQuery(q);
} }
private void openAllStories(boolean isAllStoriesSearch) {
Intent intent = new Intent(this, AllStoriesItemsList.class);
intent.putExtra(ItemsList.EXTRA_FEED_SET, FeedSet.allFeeds());
intent.putExtra(ItemsList.EXTRA_VISIBLE_SEARCH, isAllStoriesSearch);
startActivity(intent);
}
private void switchViewStateLeft() {
StateFilter currentState = folderFeedList.currentState;
if (currentState.equals(StateFilter.SAVED)) {
setAndNotifySelectorState(StateFilter.BEST, R.string.focused_stories);
} else if (currentState.equals(StateFilter.BEST)) {
setAndNotifySelectorState(StateFilter.SOME, R.string.unread_stories);
} else if (currentState.equals(StateFilter.SOME)) {
setAndNotifySelectorState(StateFilter.ALL, R.string.all_stories);
}
}
private void switchViewStateRight() {
StateFilter currentState = folderFeedList.currentState;
if (currentState.equals(StateFilter.ALL)) {
setAndNotifySelectorState(StateFilter.SOME, R.string.unread_stories);
} else if (currentState.equals(StateFilter.SOME)) {
setAndNotifySelectorState(StateFilter.BEST, R.string.focused_stories);
} else if (currentState.equals(StateFilter.BEST)) {
setAndNotifySelectorState(StateFilter.SAVED, R.string.saved_stories);
}
}
private void setAndNotifySelectorState(StateFilter state, @StringRes int notifyMsgRes) {
feedSelectorFragment.setState(state);
UIUtils.showSnackBar(binding.getRoot(), getString(notifyMsgRes));
}
private void showFeedShortcuts() {
FeedsShortcutFragment newFragment = new FeedsShortcutFragment();
newFragment.show(getSupportFragmentManager(), FeedsShortcutFragment.class.getName());
}
@Override
public void onKeyboardEvent(@NonNull KeyboardEvent event) {
if (event instanceof KeyboardEvent.AddFeed) {
onClickAddButton();
} else if (event instanceof KeyboardEvent.OpenAllStories) {
openAllStories(false);
} else if (event instanceof KeyboardEvent.SwitchViewLeft) {
switchViewStateLeft();
} else if (event instanceof KeyboardEvent.SwitchViewRight) {
switchViewStateRight();
} else if (event instanceof KeyboardEvent.Tutorial) {
showFeedShortcuts();
}
}
} }

View file

@ -59,15 +59,14 @@ public class MuteConfig extends FeedChooser implements MuteConfigAdapter.FeedSta
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { if (item.getItemId() == R.id.menu_mute_all) {
case R.id.menu_mute_all: setFeedsState(true);
setFeedsState(true); return true;
return true; } else if (item.getItemId() == R.id.menu_mute_none) {
case R.id.menu_mute_none: setFeedsState(false);
setFeedsState(false); return true;
return true; } else {
default: return super.onOptionsItemSelected(item);
return super.onOptionsItemSelected(item);
} }
} }

View file

@ -1,5 +1,6 @@
package com.newsblur.activity package com.newsblur.activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.database.Cursor import android.database.Cursor
@ -22,7 +23,11 @@ import com.newsblur.databinding.ActivityReadingBinding
import com.newsblur.di.IconLoader import com.newsblur.di.IconLoader
import com.newsblur.domain.Story import com.newsblur.domain.Story
import com.newsblur.fragment.ReadingItemFragment import com.newsblur.fragment.ReadingItemFragment
import com.newsblur.fragment.ReadingItemFragment.Companion.VERTICAL_SCROLL_DISTANCE_DP
import com.newsblur.fragment.ReadingPagerFragment import com.newsblur.fragment.ReadingPagerFragment
import com.newsblur.keyboard.KeyboardEvent
import com.newsblur.keyboard.KeyboardListener
import com.newsblur.keyboard.KeyboardManager
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_REBUILD import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_REBUILD
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STATUS import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STATUS
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
@ -42,7 +47,7 @@ import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
@AndroidEntryPoint @AndroidEntryPoint
abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListener { abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListener, KeyboardListener {
@Inject @Inject
lateinit var feedUtils: FeedUtils lateinit var feedUtils: FeedUtils
@ -84,6 +89,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
private var isMultiWindowModeHack = false private var isMultiWindowModeHack = false
private val pageHistory = mutableListOf<Story>() private val pageHistory = mutableListOf<Story>()
private val keyboardManager = KeyboardManager()
private lateinit var volumeKeyNavigation: VolumeKeyNavigation private lateinit var volumeKeyNavigation: VolumeKeyNavigation
private lateinit var intelState: StateFilter private lateinit var intelState: StateFilter
@ -136,7 +142,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
setupViews() setupViews()
setupListeners() setupListeners()
setupObservers() setupObservers()
getActiveStoriesCursor(true) getActiveStoriesCursor(this, true)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -164,11 +170,13 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
// this is not strictly necessary, since our first refresh with the fs will swap in // this is not strictly necessary, since our first refresh with the fs will swap in
// the correct session, but that can be delayed by sync backup, so we try here to // the correct session, but that can be delayed by sync backup, so we try here to
// reduce UI lag, or in case somehow we got redisplayed in a zero-story state // reduce UI lag, or in case somehow we got redisplayed in a zero-story state
feedUtils.prepareReadingSession(fs, false) feedUtils.prepareReadingSession(this, fs, false)
keyboardManager.addListener(this)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
keyboardManager.removeListener()
if (isMultiWindowModeHack) { if (isMultiWindowModeHack) {
isMultiWindowModeHack = false isMultiWindowModeHack = false
} else { } else {
@ -218,9 +226,10 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
} }
} }
private fun getActiveStoriesCursor(finishOnInvalidFs: Boolean = false) { private fun getActiveStoriesCursor(context: Context, finishOnInvalidFs: Boolean = false) {
fs?.let { fs?.let {
storiesViewModel.getActiveStories(it) val cursorFilters = CursorFilters(context, it)
storiesViewModel.getActiveStories(it, cursorFilters)
} ?: run { } ?: run {
if (finishOnInvalidFs) { if (finishOnInvalidFs) {
Log.e(this.javaClass.name, "can't create activity, no feedset ready") Log.e(this.javaClass.name, "can't create activity, no feedset ready")
@ -385,7 +394,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
} }
} }
if (updateType and UPDATE_STORY != 0) { if (updateType and UPDATE_STORY != 0) {
getActiveStoriesCursor() getActiveStoriesCursor(this)
updateOverlayNav() updateOverlayNav()
} }
@ -737,6 +746,10 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
return if (isVolumeKeyNavigationEvent(keyCode)) { return if (isVolumeKeyNavigationEvent(keyCode)) {
processVolumeKeyNavigationEvent(keyCode) processVolumeKeyNavigationEvent(keyCode)
true true
} else if (KeyboardManager.hasHardwareKeyboard(this)) {
val isKnownKeyCode = keyboardManager.isKnownKeyCode(keyCode)
if (isKnownKeyCode) true
else super.onKeyDown(keyCode, event)
} else { } else {
super.onKeyDown(keyCode, event) super.onKeyDown(keyCode, event)
} }
@ -748,24 +761,32 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
private fun processVolumeKeyNavigationEvent(keyCode: Int) { private fun processVolumeKeyNavigationEvent(keyCode: Int) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && volumeKeyNavigation == VolumeKeyNavigation.DOWN_NEXT || if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && volumeKeyNavigation == VolumeKeyNavigation.DOWN_NEXT ||
keyCode == KeyEvent.KEYCODE_VOLUME_UP && volumeKeyNavigation == VolumeKeyNavigation.UP_NEXT) { keyCode == KeyEvent.KEYCODE_VOLUME_UP && volumeKeyNavigation == VolumeKeyNavigation.UP_NEXT) {
if (pager == null) return nextStory()
val nextPosition = pager!!.currentItem + 1
if (nextPosition < readingAdapter!!.count) {
try {
pager!!.currentItem = nextPosition
} catch (e: Exception) {
// Just in case cursor changes.
}
}
} else { } else {
if (pager == null) return previousStory()
val nextPosition = pager!!.currentItem - 1 }
if (nextPosition >= 0) { }
try {
pager!!.currentItem = nextPosition private fun nextStory() {
} catch (e: Exception) { if (pager == null) return
// Just in case cursor changes. val nextPosition = pager!!.currentItem + 1
} if (nextPosition < readingAdapter!!.count) {
try {
pager!!.currentItem = nextPosition
} catch (e: Exception) {
// Just in case cursor changes.
}
}
}
private fun previousStory() {
if (pager == null) return
val nextPosition = pager!!.currentItem - 1
if (nextPosition >= 0) {
try {
pager!!.currentItem = nextPosition
} catch (e: Exception) {
// Just in case cursor changes.
} }
} }
} }
@ -774,6 +795,10 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
// Required to prevent the default sound playing when the volume key is pressed // Required to prevent the default sound playing when the volume key is pressed
return if (isVolumeKeyNavigationEvent(keyCode)) { return if (isVolumeKeyNavigationEvent(keyCode)) {
true true
} else if (KeyboardManager.hasHardwareKeyboard(this)) {
val handledKeyCode = keyboardManager.onKeyUp(keyCode, event)
if (handledKeyCode) true
else super.onKeyUp(keyCode, event)
} else { } else {
super.onKeyUp(keyCode, event) super.onKeyUp(keyCode, event)
} }
@ -797,6 +822,27 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
if (isActive) feedUtils.markStoryAsRead(story, this@Reading) if (isActive) feedUtils.markStoryAsRead(story, this@Reading)
} }
override fun onKeyboardEvent(event: KeyboardEvent) {
when (event) {
KeyboardEvent.NextStory -> nextStory()
KeyboardEvent.PreviousStory -> previousStory()
KeyboardEvent.NextUnreadStory -> nextUnread()
KeyboardEvent.OpenInBrowser -> readingFragment?.openBrowser()
KeyboardEvent.OpenStoryTrainer -> readingFragment?.openStoryTrainer()
KeyboardEvent.SaveUnsaveStory -> readingFragment?.switchStorySavedState(true)
KeyboardEvent.ScrollToComments -> readingFragment?.scrollToComments()
KeyboardEvent.ShareStory -> readingFragment?.openShareDialog()
KeyboardEvent.ToggleReadUnread -> readingFragment?.switchMarkStoryReadState(true)
KeyboardEvent.ToggleTextView -> readingFragment?.switchSelectedViewMode()
KeyboardEvent.Tutorial -> readingFragment?.showStoryShortcuts()
KeyboardEvent.PageDown ->
readingFragment?.scrollVerticallyBy(UIUtils.dp2px(this, VERTICAL_SCROLL_DISTANCE_DP))
KeyboardEvent.PageUp ->
readingFragment?.scrollVerticallyBy(UIUtils.dp2px(this, -VERTICAL_SCROLL_DISTANCE_DP))
else -> {}
}
}
companion object { companion object {
const val EXTRA_FEEDSET = "feed_set" const val EXTRA_FEEDSET = "feed_set"
const val EXTRA_STORY_HASH = "story_hash" const val EXTRA_STORY_HASH = "story_hash"

View file

@ -62,15 +62,14 @@ public class WidgetConfig extends FeedChooser {
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { if (item.getItemId() == R.id.menu_select_all) {
case R.id.menu_select_all: selectAllFeeds();
selectAllFeeds(); return true;
return true; } else if (item.getItemId() == R.id.menu_select_none) {
case R.id.menu_select_none: replaceWidgetFeedIds(Collections.emptySet());
replaceWidgetFeedIds(Collections.emptySet()); return true;
return true; } else {
default: return super.onOptionsItemSelected(item);
return super.onOptionsItemSelected(item);
} }
} }

View file

@ -24,13 +24,12 @@ import com.newsblur.domain.UserProfile;
import com.newsblur.network.domain.CommentResponse; import com.newsblur.network.domain.CommentResponse;
import com.newsblur.network.domain.StoriesResponse; import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.CursorFilters;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadingAction; import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadFilter; import com.newsblur.util.ReadFilter;
import com.newsblur.util.StateFilter; import com.newsblur.util.StateFilter;
import com.newsblur.util.StoryOrder; import com.newsblur.util.StoryOrder;
import com.newsblur.util.UIUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.ArrayList; import java.util.ArrayList;
@ -56,14 +55,12 @@ public class BlurDatabaseHelper {
// manual synchro isn't needed if you only use one DBHelper, but at present the app uses several // manual synchro isn't needed if you only use one DBHelper, but at present the app uses several
public final static Object RW_MUTEX = new Object(); public final static Object RW_MUTEX = new Object();
private Context context;
private final BlurDatabase dbWrapper; private final BlurDatabase dbWrapper;
private SQLiteDatabase dbRO; private final SQLiteDatabase dbRO;
private SQLiteDatabase dbRW; private final SQLiteDatabase dbRW;
public BlurDatabaseHelper(Context context) { public BlurDatabaseHelper(Context context) {
com.newsblur.util.Log.d(this.getClass().getName(), "new DB conn requested"); com.newsblur.util.Log.d(this.getClass().getName(), "new DB conn requested");
this.context = context;
synchronized (RW_MUTEX) { synchronized (RW_MUTEX) {
dbWrapper = new BlurDatabase(context); dbWrapper = new BlurDatabase(context);
dbRO = dbWrapper.getRO(); dbRO = dbWrapper.getRO();
@ -328,8 +325,7 @@ public class BlurDatabaseHelper {
return urls; return urls;
} }
public void insertStories(StoriesResponse apiResponse, boolean forImmediateReading) { public void insertStories(StoriesResponse apiResponse, StateFilter stateFilter, boolean forImmediateReading) {
StateFilter intelState = PrefsUtils.getStateFilter(context);
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
@ -369,7 +365,7 @@ public class BlurDatabaseHelper {
} }
insertSingleStoryExtSync(story); insertSingleStoryExtSync(story);
// if the story is being fetched for the immediate session, also add the hash to the session table // if the story is being fetched for the immediate session, also add the hash to the session table
if (forImmediateReading && story.isStoryVisibileInState(intelState)) { if (forImmediateReading && story.isStoryVisibleInState(stateFilter)) {
ContentValues sessionHashValues = new ContentValues(); ContentValues sessionHashValues = new ContentValues();
sessionHashValues.put(DatabaseConstants.READING_SESSION_STORY_HASH, story.storyHash); sessionHashValues.put(DatabaseConstants.READING_SESSION_STORY_HASH, story.storyHash);
dbRW.insert(DatabaseConstants.READING_SESSION_TABLE, null, sessionHashValues); dbRW.insert(DatabaseConstants.READING_SESSION_TABLE, null, sessionHashValues);
@ -481,7 +477,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, boolean forImmediateReading) { public void updateStory(StoriesResponse apiResponse, 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;
@ -500,7 +496,7 @@ public class BlurDatabaseHelper {
apiResponse.story.starredTimestamp = oldStory.starredTimestamp; apiResponse.story.starredTimestamp = oldStory.starredTimestamp;
apiResponse.story.read = oldStory.read; apiResponse.story.read = oldStory.read;
} }
insertStories(apiResponse, forImmediateReading); insertStories(apiResponse, stateFilter, forImmediateReading);
} }
/** /**
@ -754,8 +750,8 @@ public class BlurDatabaseHelper {
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;
if (olderThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " <= " + olderThan.toString(); if (olderThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " <= " + olderThan;
if (newerThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " >= " + newerThan.toString(); if (newerThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " >= " + newerThan;
StringBuilder feedSelection = null; StringBuilder feedSelection = null;
if (fs.isAllNormal()) { if (fs.isAllNormal()) {
// a null selection is fine for all stories // a null selection is fine for all stories
@ -982,7 +978,7 @@ public class BlurDatabaseHelper {
} }
} }
public void setStoryShared(String hash, boolean shared) { public void setStoryShared(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},
@ -998,15 +994,13 @@ public class BlurDatabaseHelper {
String[] sharedUserIds = TextUtils.split(c.getString(c.getColumnIndex(DatabaseConstants.STORY_SHARED_USER_IDS)), ","); String[] sharedUserIds = TextUtils.split(c.getString(c.getColumnIndex(DatabaseConstants.STORY_SHARED_USER_IDS)), ",");
closeQuietly(c); closeQuietly(c);
// the id to append to or remove from the shared list (the current user)
String currentUser = PrefsUtils.getUserId(context);
// append to set and update DB // append to set and update DB
Set<String> newIds = new HashSet<String>(Arrays.asList(sharedUserIds)); Set<String> newIds = new HashSet<String>(Arrays.asList(sharedUserIds));
// the id to append to or remove from the shared list (the current user)
if (shared) { if (shared) {
newIds.add(currentUser); newIds.add(currentUserId);
} else { } else {
newIds.remove(currentUser); newIds.remove(currentUserId);
} }
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_SHARED_USER_IDS, TextUtils.join(",", newIds)); values.put(DatabaseConstants.STORY_SHARED_USER_IDS, TextUtils.join(",", newIds));
@ -1146,16 +1140,16 @@ public class BlurDatabaseHelper {
return rawQuery(q.toString(), null, cancellationSignal); return rawQuery(q.toString(), null, cancellationSignal);
} }
public Cursor getActiveStoriesCursor(FeedSet fs, CancellationSignal cancellationSignal) { public Cursor getActiveStoriesCursor(FeedSet fs, CursorFilters cursorFilters, CancellationSignal cancellationSignal) {
final StoryOrder order = PrefsUtils.getStoryOrder(context, fs);
// get the stories for this FS // get the stories for this FS
Cursor result = getActiveStoriesCursorNoPrep(fs, order, 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
// are offline, but if a session is started, just use what was there so offsets don't change. // are offline, but if a session is started, just use what was there so offsets don't change.
if (result.getCount() < 1) { if (result.getCount() < 1) {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "priming reading session"); if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "priming reading session");
prepareReadingSession(fs); prepareReadingSession(fs, cursorFilters.getStateFilter(), cursorFilters.getReadFilter());
result = getActiveStoriesCursorNoPrep(fs, order, cancellationSignal);
result = getActiveStoriesCursorNoPrep(fs, cursorFilters.getStoryOrder(), cancellationSignal);
} }
return result; return result;
} }
@ -1183,18 +1177,12 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.READING_SESSION_TABLE, null, null);} synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.READING_SESSION_TABLE, null, null);}
} }
public void prepareReadingSession(FeedSet fs) {
ReadFilter readFilter = PrefsUtils.getReadFilter(context, fs);
StateFilter stateFilter = PrefsUtils.getStateFilter(context);
prepareReadingSession(fs, stateFilter, readFilter);
}
/** /**
* Populates the reading session table with hashes of already-fetched stories that meet the * Populates the reading session table with hashes of already-fetched stories that meet the
* 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.
*/ */
private void prepareReadingSession(FeedSet fs, StateFilter stateFilter, ReadFilter readFilter) { public void prepareReadingSession(FeedSet fs, StateFilter stateFilter, 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
@ -1366,8 +1354,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, String feedId, String commentText) { public void insertCommentPlaceholder(String storyId, @Nullable String userId, String commentText) {
String userId = PrefsUtils.getUserId(context);
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;
@ -1399,19 +1386,18 @@ public class BlurDatabaseHelper {
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) { public void clearSelfComments(String storyId, @Nullable String userId) {
String userId = PrefsUtils.getUserId(context);
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 userId, String feedId, boolean liked) { public void setCommentLiked(String storyId, 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,
DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?", DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?",
new String[]{storyId, userId}, new String[]{storyId, commentUserId},
null, null, null); null, null, null);
if ((c == null)||(c.getCount() < 1)) { if ((c == null)||(c.getCount() < 1)) {
Log.w(this.getClass().getName(), "comment removed before finishing mark-liked"); Log.w(this.getClass().getName(), "comment removed before finishing mark-liked");
@ -1422,15 +1408,13 @@ public class BlurDatabaseHelper {
Comment comment = Comment.fromCursor(c); Comment comment = Comment.fromCursor(c);
closeQuietly(c); closeQuietly(c);
// the new id to append/remove from the liking list (the current user)
String currentUser = PrefsUtils.getUserId(context);
// append to set and update DB // append to set and update DB
Set<String> newIds = new HashSet<String>(Arrays.asList(comment.likingUsers)); Set<String> newIds = new HashSet<String>(Arrays.asList(comment.likingUsers));
// the new id to append/remove from the liking list (the current user)
if (liked) { if (liked) {
newIds.add(currentUser); newIds.add(currentUserId);
} else { } else {
newIds.remove(currentUser); newIds.remove(currentUserId);
} }
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.COMMENT_LIKING_USERS, TextUtils.join(",", newIds)); values.put(DatabaseConstants.COMMENT_LIKING_USERS, TextUtils.join(",", newIds));
@ -1458,7 +1442,7 @@ public class BlurDatabaseHelper {
return replies; return replies;
} }
public void insertReplyPlaceholder(String storyId, String feedId, String commentUserId, String replyText) { public void insertReplyPlaceholder(String storyId, @Nullable String userId, String commentUserId, 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,
@ -1477,7 +1461,7 @@ public class BlurDatabaseHelper {
Reply reply = new Reply(); Reply reply = new Reply();
reply.commentId = comment.id; reply.commentId = comment.id;
reply.text = replyText; reply.text = replyText;
reply.userId = PrefsUtils.getUserId(context); reply.userId = userId;
reply.date = new Date(); reply.date = new Date();
reply.id = Reply.PLACEHOLDER_COMMENT_ID + storyId + comment.id + reply.userId; reply.id = Reply.PLACEHOLDER_COMMENT_ID + storyId + comment.id + reply.userId;
synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE);} synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE);}
@ -1582,11 +1566,8 @@ public class BlurDatabaseHelper {
public static void closeQuietly(Cursor c) { public static void closeQuietly(Cursor c) {
if (c == null) return; if (c == null) return;
try {c.close();} catch (Exception e) {;} try {c.close();} catch (Exception e) {
} }
public void sendSyncUpdate(int updateType) {
UIUtils.syncUpdateStatus(context, updateType);
} }
private static String conjoinSelections(CharSequence... args) { private static String conjoinSelections(CharSequence... args) {

View file

@ -413,52 +413,42 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
@Override @Override
public boolean onMenuItemClick(MenuItem item) { public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) { if (item.getItemId() == R.id.menu_mark_story_as_read) {
case R.id.menu_mark_story_as_read:
feedUtils.markStoryAsRead(story, context); feedUtils.markStoryAsRead(story, context);
return true; return true;
} else if (item.getItemId() == R.id.menu_mark_story_as_unread) {
case R.id.menu_mark_story_as_unread:
feedUtils.markStoryUnread(story, context); feedUtils.markStoryUnread(story, context);
return true; return true;
} else if (item.getItemId() == R.id.menu_mark_older_stories_as_read) {
case R.id.menu_mark_older_stories_as_read:
feedUtils.markRead(context, fs, story.timestamp, null, R.array.mark_older_read_options); feedUtils.markRead(context, fs, story.timestamp, null, R.array.mark_older_read_options);
return true; return true;
} else if (item.getItemId() == R.id.menu_mark_newer_stories_as_read) {
case R.id.menu_mark_newer_stories_as_read:
feedUtils.markRead(context, fs, null, story.timestamp, R.array.mark_newer_read_options); feedUtils.markRead(context, fs, null, story.timestamp, R.array.mark_newer_read_options);
return true; return true;
} else if (item.getItemId() == R.id.menu_send_story) {
case R.id.menu_send_story:
feedUtils.sendStoryUrl(story, context); feedUtils.sendStoryUrl(story, context);
return true; return true;
} else if (item.getItemId() == R.id.menu_send_story_full) {
case R.id.menu_send_story_full:
feedUtils.sendStoryFull(story, context); feedUtils.sendStoryFull(story, context);
return true; return true;
} else if (item.getItemId() == R.id.menu_save_story) {
case R.id.menu_save_story:
//TODO get folder name //TODO get folder name
feedUtils.setStorySaved(story, true, context, null); feedUtils.setStorySaved(story, true, context, null);
return true; return true;
} else if (item.getItemId() == R.id.menu_unsave_story) {
case R.id.menu_unsave_story:
feedUtils.setStorySaved(story, false, context, null); feedUtils.setStorySaved(story, false, context, null);
return true; return true;
} else if (item.getItemId() == R.id.menu_intel) {
case R.id.menu_intel:
if (story.feedId.equals("0")) return true; // cannot train on feedless stories if (story.feedId.equals("0")) return true; // cannot train on feedless stories
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs); StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
intelFrag.show(context.getSupportFragmentManager(), StoryIntelTrainerFragment.class.getName()); intelFrag.show(context.getSupportFragmentManager(), StoryIntelTrainerFragment.class.getName());
return true; return true;
} else if (item.getItemId() == R.id.menu_go_to_feed) {
case R.id.menu_go_to_feed:
FeedSet fs = FeedSet.singleFeed(story.feedId); FeedSet fs = FeedSet.singleFeed(story.feedId);
FeedItemsList.startActivity(context, fs, FeedItemsList.startActivity(context, fs,
feedUtils.getFeed(story.feedId), null, null); feedUtils.getFeed(story.feedId), null, null);
return true; return true;
default: } else {
return false; return false;
} }
} }

View file

@ -303,7 +303,7 @@ open class ItemListContextMenuDelegateImpl(
private fun restartReadingSession(fragment: ItemSetFragment, fs: FeedSet) { private fun restartReadingSession(fragment: ItemSetFragment, fs: FeedSet) {
NBSyncService.resetFetchState(fs) NBSyncService.resetFetchState(fs)
feedUtils.prepareReadingSession(fs, true) feedUtils.prepareReadingSession(activity, fs, true)
triggerSync(activity) triggerSync(activity)
fragment.resetEmptyState() fragment.resetEmptyState()
fragment.hasUpdated() fragment.hasUpdated()

View file

@ -10,10 +10,8 @@ import androidx.fragment.app.DialogFragment
import com.newsblur.R import com.newsblur.R
import com.newsblur.activity.* import com.newsblur.activity.*
import com.newsblur.database.BlurDatabaseHelper import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.fragment.FolderListFragment import com.newsblur.fragment.*
import com.newsblur.fragment.LoginAsDialogFragment import com.newsblur.keyboard.KeyboardManager
import com.newsblur.fragment.LogoutDialogFragment
import com.newsblur.fragment.NewslettersFragment
import com.newsblur.service.NBSyncService import com.newsblur.service.NBSyncService
import com.newsblur.util.ListTextSize import com.newsblur.util.ListTextSize
import com.newsblur.util.ListTextSize.Companion.fromSize import com.newsblur.util.ListTextSize.Companion.fromSize
@ -44,6 +42,10 @@ class MainContextMenuDelegateImpl(
menu.findItem(R.id.menu_loginas).isVisible = true menu.findItem(R.id.menu_loginas).isVisible = true
} }
if (KeyboardManager.hasHardwareKeyboard(activity)) {
menu.findItem(R.id.menu_shortcuts).isVisible = true
}
when (PrefsUtils.getSelectedTheme(activity)) { when (PrefsUtils.getSelectedTheme(activity)) {
ThemeValue.LIGHT -> menu.findItem(R.id.menu_theme_light).isChecked = true ThemeValue.LIGHT -> menu.findItem(R.id.menu_theme_light).isChecked = true
ThemeValue.DARK -> menu.findItem(R.id.menu_theme_dark).isChecked = true ThemeValue.DARK -> menu.findItem(R.id.menu_theme_dark).isChecked = true
@ -184,10 +186,15 @@ class MainContextMenuDelegateImpl(
true true
} }
R.id.menu_newsletters -> { R.id.menu_newsletters -> {
val newFragment: DialogFragment = NewslettersFragment() val newFragment = NewslettersFragment()
newFragment.show(activity.supportFragmentManager, NewslettersFragment::class.java.name) newFragment.show(activity.supportFragmentManager, NewslettersFragment::class.java.name)
true true
} }
R.id.menu_shortcuts -> {
val newFragment = FeedsShortcutFragment()
newFragment.show(activity.supportFragmentManager, FeedsShortcutFragment::class.java.name)
true
}
else -> false else -> false
} }
} }

View file

@ -238,7 +238,7 @@ public class Story implements Serializable {
} }
} }
public boolean isStoryVisibileInState(StateFilter state) { public boolean isStoryVisibleInState(StateFilter state) {
int score = intelligence.calcTotalIntel(); int score = intelligence.calcTotalIntel();
switch (state) { switch (state) {
case ALL: case ALL:
@ -252,7 +252,7 @@ public class Story implements Serializable {
case NEG: case NEG:
return (score < 0); return (score < 0);
case SAVED: case SAVED:
return (starred == true); return (starred);
default: default:
return true; return true;
} }

View file

@ -24,10 +24,9 @@ import com.newsblur.fragment.AddFeedFragment.AddFeedAdapter.FolderViewHolder
import com.newsblur.network.APIManager import com.newsblur.network.APIManager
import com.newsblur.service.NBSyncService import com.newsblur.service.NBSyncService
import com.newsblur.util.AppConstants import com.newsblur.util.AppConstants
import com.newsblur.util.UIUtils
import com.newsblur.util.executeAsyncTask import com.newsblur.util.executeAsyncTask
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.* import java.util.Collections
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -88,14 +87,14 @@ class AddFeedFragment : DialogFragment() {
binding.inputFolderName.text.clear() binding.inputFolderName.text.clear()
addFeed(activity, apiManager, folderName) addFeed(activity, apiManager, folderName)
} else { } else {
UIUtils.safeToast(activity, R.string.add_folder_error, Toast.LENGTH_SHORT) Toast.makeText(activity, R.string.add_folder_error, Toast.LENGTH_SHORT).show()
} }
} }
) )
} }
private fun addFeed(activity: Activity, apiManager: APIManager, folderName: String?) { private fun addFeed(activity: Activity, apiManager: APIManager, folderName: String?) {
binding.textSyncStatus.visibility = View.VISIBLE binding.containerSyncStatus.visibility = View.VISIBLE
lifecycleScope.executeAsyncTask( lifecycleScope.executeAsyncTask(
doInBackground = { doInBackground = {
(activity as AddFeedProgressListener).addFeedStarted() (activity as AddFeedProgressListener).addFeedStarted()
@ -103,7 +102,7 @@ class AddFeedFragment : DialogFragment() {
apiManager.addFeed(feedUrl, folderName) apiManager.addFeed(feedUrl, folderName)
}, },
onPostExecute = { onPostExecute = {
binding.textSyncStatus.visibility = View.GONE binding.containerSyncStatus.visibility = View.GONE
val intent = Intent(activity, Main::class.java) val intent = Intent(activity, Main::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
if (!it.isError) { if (!it.isError) {
@ -111,7 +110,7 @@ class AddFeedFragment : DialogFragment() {
NBSyncService.forceFeedsFolders() NBSyncService.forceFeedsFolders()
intent.putExtra(Main.EXTRA_FORCE_SHOW_FEED_ID, it.feed.feedId) intent.putExtra(Main.EXTRA_FORCE_SHOW_FEED_ID, it.feed.feedId)
} else { } else {
UIUtils.safeToast(activity, R.string.add_feed_error, Toast.LENGTH_SHORT) Toast.makeText(activity, R.string.add_feed_error, Toast.LENGTH_SHORT).show()
} }
activity.startActivity(intent) activity.startActivity(intent)
activity.finish() activity.finish()
@ -119,8 +118,7 @@ class AddFeedFragment : DialogFragment() {
) )
} }
private class AddFeedAdapter private class AddFeedAdapter(private val listener: OnFolderClickListener) : RecyclerView.Adapter<FolderViewHolder>() {
constructor(private val listener: OnFolderClickListener) : RecyclerView.Adapter<FolderViewHolder>() {
private val folders: MutableList<Folder> = ArrayList() private val folders: MutableList<Folder> = ArrayList()
@ -145,7 +143,7 @@ class AddFeedFragment : DialogFragment() {
Collections.sort(folders, Folder.FolderComparator) Collections.sort(folders, Folder.FolderComparator)
this.folders.clear() this.folders.clear()
this.folders.addAll(folders) this.folders.addAll(folders)
notifyDataSetChanged() this.notifyDataSetChanged()
} }
class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

View file

@ -11,7 +11,7 @@ import com.newsblur.view.StateToggleButton;
import com.newsblur.view.StateToggleButton.StateChangedListener; import com.newsblur.view.StateToggleButton.StateChangedListener;
import com.newsblur.util.StateFilter; import com.newsblur.util.StateFilter;
public class FeedIntelligenceSelectorFragment extends Fragment implements StateChangedListener { public class FeedSelectorFragment extends Fragment implements StateChangedListener {
private StateToggleButton button; private StateToggleButton button;

View file

@ -0,0 +1,19 @@
package com.newsblur.fragment
import android.app.Dialog
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.newsblur.databinding.FeedsShortcutsDialogBinding
class FeedsShortcutFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = FeedsShortcutsDialogBinding.inflate(layoutInflater)
return AlertDialog.Builder(requireContext()).apply {
setView(binding.root)
setPositiveButton(android.R.string.ok, null)
}.create()
}
}

View file

@ -8,7 +8,6 @@ import android.os.Parcelable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -31,6 +30,7 @@ import com.newsblur.di.IconLoader;
import com.newsblur.di.ThumbnailLoader; import com.newsblur.di.ThumbnailLoader;
import com.newsblur.domain.Story; import com.newsblur.domain.Story;
import com.newsblur.service.NBSyncService; import com.newsblur.service.NBSyncService;
import com.newsblur.util.CursorFilters;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
import com.newsblur.util.ImageLoader; import com.newsblur.util.ImageLoader;
@ -149,15 +149,12 @@ public class ItemSetFragment extends NbFragment {
// disable the throbbers if animations are going to have a zero time scale // disable the throbbers if animations are going to have a zero time scale
boolean isDisableAnimations = ViewUtils.isPowerSaveMode(requireContext()); boolean isDisableAnimations = ViewUtils.isPowerSaveMode(requireContext());
int[] colorsArray = {ContextCompat.getColor(requireContext(), R.color.refresh_1), int[] colorsArray = UIUtils.getLoadingColorsArray(requireContext());
ContextCompat.getColor(requireContext(), R.color.refresh_2),
ContextCompat.getColor(requireContext(), R.color.refresh_3),
ContextCompat.getColor(requireContext(), R.color.refresh_4)};
binding.topLoadingThrob.setEnabled(!isDisableAnimations); binding.topLoadingThrob.setEnabled(!isDisableAnimations);
binding.topLoadingThrob.setColors(colorsArray); binding.topLoadingThrob.setColors(colorsArray);
View footerView = inflater.inflate(R.layout.row_loading_throbber, null); View footerView = inflater.inflate(R.layout.row_loading_throbber, null);
bottomProgressView = (ProgressThrobber) footerView.findViewById(R.id.itemlist_loading_throb); bottomProgressView = footerView.findViewById(R.id.itemlist_loading_throb);
bottomProgressView.setEnabled(!isDisableAnimations); bottomProgressView.setEnabled(!isDisableAnimations);
bottomProgressView.setColors(colorsArray); bottomProgressView.setColors(colorsArray);
@ -271,7 +268,7 @@ public class ItemSetFragment extends NbFragment {
public void hasUpdated() { public void hasUpdated() {
FeedSet fs = getFeedSet(); FeedSet fs = getFeedSet();
if (isAdded() && fs != null) { if (isAdded() && fs != null) {
storiesViewModel.getActiveStories(fs); storiesViewModel.getActiveStories(fs, new CursorFilters(requireContext(), fs));
} }
} }
@ -391,7 +388,7 @@ public class ItemSetFragment extends NbFragment {
// ensure we have measured // ensure we have measured
if (itemGridWidthPx > 0) { if (itemGridWidthPx > 0) {
int itemGridWidthDp = Math.round(UIUtils.px2dp(getActivity(), itemGridWidthPx)); int itemGridWidthDp = Math.round(UIUtils.px2dp(requireContext(), itemGridWidthPx));
colsCoarse = itemGridWidthDp / 300; colsCoarse = itemGridWidthDp / 300;
colsMed = itemGridWidthDp / 200; colsMed = itemGridWidthDp / 200;
colsFine = itemGridWidthDp / 150; colsFine = itemGridWidthDp / 150;
@ -416,7 +413,7 @@ public class ItemSetFragment extends NbFragment {
if (listStyle == StoryListStyle.LIST) { if (listStyle == StoryListStyle.LIST) {
gridSpacingPx = 0; gridSpacingPx = 0;
} else { } else {
gridSpacingPx = UIUtils.dp2px(getActivity(), GRID_SPACING_DP); gridSpacingPx = UIUtils.dp2px(requireContext(), GRID_SPACING_DP);
} }
} }
@ -435,8 +432,8 @@ public class ItemSetFragment extends NbFragment {
} }
RecyclerView.ItemAnimator anim = binding.itemgridfragmentGrid.getItemAnimator(); RecyclerView.ItemAnimator anim = binding.itemgridfragmentGrid.getItemAnimator();
anim.setAddDuration((long) ((anim.getAddDuration() + targetAddDuration)/2L)); anim.setAddDuration((anim.getAddDuration() + targetAddDuration)/2L);
anim.setMoveDuration((long) ((anim.getMoveDuration() + targetMovDuration)/2L)); anim.setMoveDuration((anim.getMoveDuration() + targetMovDuration)/2L);
} }
private void onScrolled(RecyclerView recyclerView, int dx, int dy) { private void onScrolled(RecyclerView recyclerView, int dx, int dy) {

View file

@ -15,8 +15,7 @@ import com.newsblur.util.setViewVisible
class NewslettersFragment : DialogFragment() { class NewslettersFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val view = layoutInflater.inflate(R.layout.newsletter_dialog, null) val binding = NewsletterDialogBinding.inflate(layoutInflater)
val binding: NewsletterDialogBinding = NewsletterDialogBinding.bind(view)
val emailAddress = generateEmail() val emailAddress = generateEmail()
binding.txtEmail.text = emailAddress binding.txtEmail.text = emailAddress

View file

@ -47,10 +47,8 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
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_profileactivity, null) val view = inflater.inflate(R.layout.fragment_profileactivity, null)
binding = FragmentProfileactivityBinding.bind(view) binding = FragmentProfileactivityBinding.bind(view)
val colorsArray = intArrayOf(ContextCompat.getColor(requireContext(), R.color.refresh_1), val colorsArray = UIUtils.getLoadingColorsArray(requireContext())
ContextCompat.getColor(requireContext(), R.color.refresh_2),
ContextCompat.getColor(requireContext(), R.color.refresh_3),
ContextCompat.getColor(requireContext(), R.color.refresh_4))
binding.emptyViewLoadingThrob.setColors(*colorsArray) binding.emptyViewLoadingThrob.setColors(*colorsArray)
binding.profileDetailsActivitylist.setFooterDividersEnabled(false) binding.profileDetailsActivitylist.setFooterDividersEnabled(false)
binding.profileDetailsActivitylist.emptyView = binding.emptyView binding.profileDetailsActivitylist.emptyView = binding.emptyView

View file

@ -15,6 +15,7 @@ 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
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -30,6 +31,7 @@ import com.newsblur.di.StoryImageCache
import com.newsblur.domain.Classifier import com.newsblur.domain.Classifier
import com.newsblur.domain.Story import com.newsblur.domain.Story
import com.newsblur.domain.UserDetails import com.newsblur.domain.UserDetails
import com.newsblur.keyboard.KeyboardManager
import com.newsblur.network.APIManager import com.newsblur.network.APIManager
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_INTEL import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_INTEL
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_SOCIAL import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_SOCIAL
@ -198,10 +200,10 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.storyContextMenuButton.setOnClickListener { onClickMenuButton() } binding.storyContextMenuButton.setOnClickListener { onClickMenuButton() }
readingItemActionsBinding.markReadStoryButton.setOnClickListener { clickMarkStoryRead() } readingItemActionsBinding.markReadStoryButton.setOnClickListener { switchMarkStoryReadState() }
readingItemActionsBinding.trainStoryButton.setOnClickListener { clickTrain() } readingItemActionsBinding.trainStoryButton.setOnClickListener { openStoryTrainer() }
readingItemActionsBinding.saveStoryButton.setOnClickListener { clickSave() } readingItemActionsBinding.saveStoryButton.setOnClickListener { switchStorySavedState() }
readingItemActionsBinding.shareStoryButton.setOnClickListener { clickShare() } readingItemActionsBinding.shareStoryButton.setOnClickListener { openShareDialog() }
} }
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo?) { override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo?) {
@ -262,6 +264,10 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
menu.findItem(R.id.menu_reading_save).setTitle(if (story!!.starred) R.string.menu_unsave_story else R.string.menu_save_story) menu.findItem(R.id.menu_reading_save).setTitle(if (story!!.starred) R.string.menu_unsave_story else R.string.menu_save_story)
if (fs!!.isFilterSaved || fs!!.isAllSaved || fs!!.singleSavedTag != null) menu.findItem(R.id.menu_reading_markunread).isVisible = false if (fs!!.isFilterSaved || fs!!.isAllSaved || fs!!.singleSavedTag != null) menu.findItem(R.id.menu_reading_markunread).isVisible = false
if (KeyboardManager.hasHardwareKeyboard(requireContext())) {
menu.findItem(R.id.menu_shortcuts).isVisible = true
}
when (PrefsUtils.getSelectedTheme(requireContext())) { when (PrefsUtils.getSelectedTheme(requireContext())) {
ThemeValue.LIGHT -> menu.findItem(R.id.menu_theme_light).isChecked = true ThemeValue.LIGHT -> menu.findItem(R.id.menu_theme_light).isChecked = true
ThemeValue.DARK -> menu.findItem(R.id.menu_theme_dark).isChecked = true ThemeValue.DARK -> menu.findItem(R.id.menu_theme_dark).isChecked = true
@ -298,8 +304,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) { override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.menu_reading_original -> { R.id.menu_reading_original -> {
val uri = Uri.parse(story!!.permalink) openBrowser()
UIUtils.handleUri(requireContext(), uri)
true true
} }
R.id.menu_reading_sharenewsblur -> { R.id.menu_reading_sharenewsblur -> {
@ -317,6 +322,10 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
feedUtils.sendStoryFull(story, requireContext()) feedUtils.sendStoryFull(story, requireContext())
true true
} }
R.id.menu_shortcuts -> {
showStoryShortcuts()
true
}
R.id.menu_text_size_xs -> { R.id.menu_text_size_xs -> {
setTextSizeStyle(ReadingTextSize.XS) setTextSizeStyle(ReadingTextSize.XS)
true true
@ -408,7 +417,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
R.id.menu_intel -> { R.id.menu_intel -> {
// check against training on feedless stories // check against training on feedless stories
if (story!!.feedId != "0") { if (story!!.feedId != "0") {
clickTrain() openStoryTrainer()
} }
true true
} }
@ -421,9 +430,19 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
} }
} }
private fun clickMarkStoryRead() { fun switchMarkStoryReadState(notifyUser: Boolean = false) {
if (story!!.read) feedUtils.markStoryUnread(story!!, requireContext()) story?.let {
else feedUtils.markStoryAsRead(story!!, requireContext()) val msg = if (it.read) {
feedUtils.markStoryUnread(it, requireContext())
getString(R.string.story_unread)
}
else {
feedUtils.markStoryAsRead(it, requireContext())
getString(R.string.story_read)
}
if (notifyUser) UIUtils.showSnackBar(binding.root, msg)
} ?: Log.e(this.javaClass.name, "Error switching null story read state.")
} }
private fun updateMarkStoryReadState() { private fun updateMarkStoryReadState() {
@ -437,7 +456,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
sampledQueue?.add { updateStoryReadTitleState.invoke() } ?: updateStoryReadTitleState.invoke() sampledQueue?.add { updateStoryReadTitleState.invoke() } ?: updateStoryReadTitleState.invoke()
} }
private fun clickTrain() { fun openStoryTrainer() {
val intelFrag = StoryIntelTrainerFragment.newInstance(story, fs) val intelFrag = StoryIntelTrainerFragment.newInstance(story, fs)
intelFrag.show(requireActivity().supportFragmentManager, StoryIntelTrainerFragment::class.java.name) intelFrag.show(requireActivity().supportFragmentManager, StoryIntelTrainerFragment::class.java.name)
} }
@ -446,19 +465,25 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
readingItemActionsBinding.trainStoryButton.visibility = if (story!!.feedId == "0") View.GONE else View.VISIBLE readingItemActionsBinding.trainStoryButton.visibility = if (story!!.feedId == "0") View.GONE else View.VISIBLE
} }
private fun clickSave() { fun switchStorySavedState(notifyUser: Boolean = false) {
if (story!!.starred) { story?.let {
feedUtils.setStorySaved(story!!.storyHash, false, requireContext()) val msg = if (it.starred) {
} else { feedUtils.setStorySaved(it.storyHash, false, requireContext())
feedUtils.setStorySaved(story!!.storyHash, true, requireContext()) getString(R.string.story_saved)
} } else {
feedUtils.setStorySaved(it.storyHash, true, requireContext())
getString(R.string.story_unsaved)
}
if (notifyUser) UIUtils.showSnackBar(binding.root, msg)
} ?: Log.e(this.javaClass.name, "Error switching null story saved state.")
} }
private fun updateSaveButton() { private fun updateSaveButton() {
readingItemActionsBinding.saveStoryButton.setText(if (story!!.starred) R.string.unsave_this else R.string.save_this) readingItemActionsBinding.saveStoryButton.setText(if (story!!.starred) R.string.unsave_this else R.string.save_this)
} }
private fun clickShare() { fun openShareDialog() {
val newFragment: DialogFragment = ShareDialogFragment.newInstance(story, sourceUserId) val newFragment: DialogFragment = ShareDialogFragment.newInstance(story, sourceUserId)
newFragment.show(parentFragmentManager, "dialog") newFragment.show(parentFragmentManager, "dialog")
} }
@ -530,17 +555,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
val intelFrag = StoryIntelTrainerFragment.newInstance(story, fs) val intelFrag = StoryIntelTrainerFragment.newInstance(story, fs)
intelFrag.show(parentFragmentManager, StoryIntelTrainerFragment::class.java.name) intelFrag.show(parentFragmentManager, StoryIntelTrainerFragment::class.java.name)
}) })
binding.readingItemTitle.setOnClickListener(object : View.OnClickListener { binding.readingItemTitle.setOnClickListener { openBrowser() }
override fun onClick(v: View) {
try {
UIUtils.handleUri(requireContext(), Uri.parse(story!!.permalink))
} catch (t: Throwable) {
// we don't actually know if the user will successfully be able to open whatever string
// was in the permalink or if the Intent could throw errors
Log.e(this.javaClass.name, "Error opening story by permalink URL.", t)
}
}
})
setupTagsAndIntel() setupTagsAndIntel()
} }
@ -963,6 +978,11 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
} }
} }
fun showStoryShortcuts() {
val newFragment = StoryShortcutsFragment()
newFragment.show(requireActivity().supportFragmentManager, StoryShortcutsFragment::class.java.name)
}
fun flagWebviewError() { fun flagWebviewError() {
// TODO: enable a selective reload mechanism on load failures? // TODO: enable a selective reload mechanism on load failures?
} }
@ -989,8 +1009,31 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
reloadStoryContent() reloadStoryContent()
} }
fun openBrowser() {
story?.let {
val uri = Uri.parse(it.permalink)
UIUtils.handleUri(requireContext(), uri)
} ?: Log.e(this.javaClass.name, "Error opening null story by permalink URL.")
}
fun scrollToComments() {
val targetView = if (readingItemActionsBinding.readingFriendCommentHeader.isVisible) {
readingItemActionsBinding.readingFriendCommentContainer
} else if (readingItemActionsBinding.readingPublicCommentHeader.isVisible) {
readingItemActionsBinding.readingPublicCommentContainer
} else null
targetView?.let {
it.parent.requestChildFocus(targetView, it)
}
}
fun scrollVerticallyBy(dy: Int) {
binding.readingScrollview.smoothScrollBy(0, dy)
}
companion object { companion object {
private const val BUNDLE_SCROLL_POS_REL = "scrollStateRel" private const val BUNDLE_SCROLL_POS_REL = "scrollStateRel"
const val VERTICAL_SCROLL_DISTANCE_DP = 240
@JvmStatic @JvmStatic
fun newInstance(story: Story?, feedTitle: String?, feedFaviconColor: String?, feedFaviconFade: String?, feedFaviconBorder: String?, faviconText: String?, faviconUrl: String?, classifier: Classifier?, displayFeedDetails: Boolean, sourceUserId: String?): ReadingItemFragment { fun newInstance(story: Story?, feedTitle: String?, feedFaviconColor: String?, feedFaviconFade: String?, feedFaviconBorder: String?, faviconText: String?, faviconUrl: String?, classifier: Classifier?, displayFeedDetails: Boolean, sourceUserId: String?): ReadingItemFragment {

View file

@ -2,17 +2,16 @@ package com.newsblur.fragment;
import java.util.Map; import java.util.Map;
import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import android.text.InputType; import android.text.InputType;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.Gravity; import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.TextView; import android.widget.TextView;
@ -57,6 +56,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
return fragment; return fragment;
} }
@NonNull
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -64,9 +64,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
fs = (FeedSet) getArguments().getSerializable("feedset"); fs = (FeedSet) getArguments().getSerializable("feedset");
classifier = dbHelper.getClassifierForFeed(story.feedId); classifier = dbHelper.getClassifierForFeed(story.feedId);
final Activity activity = getActivity(); View v = getLayoutInflater().inflate(R.layout.dialog_trainstory, null);
LayoutInflater inflater = LayoutInflater.from(activity);
View v = inflater.inflate(R.layout.dialog_trainstory, null);
binding = DialogTrainstoryBinding.bind(v); binding = DialogTrainstoryBinding.bind(v);
// set up the special title training box for the title from this story and the associated buttons // set up the special title training box for the title from this story and the associated buttons
@ -111,7 +109,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
// scan trained title fragments for this feed and see if any apply to this story // scan trained title fragments for this feed and see if any apply to this story
for (Map.Entry<String, Integer> rule : classifier.title.entrySet()) { for (Map.Entry<String, Integer> rule : classifier.title.entrySet()) {
if (story.title.indexOf(rule.getKey()) >= 0) { if (story.title.indexOf(rule.getKey()) >= 0) {
View row = inflater.inflate(R.layout.include_intel_row, null); View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView label = (TextView) row.findViewById(R.id.intel_row_label); TextView label = (TextView) row.findViewById(R.id.intel_row_label);
label.setText(rule.getKey()); label.setText(rule.getKey());
UIUtils.setupIntelDialogRow(row, classifier.title, rule.getKey()); UIUtils.setupIntelDialogRow(row, classifier.title, rule.getKey());
@ -121,7 +119,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
// list all tags for this story, trained or not // list all tags for this story, trained or not
for (String tag : story.tags) { for (String tag : story.tags) {
View row = inflater.inflate(R.layout.include_intel_row, null); View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView label = (TextView) row.findViewById(R.id.intel_row_label); TextView label = (TextView) row.findViewById(R.id.intel_row_label);
label.setText(tag); label.setText(tag);
UIUtils.setupIntelDialogRow(row, classifier.tags, tag); UIUtils.setupIntelDialogRow(row, classifier.tags, tag);
@ -131,7 +129,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
// there is a single author per story // there is a single author per story
if (!TextUtils.isEmpty(story.authors)) { if (!TextUtils.isEmpty(story.authors)) {
View rowAuthor = inflater.inflate(R.layout.include_intel_row, null); View rowAuthor = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView labelAuthor = (TextView) rowAuthor.findViewById(R.id.intel_row_label); TextView labelAuthor = (TextView) rowAuthor.findViewById(R.id.intel_row_label);
labelAuthor.setText(story.authors); labelAuthor.setText(story.authors);
UIUtils.setupIntelDialogRow(rowAuthor, classifier.authors, story.authors); UIUtils.setupIntelDialogRow(rowAuthor, classifier.authors, story.authors);
@ -142,13 +140,13 @@ public class StoryIntelTrainerFragment extends DialogFragment {
// there is a single feed to be trained, but it is a bit odd in that the label is the title and // there is a single feed to be trained, but it is a bit odd in that the label is the title and
// the intel identifier is the feed ID // the intel identifier is the feed ID
View rowFeed = inflater.inflate(R.layout.include_intel_row, null); View rowFeed = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView labelFeed = (TextView) rowFeed.findViewById(R.id.intel_row_label); TextView labelFeed = (TextView) rowFeed.findViewById(R.id.intel_row_label);
labelFeed.setText(feedUtils.getFeedTitle(story.feedId)); labelFeed.setText(feedUtils.getFeedTitle(story.feedId));
UIUtils.setupIntelDialogRow(rowFeed, classifier.feeds, story.feedId); UIUtils.setupIntelDialogRow(rowFeed, classifier.feeds, story.feedId);
binding.existingFeedIntelContainer.addView(rowFeed); binding.existingFeedIntelContainer.addView(rowFeed);
AlertDialog.Builder builder = new AlertDialog.Builder(activity); AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
builder.setTitle(R.string.story_intel_dialog_title); builder.setTitle(R.string.story_intel_dialog_title);
builder.setView(v); builder.setView(v);
@ -164,7 +162,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
if ((newTitleTraining != null) && (!TextUtils.isEmpty(binding.intelTitleSelection.getSelection()))) { if ((newTitleTraining != null) && (!TextUtils.isEmpty(binding.intelTitleSelection.getSelection()))) {
classifier.title.put(binding.intelTitleSelection.getSelection(), newTitleTraining); classifier.title.put(binding.intelTitleSelection.getSelection(), newTitleTraining);
} }
feedUtils.updateClassifier(story.feedId, classifier, fs, activity); feedUtils.updateClassifier(story.feedId, classifier, fs, requireActivity());
StoryIntelTrainerFragment.this.dismiss(); StoryIntelTrainerFragment.this.dismiss();
} }
}); });

View file

@ -0,0 +1,44 @@
package com.newsblur.fragment
import android.app.Dialog
import android.graphics.Typeface
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.style.AbsoluteSizeSpan
import android.text.style.StyleSpan
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.newsblur.R
import com.newsblur.databinding.StoryShortcutsDialogBinding
class StoryShortcutsFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = StoryShortcutsDialogBinding.inflate(layoutInflater)
SpannableString(getString(R.string.short_share_this_story_key)).apply {
shiftKeySpannable()
}.also {
binding.txtShareStoryKey.text = it
}
SpannableString(getString(R.string.short_page_up_key)).apply {
shiftKeySpannable()
}.also {
binding.txtPageUpKey.text = it
}
return AlertDialog.Builder(requireContext()).apply {
setView(binding.root)
setPositiveButton(android.R.string.ok, null)
}.create()
}
private fun SpannableString.shiftKeySpannable() {
setSpan(AbsoluteSizeSpan(18, true),
0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
setSpan(StyleSpan(Typeface.BOLD),
0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
}
}

View file

@ -0,0 +1,35 @@
package com.newsblur.keyboard
interface KeyboardListener {
fun onKeyboardEvent(event: KeyboardEvent)
}
sealed class KeyboardEvent {
/**
* Keyboard events for Home
*/
object OpenAllStories : KeyboardEvent()
object AddFeed : KeyboardEvent()
object SwitchViewRight : KeyboardEvent()
object SwitchViewLeft : KeyboardEvent()
/**
* Keyboard events for Reading
*/
object NextStory : KeyboardEvent()
object PreviousStory : KeyboardEvent()
object ToggleTextView : KeyboardEvent()
object NextUnreadStory : KeyboardEvent()
object ToggleReadUnread : KeyboardEvent()
object SaveUnsaveStory : KeyboardEvent()
object OpenInBrowser : KeyboardEvent()
object ShareStory : KeyboardEvent()
object ScrollToComments : KeyboardEvent()
object OpenStoryTrainer : KeyboardEvent()
object PageDown: KeyboardEvent()
object PageUp: KeyboardEvent()
object Tutorial: KeyboardEvent()
}

View file

@ -0,0 +1,147 @@
package com.newsblur.keyboard
import android.content.Context
import android.content.res.Configuration
import android.view.KeyEvent
class KeyboardManager {
private var listener: KeyboardListener? = null
fun addListener(listener: KeyboardListener) {
this.listener = listener
}
fun removeListener() {
this.listener = null
}
/**
* @return Return <code>true</code> to prevent this event from being propagated
* further, or <code>false</code> to indicate that you have not handled
*/
fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean = when (keyCode) {
/**
* Home events
*/
KeyEvent.KEYCODE_E -> {
handleKeycodeE(event)
}
KeyEvent.KEYCODE_A -> {
handleKeycodeA(event)
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
listener?.onKeyboardEvent(KeyboardEvent.SwitchViewRight)
true
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
listener?.onKeyboardEvent(KeyboardEvent.SwitchViewLeft)
true
}
/**
* Story events
*/
KeyEvent.KEYCODE_J,
KeyEvent.KEYCODE_DPAD_DOWN -> {
listener?.onKeyboardEvent(KeyboardEvent.PreviousStory)
true
}
KeyEvent.KEYCODE_K,
KeyEvent.KEYCODE_DPAD_UP -> {
listener?.onKeyboardEvent(KeyboardEvent.NextStory)
true
}
KeyEvent.KEYCODE_N -> {
listener?.onKeyboardEvent(KeyboardEvent.NextUnreadStory)
true
}
KeyEvent.KEYCODE_U, KeyEvent.KEYCODE_M -> {
listener?.onKeyboardEvent(KeyboardEvent.ToggleReadUnread)
true
}
KeyEvent.KEYCODE_S -> {
if (event.isShiftPressed) listener?.onKeyboardEvent(KeyboardEvent.ShareStory)
else listener?.onKeyboardEvent(KeyboardEvent.SaveUnsaveStory)
true
}
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_V -> {
listener?.onKeyboardEvent(KeyboardEvent.OpenInBrowser)
true
}
KeyEvent.KEYCODE_C -> {
listener?.onKeyboardEvent(KeyboardEvent.ScrollToComments)
true
}
KeyEvent.KEYCODE_T -> {
listener?.onKeyboardEvent(KeyboardEvent.OpenStoryTrainer)
true
}
KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_NUMPAD_ENTER -> {
if (event.isShiftPressed) {
listener?.onKeyboardEvent(KeyboardEvent.ToggleTextView)
true
} else false
}
KeyEvent.KEYCODE_SPACE -> {
if (event.isShiftPressed) listener?.onKeyboardEvent(KeyboardEvent.PageUp)
else listener?.onKeyboardEvent(KeyboardEvent.PageDown)
true
}
KeyEvent.KEYCODE_ALT_RIGHT,
KeyEvent.KEYCODE_ALT_LEFT -> {
listener?.onKeyboardEvent(KeyboardEvent.Tutorial)
true
}
else -> false
}
private fun handleKeycodeE(event: KeyEvent): Boolean = if (event.isAltPressed) {
listener?.onKeyboardEvent(KeyboardEvent.OpenAllStories)
true
} else false
private fun handleKeycodeA(event: KeyEvent): Boolean = if (event.isAltPressed) {
listener?.onKeyboardEvent(KeyboardEvent.AddFeed)
true
} else false
fun isKnownKeyCode(keyCode: Int): Boolean =
isShortcutKeyCode(keyCode) && isSpecialKeyCode(keyCode)
private fun isSpecialKeyCode(keyCode: Int) = when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_SPACE,
-> true
else -> false
}
private fun isShortcutKeyCode(keyCode: Int) = when (keyCode) {
KeyEvent.KEYCODE_E,
KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_J,
KeyEvent.KEYCODE_K,
KeyEvent.KEYCODE_N,
KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_M,
KeyEvent.KEYCODE_S,
KeyEvent.KEYCODE_O,
KeyEvent.KEYCODE_V,
KeyEvent.KEYCODE_C,
KeyEvent.KEYCODE_T,
-> true
else -> false
}
companion object {
@JvmStatic
fun hasHardwareKeyboard(context: Context) =
context.resources.configuration.keyboard == Configuration.KEYBOARD_QWERTY
}
}

View file

@ -205,7 +205,7 @@ public class APIManager {
public ProfileResponse updateUserProfile() { public ProfileResponse updateUserProfile() {
final APIResponse response = get(buildUrl(APIConstants.PATH_MY_PROFILE)); final APIResponse response = get(buildUrl(APIConstants.PATH_MY_PROFILE));
if (!response.isError()) { if (!response.isError()) {
ProfileResponse profileResponse = (ProfileResponse) response.getResponse(gson, ProfileResponse.class); ProfileResponse profileResponse = response.getResponse(gson, ProfileResponse.class);
PrefsUtils.saveUserDetails(context, profileResponse.user); PrefsUtils.saveUserDetails(context, profileResponse.user);
return profileResponse; return profileResponse;
} else { } else {
@ -234,14 +234,14 @@ public class APIManager {
values.put(APIConstants.PARAMETER_FEEDID, id); values.put(APIConstants.PARAMETER_FEEDID, id);
} }
APIResponse response = get(buildUrl(APIConstants.PATH_FEED_UNREAD_COUNT), values); APIResponse response = get(buildUrl(APIConstants.PATH_FEED_UNREAD_COUNT), values);
return (UnreadCountResponse) response.getResponse(gson, UnreadCountResponse.class); return response.getResponse(gson, UnreadCountResponse.class);
} }
public UnreadStoryHashesResponse getUnreadStoryHashes() { public UnreadStoryHashesResponse getUnreadStoryHashes() {
ValueMultimap values = new ValueMultimap(); ValueMultimap values = new ValueMultimap();
values.put(APIConstants.PARAMETER_INCLUDE_TIMESTAMPS, "1"); values.put(APIConstants.PARAMETER_INCLUDE_TIMESTAMPS, "1");
APIResponse response = get(buildUrl(APIConstants.PATH_UNREAD_HASHES), values); APIResponse response = get(buildUrl(APIConstants.PATH_UNREAD_HASHES), values);
return (UnreadStoryHashesResponse) response.getResponse(gson, UnreadStoryHashesResponse.class); return response.getResponse(gson, UnreadStoryHashesResponse.class);
} }
public StarredStoryHashesResponse getStarredStoryHashes() { public StarredStoryHashesResponse getStarredStoryHashes() {
@ -256,7 +256,7 @@ public class APIManager {
} }
values.put(APIConstants.PARAMETER_INCLUDE_HIDDEN, APIConstants.VALUE_TRUE); values.put(APIConstants.PARAMETER_INCLUDE_HIDDEN, APIConstants.VALUE_TRUE);
APIResponse response = get(buildUrl(APIConstants.PATH_RIVER_STORIES), values); APIResponse response = get(buildUrl(APIConstants.PATH_RIVER_STORIES), values);
return (StoriesResponse) response.getResponse(gson, StoriesResponse.class); return response.getResponse(gson, StoriesResponse.class);
} }
/** /**
@ -264,7 +264,7 @@ public class APIManager {
* request parameters as needed. * request parameters as needed.
*/ */
public StoriesResponse getStories(FeedSet fs, int pageNumber, StoryOrder order, ReadFilter filter) { public StoriesResponse getStories(FeedSet fs, int pageNumber, StoryOrder order, ReadFilter filter) {
Uri uri = null; Uri uri;
ValueMultimap values = new ValueMultimap(); ValueMultimap values = new ValueMultimap();
// create the URI and populate request params depending on what kind of stories we want // create the URI and populate request params depending on what kind of stories we want
@ -331,29 +331,21 @@ public class APIManager {
} }
APIResponse response = get(uri.toString(), values); APIResponse response = get(uri.toString(), values);
return (StoriesResponse) response.getResponse(gson, StoriesResponse.class); return response.getResponse(gson, StoriesResponse.class);
} }
public boolean followUser(final String userId) { public boolean followUser(final String userId) {
final ContentValues values = new ContentValues(); final ContentValues values = new ContentValues();
values.put(APIConstants.PARAMETER_USERID, userId); values.put(APIConstants.PARAMETER_USERID, userId);
final APIResponse response = post(buildUrl(APIConstants.PATH_FOLLOW), values); final APIResponse response = post(buildUrl(APIConstants.PATH_FOLLOW), values);
if (!response.isError()) { return !response.isError();
return true;
} else {
return false;
}
} }
public boolean unfollowUser(final String userId) { public boolean unfollowUser(final String userId) {
final ContentValues values = new ContentValues(); final ContentValues values = new ContentValues();
values.put(APIConstants.PARAMETER_USERID, userId); values.put(APIConstants.PARAMETER_USERID, userId);
final APIResponse response = post(buildUrl(APIConstants.PATH_UNFOLLOW), values); final APIResponse response = post(buildUrl(APIConstants.PATH_UNFOLLOW), values);
if (!response.isError()) { return !response.isError();
return true;
} else {
return false;
}
} }
public APIResponse saveExternalStory(@NonNull String storyTitle, @NonNull String storyUrl) { public APIResponse saveExternalStory(@NonNull String storyTitle, @NonNull String storyUrl) {
@ -386,7 +378,7 @@ public class APIManager {
APIResponse response = post(buildUrl(APIConstants.PATH_SHARE_STORY), values); APIResponse response = post(buildUrl(APIConstants.PATH_SHARE_STORY), values);
// this call returns a new copy of the story with all fields updated and some metadata // this call returns a new copy of the story with all fields updated and some metadata
return (StoriesResponse) response.getResponse(gson, StoriesResponse.class); return response.getResponse(gson, StoriesResponse.class);
} }
public StoriesResponse unshareStory(String storyId, String feedId) { public StoriesResponse unshareStory(String storyId, String feedId) {
@ -396,7 +388,7 @@ public class APIManager {
APIResponse response = post(buildUrl(APIConstants.PATH_UNSHARE_STORY), values); APIResponse response = post(buildUrl(APIConstants.PATH_UNSHARE_STORY), values);
// this call returns a new copy of the story with all fields updated and some metadata // this call returns a new copy of the story with all fields updated and some metadata
return (StoriesResponse) response.getResponse(gson, StoriesResponse.class); return response.getResponse(gson, StoriesResponse.class);
} }
/** /**
@ -438,8 +430,7 @@ public class APIManager {
values.put(APIConstants.PARAMETER_USER_ID, userId); values.put(APIConstants.PARAMETER_USER_ID, userId);
final APIResponse response = get(buildUrl(APIConstants.PATH_USER_PROFILE), values); final APIResponse response = get(buildUrl(APIConstants.PATH_USER_PROFILE), values);
if (!response.isError()) { if (!response.isError()) {
ProfileResponse profileResponse = (ProfileResponse) response.getResponse(gson, ProfileResponse.class); return response.getResponse(gson, ProfileResponse.class);
return profileResponse;
} else { } else {
return null; return null;
} }
@ -452,8 +443,7 @@ public class APIManager {
values.put(APIConstants.PARAMETER_PAGE_NUMBER, Integer.toString(pageNumber)); values.put(APIConstants.PARAMETER_PAGE_NUMBER, Integer.toString(pageNumber));
final APIResponse response = get(buildUrl(APIConstants.PATH_USER_ACTIVITIES), values); final APIResponse response = get(buildUrl(APIConstants.PATH_USER_ACTIVITIES), values);
if (!response.isError()) { if (!response.isError()) {
ActivitiesResponse activitiesResponse = (ActivitiesResponse) response.getResponse(gson, ActivitiesResponse.class); return response.getResponse(gson, ActivitiesResponse.class);
return activitiesResponse;
} else { } else {
return null; return null;
} }
@ -466,8 +456,7 @@ public class APIManager {
values.put(APIConstants.PARAMETER_PAGE_NUMBER, Integer.toString(pageNumber)); values.put(APIConstants.PARAMETER_PAGE_NUMBER, Integer.toString(pageNumber));
final APIResponse response = get(buildUrl(APIConstants.PATH_USER_INTERACTIONS), values); final APIResponse response = get(buildUrl(APIConstants.PATH_USER_INTERACTIONS), values);
if (!response.isError()) { if (!response.isError()) {
InteractionsResponse interactionsResponse = (InteractionsResponse) response.getResponse(gson, InteractionsResponse.class); return response.getResponse(gson, InteractionsResponse.class);
return interactionsResponse;
} else { } else {
return null; return null;
} }
@ -479,8 +468,7 @@ public class APIManager {
values.put(APIConstants.PARAMETER_STORYID, storyId); values.put(APIConstants.PARAMETER_STORYID, storyId);
final APIResponse response = get(buildUrl(APIConstants.PATH_STORY_TEXT), values); final APIResponse response = get(buildUrl(APIConstants.PATH_STORY_TEXT), values);
if (!response.isError()) { if (!response.isError()) {
StoryTextResponse storyTextResponse = (StoryTextResponse) response.getResponse(gson, StoryTextResponse.class); return response.getResponse(gson, StoryTextResponse.class);
return storyTextResponse;
} else { } else {
return null; return null;
} }
@ -520,7 +508,7 @@ public class APIManager {
values.put(APIConstants.PARAMETER_REPLY_TEXT, reply); values.put(APIConstants.PARAMETER_REPLY_TEXT, reply);
APIResponse response = post(buildUrl(APIConstants.PATH_REPLY_TO), values); APIResponse response = post(buildUrl(APIConstants.PATH_REPLY_TO), values);
// this call returns a new copy of the comment with all fields updated // this call returns a new copy of the comment with all fields updated
return (CommentResponse) response.getResponse(gson, CommentResponse.class); return response.getResponse(gson, CommentResponse.class);
} }
public CommentResponse editReply(String storyId, String storyFeedId, String commentUserId, String replyId, String reply) { public CommentResponse editReply(String storyId, String storyFeedId, String commentUserId, String replyId, String reply) {
@ -532,7 +520,7 @@ public class APIManager {
values.put(APIConstants.PARAMETER_REPLY_TEXT, reply); values.put(APIConstants.PARAMETER_REPLY_TEXT, reply);
APIResponse response = post(buildUrl(APIConstants.PATH_EDIT_REPLY), values); APIResponse response = post(buildUrl(APIConstants.PATH_EDIT_REPLY), values);
// this call returns a new copy of the comment with all fields updated // this call returns a new copy of the comment with all fields updated
return (CommentResponse) response.getResponse(gson, CommentResponse.class); return response.getResponse(gson, CommentResponse.class);
} }
public CommentResponse deleteReply(String storyId, String storyFeedId, String commentUserId, String replyId) { public CommentResponse deleteReply(String storyId, String storyFeedId, String commentUserId, String replyId) {
@ -543,7 +531,7 @@ public class APIManager {
values.put(APIConstants.PARAMETER_REPLY_ID, replyId); values.put(APIConstants.PARAMETER_REPLY_ID, replyId);
APIResponse response = post(buildUrl(APIConstants.PATH_DELETE_REPLY), values); APIResponse response = post(buildUrl(APIConstants.PATH_DELETE_REPLY), values);
// this call returns a new copy of the comment with all fields updated // this call returns a new copy of the comment with all fields updated
return (CommentResponse) response.getResponse(gson, CommentResponse.class); return response.getResponse(gson, CommentResponse.class);
} }
public NewsBlurResponse addFolder(String folderName) { public NewsBlurResponse addFolder(String folderName) {
@ -711,10 +699,10 @@ public class APIManager {
} }
private String builderGetParametersString(ContentValues values) { private String builderGetParametersString(ContentValues values) {
List<String> parameters = new ArrayList<String>(); List<String> parameters = new ArrayList<>();
for (Entry<String, Object> entry : values.valueSet()) { for (Entry<String, Object> entry : values.valueSet()) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append((String) entry.getKey()); builder.append(entry.getKey());
builder.append("="); builder.append("=");
builder.append(NetworkUtils.encodeURL((String) entry.getValue())); builder.append(NetworkUtils.encodeURL((String) entry.getValue()));
parameters.add(builder.toString()); parameters.add(builder.toString());
@ -749,7 +737,7 @@ public class APIManager {
formBody.writeTo(buffer); formBody.writeTo(buffer);
body = buffer.readUtf8(); body = buffer.readUtf8();
} catch (Exception e) { } catch (Exception e) {
; // this is debug code, do not raise // this is debug code, do not raise
} }
Log.d(this.getClass().getName(), "post body: " + body); Log.d(this.getClass().getName(), "post body: " + body);
} }

View file

@ -3,7 +3,6 @@ package com.newsblur.network;
import java.io.IOException; import java.io.IOException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@ -109,7 +108,6 @@ public class APIResponse {
* may be used for calls that return data, or the parent class may be used if no * may be used for calls that return data, or the parent class may be used if no
* return data are expected. * return data are expected.
*/ */
@SuppressWarnings("unchecked")
public <T extends NewsBlurResponse> T getResponse(Gson gson, Class<T> classOfT) { public <T extends NewsBlurResponse> T getResponse(Gson gson, Class<T> classOfT) {
if (this.isError) { if (this.isError) {
// if we encountered an error, make a generic response type and populate // if we encountered an error, make a generic response type and populate

View file

@ -1,5 +1,6 @@
package com.newsblur.service; package com.newsblur.service;
import com.newsblur.util.ExtensionsKt;
import com.newsblur.util.PrefConstants; import com.newsblur.util.PrefConstants;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
@ -8,7 +9,7 @@ public class CleanupService extends SubService {
public static boolean activelyRunning = false; public static boolean activelyRunning = false;
public CleanupService(NBSyncService parent) { public CleanupService(NBSyncService parent) {
super(parent); super(parent, ExtensionsKt.NBScope);
} }
@Override @Override

View file

@ -3,6 +3,7 @@ package com.newsblur.service;
import android.util.Log; import android.util.Log;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.ExtensionsKt;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
import java.util.Collections; import java.util.Collections;
@ -19,7 +20,7 @@ public class ImagePrefetchService extends SubService {
static Set<String> ThumbnailQueue = Collections.synchronizedSet(new HashSet<>()); static Set<String> ThumbnailQueue = Collections.synchronizedSet(new HashSet<>());
public ImagePrefetchService(NBSyncService parent) { public ImagePrefetchService(NBSyncService parent) {
super(parent); super(parent, ExtensionsKt.NBScope);
} }
@Override @Override
@ -33,7 +34,6 @@ public class ImagePrefetchService extends SubService {
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return; if (! PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return; if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
startExpensiveCycle();
com.newsblur.util.Log.d(this, "story images to prefetch: " + StoryImageQueue.size()); com.newsblur.util.Log.d(this, "story images to prefetch: " + StoryImageQueue.size());
// on each batch, re-query the DB for images associated with yet-unread stories // on each batch, re-query the DB for images associated with yet-unread stories
// this is a bit expensive, but we are running totally async at a really low priority // this is a bit expensive, but we are running totally async at a really low priority
@ -66,7 +66,6 @@ public class ImagePrefetchService extends SubService {
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return; if (! PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return; if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
startExpensiveCycle();
com.newsblur.util.Log.d(this, "story thumbs to prefetch: " + StoryImageQueue.size()); com.newsblur.util.Log.d(this, "story thumbs to prefetch: " + StoryImageQueue.size());
// on each batch, re-query the DB for images associated with yet-unread stories // on each batch, re-query the DB for images associated with yet-unread stories
// this is a bit expensive, but we are running totally async at a really low priority // this is a bit expensive, but we are running totally async at a really low priority

View file

@ -38,6 +38,7 @@ import com.newsblur.network.domain.NewsBlurResponse;
import com.newsblur.network.domain.StoriesResponse; import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.network.domain.UnreadCountResponse; import com.newsblur.network.domain.UnreadCountResponse;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.CursorFilters;
import com.newsblur.util.DefaultFeedView; import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.FileCache; import com.newsblur.util.FileCache;
@ -49,6 +50,7 @@ import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadFilter; import com.newsblur.util.ReadFilter;
import com.newsblur.util.StateFilter; import com.newsblur.util.StateFilter;
import com.newsblur.util.StoryOrder; import com.newsblur.util.StoryOrder;
import com.newsblur.util.UIUtils;
import com.newsblur.widget.WidgetUtils; import com.newsblur.widget.WidgetUtils;
import java.util.ArrayList; import java.util.ArrayList;
@ -411,6 +413,8 @@ public class NBSyncService extends JobService {
ActionsRunning = true; ActionsRunning = true;
StateFilter stateFilter = PrefsUtils.getStateFilter(this);
actionsloop : while (c.moveToNext()) { actionsloop : while (c.moveToNext()) {
sendSyncUpdate(UPDATE_STATUS); sendSyncUpdate(UPDATE_STATUS);
String id = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_ID)); String id = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_ID));
@ -427,7 +431,7 @@ public class NBSyncService extends JobService {
if ((ra.getTried() > 0) && (PendingFeed != null)) continue actionsloop; if ((ra.getTried() > 0) && (PendingFeed != null)) continue actionsloop;
com.newsblur.util.Log.d(this, "attempting action: " + ra.toContentValues().toString()); com.newsblur.util.Log.d(this, "attempting action: " + ra.toContentValues().toString());
NewsBlurResponse response = ra.doRemote(apiManager, dbHelper); NewsBlurResponse response = ra.doRemote(apiManager, dbHelper, stateFilter);
if (response == null) { if (response == null) {
com.newsblur.util.Log.e(this.getClass().getName(), "Discarding reading action with client-side error."); com.newsblur.util.Log.e(this.getClass().getName(), "Discarding reading action with client-side error.");
@ -471,7 +475,7 @@ public class NBSyncService extends JobService {
Log.d(this, "double-checking " + FollowupActions.size() + " actions"); Log.d(this, "double-checking " + FollowupActions.size() + " actions");
int impactFlags = 0; int impactFlags = 0;
for (ReadingAction ra : FollowupActions) { for (ReadingAction ra : FollowupActions) {
int impact = ra.doLocal(dbHelper, true); int impact = ra.doLocal(this, dbHelper, true);
impactFlags |= impact; impactFlags |= impact;
} }
sendSyncUpdate(impactFlags); sendSyncUpdate(impactFlags);
@ -766,7 +770,7 @@ public class NBSyncService extends JobService {
return; return;
} }
prepareReadingSession(dbHelper, fs); prepareReadingSession(this, dbHelper, fs);
LastFeedSet = fs; LastFeedSet = fs;
@ -785,8 +789,7 @@ public class NBSyncService extends JobService {
int pageNumber = FeedPagesSeen.get(fs); int pageNumber = FeedPagesSeen.get(fs);
int totalStoriesSeen = FeedStoriesSeen.get(fs); int totalStoriesSeen = FeedStoriesSeen.get(fs);
StoryOrder order = PrefsUtils.getStoryOrder(this, fs); CursorFilters cursorFilters = new CursorFilters(this, fs);
ReadFilter filter = PrefsUtils.getReadFilter(this, fs);
StorySyncRunning = true; StorySyncRunning = true;
sendSyncUpdate(UPDATE_STATUS); sendSyncUpdate(UPDATE_STATUS);
@ -802,7 +805,7 @@ public class NBSyncService extends JobService {
} }
pageNumber++; pageNumber++;
StoriesResponse apiResponse = apiManager.getStories(fs, pageNumber, order, filter); StoriesResponse apiResponse = apiManager.getStories(fs, pageNumber, cursorFilters.getStoryOrder(), cursorFilters.getReadFilter());
if (! isStoryResponseGood(apiResponse)) return; if (! isStoryResponseGood(apiResponse)) return;
@ -810,7 +813,7 @@ public class NBSyncService extends JobService {
return; return;
} }
insertStories(apiResponse, fs); insertStories(apiResponse, fs, cursorFilters.getStateFilter());
// re-do any very recent actions that were incorrectly overwritten by this page // re-do any very recent actions that were incorrectly overwritten by this page
finishActions(); finishActions();
sendSyncUpdate(UPDATE_STORY | UPDATE_STATUS); sendSyncUpdate(UPDATE_STORY | UPDATE_STATUS);
@ -855,7 +858,7 @@ public class NBSyncService extends JobService {
private long workaroundReadStoryTimestamp; private long workaroundReadStoryTimestamp;
private long workaroundGloblaSharedStoryTimestamp; private long workaroundGloblaSharedStoryTimestamp;
private void insertStories(StoriesResponse apiResponse, FeedSet fs) { private void insertStories(StoriesResponse apiResponse, FeedSet fs, StateFilter stateFilter) {
if (fs.isAllRead()) { if (fs.isAllRead()) {
// Ugly Hack Warning: the API doesn't vend the sortation key necessary to display // Ugly Hack Warning: the API doesn't vend the sortation key necessary to display
// stories when in the "read stories" view. It does, however, return them in the // stories when in the "read stories" view. It does, however, return them in the
@ -919,12 +922,12 @@ public class NBSyncService extends JobService {
} }
com.newsblur.util.Log.d(NBSyncService.class.getName(), "got stories from main fetch loop: " + apiResponse.stories.length); com.newsblur.util.Log.d(NBSyncService.class.getName(), "got stories from main fetch loop: " + apiResponse.stories.length);
dbHelper.insertStories(apiResponse, true); dbHelper.insertStories(apiResponse, stateFilter, true);
} }
void insertStories(StoriesResponse apiResponse) { void insertStories(StoriesResponse apiResponse, StateFilter stateFilter) {
com.newsblur.util.Log.d(NBSyncService.class.getName(), "got stories from sub sync: " + apiResponse.stories.length); com.newsblur.util.Log.d(NBSyncService.class.getName(), "got stories from sub sync: " + apiResponse.stories.length);
dbHelper.insertStories(apiResponse, false); dbHelper.insertStories(apiResponse, stateFilter, false);
} }
void prefetchOriginalText(StoriesResponse apiResponse) { void prefetchOriginalText(StoriesResponse apiResponse) {
@ -1144,8 +1147,9 @@ public class NBSyncService extends JobService {
* set but also when we sync a page of stories, since there are no guarantees which * set but also when we sync a page of stories, since there are no guarantees which
* will happen first. * will happen first.
*/ */
public static void prepareReadingSession(BlurDatabaseHelper dbHelper, FeedSet fs) { public static void prepareReadingSession(Context context, BlurDatabaseHelper dbHelper, FeedSet fs) {
synchronized (PENDING_FEED_MUTEX) { synchronized (PENDING_FEED_MUTEX) {
CursorFilters cursorFilters = new CursorFilters(context, fs);
if (! fs.equals(dbHelper.getSessionFeedSet())) { if (! fs.equals(dbHelper.getSessionFeedSet())) {
com.newsblur.util.Log.d(NBSyncService.class.getName(), "preparing new reading session"); com.newsblur.util.Log.d(NBSyncService.class.getName(), "preparing new reading session");
// the next fetch will be the start of a new reading session; clear it so it // the next fetch will be the start of a new reading session; clear it so it
@ -1153,10 +1157,10 @@ public class NBSyncService extends JobService {
dbHelper.clearStorySession(); dbHelper.clearStorySession();
// don't just rely on the auto-prepare code when fetching stories, it might be called // don't just rely on the auto-prepare code when fetching stories, it might be called
// after we insert our first page and not trigger // after we insert our first page and not trigger
dbHelper.prepareReadingSession(fs); dbHelper.prepareReadingSession(fs, cursorFilters.getStateFilter(), cursorFilters.getReadFilter());
// note which feedset we are loading so we can trigger another reset when it changes // note which feedset we are loading so we can trigger another reset when it changes
dbHelper.setSessionFeedSet(fs); dbHelper.setSessionFeedSet(fs);
dbHelper.sendSyncUpdate(UPDATE_STORY | UPDATE_STATUS); UIUtils.syncUpdateStatus(context, UPDATE_STORY | UPDATE_STATUS);
} }
} }
} }

View file

@ -5,6 +5,7 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_TEXT;
import com.newsblur.database.DatabaseConstants; import com.newsblur.database.DatabaseConstants;
import com.newsblur.network.domain.StoryTextResponse; import com.newsblur.network.domain.StoryTextResponse;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.ExtensionsKt;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
import java.util.HashSet; import java.util.HashSet;
@ -22,14 +23,12 @@ public class OriginalTextService extends SubService {
private static final Pattern imgSniff = Pattern.compile("<img[^>]*src=(['\"])((?:(?!\\1).)*)\\1[^>]*>", Pattern.CASE_INSENSITIVE); private static final Pattern imgSniff = Pattern.compile("<img[^>]*src=(['\"])((?:(?!\\1).)*)\\1[^>]*>", Pattern.CASE_INSENSITIVE);
/** story hashes we need to fetch (from newly found stories) */ /** story hashes we need to fetch (from newly found stories) */
private static Set<String> Hashes; private static final Set<String> Hashes = new HashSet<>();
static {Hashes = new HashSet<String>();}
/** story hashes we should fetch ASAP (they are waiting on-screen) */ /** story hashes we should fetch ASAP (they are waiting on-screen) */
private static Set<String> PriorityHashes; private static final Set<String> PriorityHashes = new HashSet<>();
static {PriorityHashes = new HashSet<String>();}
public OriginalTextService(NBSyncService parent) { public OriginalTextService(NBSyncService parent) {
super(parent); super(parent, ExtensionsKt.NBScope);
} }
@Override @Override

View file

@ -1,129 +0,0 @@
package com.newsblur.service;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
import android.os.Process;
import com.newsblur.NbApplication;
import com.newsblur.util.AppConstants;
import com.newsblur.util.Log;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.RejectedExecutionException;
/**
* A utility construct to make NbSyncService a bit more modular by encapsulating sync tasks
* that can be run fully asynchronously from the main sync loop. Like all of the sync service,
* flags and data used by these modules need to be static so that parts of the app without a
* handle to the service object can access them.
*/
public abstract class SubService {
protected NBSyncService parent;
private ThreadPoolExecutor executor;
private long cycleStartTime = 0L;
private SubService() {
; // no default construction
}
SubService(NBSyncService parent) {
this.parent = parent;
executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
public void start() {
Runnable r = new Runnable() {
public void run() {
if (parent.stopSync()) return;
if (!NbApplication.isAppForeground()) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE );
} else {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE + Process.THREAD_PRIORITY_LESS_FAVORABLE );
}
Thread.currentThread().setName(this.getClass().getName());
exec_();
}
};
try {
executor.execute(r);
// enqueue a check task that will run strictly after the real one, so the callback
// can effectively check queue size to see if there are queued tasks
executor.execute(new Runnable() {
public void run() {
parent.checkCompletion();
parent.sendSyncUpdate(UPDATE_STATUS);
}
});
} catch (RejectedExecutionException ree) {
// this is perfectly normal, as service soft-stop mechanics might have shut down our thread pool
// while peer subservices are still running
}
}
private synchronized void exec_() {
try {
exec();
cycleStartTime = 0;
} catch (Exception e) {
com.newsblur.util.Log.e(this.getClass().getName(), "Sync error.", e);
}
}
protected abstract void exec();
public void shutdown() {
Log.d(this, "SubService stopping");
executor.shutdown();
try {
executor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
} finally {
Log.d(this, "SubService stopped");
}
}
public boolean isRunning() {
// don't advise completion until there are no tasks, or just one check task left
return (executor.getQueue().size() > 0);
}
/**
* If called at the beginning of an expensive loop in a SubService, enforces the maximum duty cycle
* defined in AppConstants by sleeping for a short while so the SubService does not dominate system
* resources.
*/
protected void startExpensiveCycle() {
if (cycleStartTime == 0) {
cycleStartTime = System.nanoTime();
return;
}
double lastCycleTime = (System.nanoTime() - cycleStartTime);
if (lastCycleTime < 1) return;
cycleStartTime = System.nanoTime();
double cooloffTime = lastCycleTime * (1.0 - AppConstants.MAX_BG_DUTY_CYCLE);
if (cooloffTime < 1) return;
long cooloffTimeMs = Math.round(cooloffTime / 1000000.0);
if (cooloffTimeMs > AppConstants.DUTY_CYCLE_BACKOFF_CAP_MILLIS) cooloffTimeMs = AppConstants.DUTY_CYCLE_BACKOFF_CAP_MILLIS;
if (NbApplication.isAppForeground()) {
com.newsblur.util.Log.d(this.getClass().getName(), "Sleeping for : " + cooloffTimeMs + "ms to enforce max duty cycle.");
try {
Thread.sleep(cooloffTimeMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}

View file

@ -0,0 +1,66 @@
package com.newsblur.service
import com.newsblur.util.Log
import com.newsblur.util.NBScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.concurrent.CancellationException
/**
* A utility construct to make NbSyncService a bit more modular by encapsulating sync tasks
* that can be run fully asynchronously from the main sync loop. Like all of the sync service,
* flags and data used by these modules need to be static so that parts of the app without a
* handle to the service object can access them.
*/
abstract class SubService(
@JvmField
protected val parent: NBSyncService,
private val coroutineScope: CoroutineScope = NBScope,
) {
private var mainJob: Job? = null
protected abstract fun exec()
fun start() {
mainJob = coroutineScope.launch(Dispatchers.IO) {
if (parent.stopSync()) return@launch
Thread.currentThread().name = this@SubService.javaClass.name
execInternal()
if (isActive) {
parent.checkCompletion()
parent.sendSyncUpdate(NBSyncReceiver.UPDATE_STATUS)
}
}
}
private suspend fun execInternal() = coroutineScope {
try {
ensureActive()
exec()
} catch (e: Exception) {
Log.e(this@SubService.javaClass.name, "Sync error.", e)
}
}
fun shutdown() {
Log.d(this, "SubService shutdown")
try {
mainJob?.cancel()
} catch (e: CancellationException) {
Log.d(this, "SubService cancelled")
} finally {
Log.d(this, "SubService stopped")
}
}
val isRunning: Boolean
get() = mainJob?.isActive ?: false
}

View file

@ -24,8 +24,6 @@ import kotlinx.coroutines.launch
*/ */
class SubscriptionSyncService : JobService() { class SubscriptionSyncService : JobService() {
private val scope = NBScope
override fun onStartJob(params: JobParameters?): Boolean { override fun onStartJob(params: JobParameters?): Boolean {
Log.d(this, "onStartJob") Log.d(this, "onStartJob")
if (!PrefsUtils.hasCookie(this)) { if (!PrefsUtils.hasCookie(this)) {
@ -33,10 +31,10 @@ class SubscriptionSyncService : JobService() {
return false return false
} }
val subscriptionManager = SubscriptionManagerImpl(this@SubscriptionSyncService, scope) val subscriptionManager = SubscriptionManagerImpl(this@SubscriptionSyncService)
subscriptionManager.startBillingConnection(object : SubscriptionsListener { subscriptionManager.startBillingConnection(object : SubscriptionsListener {
override fun onBillingConnectionReady() { override fun onBillingConnectionReady() {
scope.launch { NBScope.launch {
subscriptionManager.syncActiveSubscription() subscriptionManager.syncActiveSubscription()
Log.d(this, "sync active subscription completed.") Log.d(this, "sync active subscription completed.")
// manually call jobFinished after work is done // manually call jobFinished after work is done

View file

@ -3,8 +3,10 @@ package com.newsblur.service;
import com.newsblur.network.domain.StoriesResponse; import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.network.domain.UnreadStoryHashesResponse; import com.newsblur.network.domain.UnreadStoryHashesResponse;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.ExtensionsKt;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
import com.newsblur.util.StateFilter;
import com.newsblur.util.StoryOrder; import com.newsblur.util.StoryOrder;
import java.util.ArrayList; import java.util.ArrayList;
@ -25,7 +27,7 @@ public class UnreadsService extends SubService {
static { StoryHashQueue = new ArrayList<String>(); } static { StoryHashQueue = new ArrayList<String>(); }
public UnreadsService(NBSyncService parent) { public UnreadsService(NBSyncService parent) {
super(parent); super(parent, ExtensionsKt.NBScope);
} }
@Override @Override
@ -137,8 +139,6 @@ public class UnreadsService extends SubService {
boolean isTextPrefetchEnabled = PrefsUtils.isTextPrefetchEnabled(parent); boolean isTextPrefetchEnabled = PrefsUtils.isTextPrefetchEnabled(parent);
if (! (isOfflineEnabled || isEnableNotifications)) return; if (! (isOfflineEnabled || isEnableNotifications)) return;
startExpensiveCycle();
List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE); List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
List<String> hashSkips = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE); List<String> hashSkips = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
batchloop: for (String hash : StoryHashQueue) { batchloop: for (String hash : StoryHashQueue) {
@ -156,7 +156,8 @@ public class UnreadsService extends SubService {
break unreadsyncloop; break unreadsyncloop;
} }
parent.insertStories(response); StateFilter stateFilter = PrefsUtils.getStateFilter(parent);
parent.insertStories(response, stateFilter);
for (String hash : hashBatch) { for (String hash : hashBatch) {
StoryHashQueue.remove(hash); StoryHashQueue.remove(hash);
} }

View file

@ -0,0 +1,16 @@
package com.newsblur.util
import android.content.Context
data class CursorFilters(
val stateFilter: StateFilter,
val readFilter: ReadFilter,
val storyOrder: StoryOrder,
) {
constructor(context: Context, fs: FeedSet) : this(
stateFilter = PrefsUtils.getStateFilter(context),
readFilter = PrefsUtils.getReadFilter(context, fs),
storyOrder = PrefsUtils.getStoryOrder(context, fs),
)
}

View file

@ -20,6 +20,7 @@ fun <R> CoroutineScope.executeAsyncTask(
withContext(Dispatchers.Main) { onPostExecute(result) } withContext(Dispatchers.Main) { onPostExecute(result) }
} }
@JvmField
val NBScope = CoroutineScope( val NBScope = CoroutineScope(
CoroutineName(TAG) + CoroutineName(TAG) +
Dispatchers.Default + Dispatchers.Default +

View file

@ -34,12 +34,14 @@ class FeedUtils(
@JvmField @JvmField
var currentFolderName: String? = null var currentFolderName: String? = null
fun prepareReadingSession(fs: FeedSet?, resetFirst: Boolean) { fun prepareReadingSession(context: Context, fs: FeedSet?, resetFirst: Boolean) {
NBScope.executeAsyncTask( NBScope.executeAsyncTask(
doInBackground = { doInBackground = {
try { try {
if (resetFirst) NBSyncService.resetReadingSession(dbHelper) if (resetFirst) NBSyncService.resetReadingSession(dbHelper)
NBSyncService.prepareReadingSession(dbHelper, fs) fs?.let {
NBSyncService.prepareReadingSession(context, dbHelper, it)
}
} catch (e: Exception) { } catch (e: Exception) {
// this is a UI hinting call and might fail if the DB is being reset, but that is fine // this is a UI hinting call and might fail if the DB is being reset, but that is fine
} }
@ -63,7 +65,7 @@ class FeedUtils(
NBScope.executeAsyncTask( NBScope.executeAsyncTask(
doInBackground = { doInBackground = {
val ra = if (saved) ReadingAction.saveStory(storyHash, userTags) else ReadingAction.unsaveStory(storyHash) val ra = if (saved) ReadingAction.saveStory(storyHash, userTags) else ReadingAction.unsaveStory(storyHash)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_STORY) syncUpdateStatus(context, UPDATE_STORY)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
triggerSync(context) triggerSync(context)
@ -306,7 +308,7 @@ class FeedUtils(
NBScope.executeAsyncTask( NBScope.executeAsyncTask(
doInBackground = { doInBackground = {
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
val impact = ra.doLocal(dbHelper) val impact = ra.doLocal(context, dbHelper)
syncUpdateStatus(context, impact) syncUpdateStatus(context, impact)
triggerSync(context) triggerSync(context)
} }
@ -347,7 +349,7 @@ class FeedUtils(
} }
val ra = ReadingAction.shareStory(story.storyHash, story.id, story.feedId, sourceUserId, comment) val ra = ReadingAction.shareStory(story.storyHash, story.id, story.feedId, sourceUserId, comment)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY) syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY)
triggerSync(context) triggerSync(context)
} }
@ -355,7 +357,7 @@ class FeedUtils(
fun renameFeed(context: Context, feedId: String?, newFeedName: String?) { fun renameFeed(context: Context, feedId: String?, newFeedName: String?) {
val ra = ReadingAction.renameFeed(feedId, newFeedName) val ra = ReadingAction.renameFeed(feedId, newFeedName)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
val impact = ra.doLocal(dbHelper) val impact = ra.doLocal(context, dbHelper)
syncUpdateStatus(context, impact) syncUpdateStatus(context, impact)
triggerSync(context) triggerSync(context)
} }
@ -363,7 +365,7 @@ class FeedUtils(
fun unshareStory(story: Story, context: Context) { fun unshareStory(story: Story, context: Context) {
val ra = ReadingAction.unshareStory(story.storyHash, story.id, story.feedId) val ra = ReadingAction.unshareStory(story.storyHash, story.id, story.feedId)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY) syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY)
triggerSync(context) triggerSync(context)
} }
@ -371,7 +373,7 @@ class FeedUtils(
fun likeComment(story: Story, commentUserId: String?, context: Context) { fun likeComment(story: Story, commentUserId: String?, context: Context) {
val ra = ReadingAction.likeComment(story.id, commentUserId, story.feedId) val ra = ReadingAction.likeComment(story.id, commentUserId, story.feedId)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL) syncUpdateStatus(context, UPDATE_SOCIAL)
triggerSync(context) triggerSync(context)
} }
@ -379,7 +381,7 @@ class FeedUtils(
fun unlikeComment(story: Story, commentUserId: String?, context: Context) { fun unlikeComment(story: Story, commentUserId: String?, context: Context) {
val ra = ReadingAction.unlikeComment(story.id, commentUserId, story.feedId) val ra = ReadingAction.unlikeComment(story.id, commentUserId, story.feedId)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL) syncUpdateStatus(context, UPDATE_SOCIAL)
triggerSync(context) triggerSync(context)
} }
@ -387,7 +389,7 @@ class FeedUtils(
fun replyToComment(storyId: String?, feedId: String?, commentUserId: String?, replyText: String?, context: Context) { fun replyToComment(storyId: String?, feedId: String?, commentUserId: String?, replyText: String?, context: Context) {
val ra = ReadingAction.replyToComment(storyId, feedId, commentUserId, replyText) val ra = ReadingAction.replyToComment(storyId, feedId, commentUserId, replyText)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL) syncUpdateStatus(context, UPDATE_SOCIAL)
triggerSync(context) triggerSync(context)
} }
@ -395,7 +397,7 @@ class FeedUtils(
fun updateReply(context: Context, story: Story, commentUserId: String?, replyId: String?, replyText: String?) { fun updateReply(context: Context, story: Story, commentUserId: String?, replyId: String?, replyText: String?) {
val ra = ReadingAction.updateReply(story.id, story.feedId, commentUserId, replyId, replyText) val ra = ReadingAction.updateReply(story.id, story.feedId, commentUserId, replyId, replyText)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL) syncUpdateStatus(context, UPDATE_SOCIAL)
triggerSync(context) triggerSync(context)
} }
@ -403,7 +405,7 @@ class FeedUtils(
fun deleteReply(context: Context, story: Story, commentUserId: String?, replyId: String?) { fun deleteReply(context: Context, story: Story, commentUserId: String?, replyId: String?) {
val ra = ReadingAction.deleteReply(story.id, story.feedId, commentUserId, replyId) val ra = ReadingAction.deleteReply(story.id, story.feedId, commentUserId, replyId)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL) syncUpdateStatus(context, UPDATE_SOCIAL)
triggerSync(context) triggerSync(context)
} }
@ -448,7 +450,7 @@ class FeedUtils(
} }
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_METADATA) syncUpdateStatus(context, UPDATE_METADATA)
triggerSync(context) triggerSync(context)
@ -459,7 +461,7 @@ class FeedUtils(
fun instaFetchFeed(context: Context, feedId: String?) { fun instaFetchFeed(context: Context, feedId: String?) {
val ra = ReadingAction.instaFetch(feedId) val ra = ReadingAction.instaFetch(feedId)
dbHelper.enqueueAction(ra) dbHelper.enqueueAction(ra)
ra.doLocal(dbHelper) ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_METADATA) syncUpdateStatus(context, UPDATE_METADATA)
triggerSync(context) triggerSync(context)
} }

View file

@ -103,8 +103,6 @@ public class NotificationUtils {
} }
} }
// addAction deprecated in 23 but replacement not avail until 21
@SuppressWarnings("deprecation")
private static Notification buildStoryNotification(Story story, Cursor cursor, Context context, FileCache iconCache) { private static Notification buildStoryNotification(Story story, Cursor cursor, Context context, FileCache iconCache) {
Log.d(NotificationUtils.class.getName(), "Building notification"); Log.d(NotificationUtils.class.getName(), "Building notification");
Intent i = new Intent(context, FeedReading.class); Intent i = new Intent(context, FeedReading.class);

View file

@ -22,6 +22,8 @@ import android.graphics.BitmapFactory;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import android.util.Log; import android.util.Log;
@ -40,7 +42,7 @@ public class PrefsUtils {
private PrefsUtils() {} // util class - no instances private PrefsUtils() {} // util class - no instances
public static void saveCustomServer(Context context, String customServer) { public static void saveCustomServer(Context context, @Nullable String customServer) {
if (customServer == null) return; if (customServer == null) return;
if (customServer.length() <= 0) return; if (customServer.length() <= 0) return;
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0); SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
@ -88,7 +90,7 @@ public class PrefsUtils {
} }
public static void updateVersion(Context context, String appVersion) { public static void updateVersion(Context context, @Nullable String appVersion) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0); SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
// store the current version // store the current version
prefs.edit().putString(AppConstants.LAST_APP_VERSION, appVersion).commit(); prefs.edit().putString(AppConstants.LAST_APP_VERSION, appVersion).commit();
@ -96,6 +98,7 @@ public class PrefsUtils {
prefs.edit().putLong(AppConstants.LAST_SYNC_TIME, 0L).commit(); prefs.edit().putLong(AppConstants.LAST_SYNC_TIME, 0L).commit();
} }
@Nullable
public static String getVersion(Context context) { public static String getVersion(Context context) {
try { try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
@ -105,7 +108,7 @@ public class PrefsUtils {
} }
} }
public static String createFeedbackLink(Context context, BlurDatabaseHelper dbHelper) { public static String createFeedbackLink(Context context, @NonNull BlurDatabaseHelper dbHelper) {
StringBuilder s = new StringBuilder(AppConstants.FEEDBACK_URL); StringBuilder s = new StringBuilder(AppConstants.FEEDBACK_URL);
s.append("<give us some feedback!>%0A%0A%0A"); s.append("<give us some feedback!>%0A%0A%0A");
String info = getDebugInfo(context, dbHelper); String info = getDebugInfo(context, dbHelper);
@ -113,7 +116,7 @@ public class PrefsUtils {
return s.toString(); return s.toString();
} }
public static void sendLogEmail(Context context, BlurDatabaseHelper dbHelper) { public static void sendLogEmail(Context context, @NonNull BlurDatabaseHelper dbHelper) {
File f = com.newsblur.util.Log.getLogfile(); File f = com.newsblur.util.Log.getLogfile();
if (f == null) return; if (f == null) return;
String debugInfo = "Tell us a bit about your problem:\n\n\n\n" + getDebugInfo(context, dbHelper); String debugInfo = "Tell us a bit about your problem:\n\n\n\n" + getDebugInfo(context, dbHelper);
@ -129,7 +132,7 @@ public class PrefsUtils {
} }
} }
private static String getDebugInfo(Context context, BlurDatabaseHelper dbHelper) { private static String getDebugInfo(Context context, @NonNull BlurDatabaseHelper dbHelper) {
StringBuilder s = new StringBuilder(); StringBuilder s = new StringBuilder();
s.append("app version: ").append(getVersion(context)); s.append("app version: ").append(getVersion(context));
s.append("\n"); s.append("\n");
@ -167,7 +170,7 @@ public class PrefsUtils {
return s.toString(); return s.toString();
} }
public static void logout(Context context, BlurDatabaseHelper dbHelper) { public static void logout(Context context, @NonNull BlurDatabaseHelper dbHelper) {
NBSyncService.softInterrupt(); NBSyncService.softInterrupt();
NBSyncService.clearState(); NBSyncService.clearState();
@ -194,7 +197,7 @@ public class PrefsUtils {
context.startActivity(i); context.startActivity(i);
} }
public static void clearPrefsAndDbForLoginAs(Context context, BlurDatabaseHelper dbHelper) { public static void clearPrefsAndDbForLoginAs(Context context, @NonNull BlurDatabaseHelper dbHelper) {
NBSyncService.softInterrupt(); NBSyncService.softInterrupt();
NBSyncService.clearState(); NBSyncService.clearState();
@ -252,6 +255,7 @@ public class PrefsUtils {
saveUserImage(context, profile.photoUrl); saveUserImage(context, profile.photoUrl);
} }
@Nullable
public static String getUserId(Context context) { public static String getUserId(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0); SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return preferences.getString(PrefConstants.USER_ID, null); return preferences.getString(PrefConstants.USER_ID, null);
@ -282,7 +286,7 @@ public class PrefsUtils {
} }
private static void saveUserImage(final Context context, String pictureUrl) { private static void saveUserImage(final Context context, String pictureUrl) {
Bitmap bitmap = null; Bitmap bitmap;
try { try {
URL url = new URL(pictureUrl); URL url = new URL(pictureUrl);
URLConnection connection; URLConnection connection;

View file

@ -6,6 +6,7 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_SOCIAL;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY; import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import java.io.Serializable; import java.io.Serializable;
@ -277,7 +278,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) { public NewsBlurResponse doRemote(APIManager apiManager, BlurDatabaseHelper dbHelper, 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
@ -370,7 +371,7 @@ public class ReadingAction implements Serializable {
if (storiesResponse != null) { if (storiesResponse != null) {
result = storiesResponse; result = storiesResponse;
if (storiesResponse.story != null) { if (storiesResponse.story != null) {
dbHelper.updateStory(storiesResponse, true); dbHelper.updateStory(storiesResponse, stateFilter, true);
} else { } else {
com.newsblur.util.Log.w(this, "failed to refresh story data after action"); com.newsblur.util.Log.w(this, "failed to refresh story data after action");
} }
@ -391,8 +392,8 @@ public class ReadingAction implements Serializable {
return result; return result;
} }
public int doLocal(BlurDatabaseHelper dbHelper) { public int doLocal(Context context, BlurDatabaseHelper dbHelper) {
return doLocal(dbHelper, false); return doLocal(context, dbHelper, false);
} }
/** /**
@ -402,7 +403,8 @@ 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(BlurDatabaseHelper dbHelper, boolean isFollowup) { public int doLocal(Context context, BlurDatabaseHelper dbHelper, boolean isFollowup) {
String userId = PrefsUtils.getUserId(context);
int impact = 0; int impact = 0;
switch (type) { switch (type) {
@ -434,32 +436,32 @@ public class ReadingAction implements Serializable {
case SHARE: case SHARE:
if (isFollowup) break; // shares are only placeholders if (isFollowup) break; // shares are only placeholders
dbHelper.setStoryShared(storyHash, true); dbHelper.setStoryShared(storyHash, userId, true);
dbHelper.insertCommentPlaceholder(storyId, feedId, commentReplyText); dbHelper.insertCommentPlaceholder(storyId, userId, commentReplyText);
impact |= UPDATE_SOCIAL; impact |= UPDATE_SOCIAL;
impact |= UPDATE_STORY; impact |= UPDATE_STORY;
break; break;
case UNSHARE: case UNSHARE:
dbHelper.setStoryShared(storyHash, false); dbHelper.setStoryShared(storyHash, userId, false);
dbHelper.clearSelfComments(storyId); dbHelper.clearSelfComments(storyId, userId);
impact |= UPDATE_SOCIAL; impact |= UPDATE_SOCIAL;
impact |= UPDATE_STORY; impact |= UPDATE_STORY;
break; break;
case LIKE_COMMENT: case LIKE_COMMENT:
dbHelper.setCommentLiked(storyId, commentUserId, feedId, true); dbHelper.setCommentLiked(storyId, commentUserId, userId, true);
impact |= UPDATE_SOCIAL; impact |= UPDATE_SOCIAL;
break; break;
case UNLIKE_COMMENT: case UNLIKE_COMMENT:
dbHelper.setCommentLiked(storyId, commentUserId, feedId, false); dbHelper.setCommentLiked(storyId, commentUserId, userId, false);
impact |= UPDATE_SOCIAL; impact |= UPDATE_SOCIAL;
break; break;
case REPLY: case REPLY:
if (isFollowup) break; // replies are only placeholders if (isFollowup) break; // replies are only placeholders
dbHelper.insertReplyPlaceholder(storyId, feedId, commentUserId, commentReplyText); dbHelper.insertReplyPlaceholder(storyId, userId, commentUserId, commentReplyText);
break; break;
case EDIT_REPLY: case EDIT_REPLY:

View file

@ -2,6 +2,7 @@ package com.newsblur.util;
import java.io.File; import java.io.File;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import static android.graphics.Bitmap.Config.ARGB_8888; import static android.graphics.Bitmap.Config.ARGB_8888;
import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL; import static com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL;
@ -35,6 +36,7 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabColorSchemeParams;
@ -44,6 +46,7 @@ import androidx.core.content.ContextCompat;
import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.color.MaterialColors; import com.google.android.material.color.MaterialColors;
import com.google.android.material.snackbar.Snackbar;
import com.newsblur.NbApplication; import com.newsblur.NbApplication;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.activity.*; import com.newsblur.activity.*;
@ -422,7 +425,7 @@ public class UIUtils {
* upon the provided classifier sub-type map while also setting up handlers to alter said * upon the provided classifier sub-type map while also setting up handlers to alter said
* map if the buttons are pressed. * map if the buttons are pressed.
*/ */
public static void setupIntelDialogRow(final View row, final Map<String,Integer> classifier, final String key) { public static void setupIntelDialogRow(final View row, @NonNull final Map<String,Integer> classifier, final String key) {
colourIntelDialogRow(row, classifier, key); colourIntelDialogRow(row, classifier, key);
row.findViewById(R.id.intel_row_like).setOnClickListener(v -> { row.findViewById(R.id.intel_row_like).setOnClickListener(v -> {
classifier.put(key, Classifier.LIKE); classifier.put(key, Classifier.LIKE);
@ -433,7 +436,11 @@ public class UIUtils {
colourIntelDialogRow(row, classifier, key); colourIntelDialogRow(row, classifier, key);
}); });
row.findViewById(R.id.intel_row_clear).setOnClickListener(v -> { row.findViewById(R.id.intel_row_clear).setOnClickListener(v -> {
classifier.put(key, Classifier.CLEAR_LIKE); if (Objects.equals(classifier.get(key), Classifier.DISLIKE)) {
classifier.put(key, Classifier.CLEAR_DISLIKE);
} else {
classifier.put(key, Classifier.CLEAR_LIKE);
}
colourIntelDialogRow(row, classifier, key); colourIntelDialogRow(row, classifier, key);
}); });
} }
@ -599,4 +606,15 @@ public class UIUtils {
context.sendBroadcast(intent); context.sendBroadcast(intent);
} }
} }
public static void showSnackBar(View view, String message) {
Snackbar.make(view, message, 600).show();
}
public static int[] getLoadingColorsArray(Context context) {
return new int[]{ContextCompat.getColor(context, R.color.refresh_1),
ContextCompat.getColor(context, R.color.refresh_2),
ContextCompat.getColor(context, R.color.refresh_3),
ContextCompat.getColor(context, R.color.refresh_4)};
}
} }

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.newsblur.database.BlurDatabaseHelper import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.util.CursorFilters
import com.newsblur.util.FeedSet import com.newsblur.util.FeedSet
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -21,9 +22,9 @@ class StoriesViewModel
private val _activeStoriesLiveData = MutableLiveData<Cursor>() private val _activeStoriesLiveData = MutableLiveData<Cursor>()
val activeStoriesLiveData: LiveData<Cursor> = _activeStoriesLiveData val activeStoriesLiveData: LiveData<Cursor> = _activeStoriesLiveData
fun getActiveStories(fs: FeedSet) { fun getActiveStories(fs: FeedSet, cursorFilters: CursorFilters) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
dbHelper.getActiveStoriesCursor(fs, cancellationSignal).let { dbHelper.getActiveStoriesCursor(fs, cursorFilters, cancellationSignal).let {
_activeStoriesLiveData.postValue(it) _activeStoriesLiveData.postValue(it)
} }
} }

View file

@ -149,9 +149,10 @@ class WidgetRemoteViewsFactory(context: Context, intent: Intent) : RemoteViewsFa
Log.d(this.javaClass.name, "onDataSetChanged - get remote stories") Log.d(this.javaClass.name, "onDataSetChanged - get remote stories")
val response = apiManager.getStories(fs, 1, StoryOrder.NEWEST, ReadFilter.ALL) val response = apiManager.getStories(fs, 1, StoryOrder.NEWEST, ReadFilter.ALL)
response.stories?.let { response.stories?.let {
val stateFilter = PrefsUtils.getStateFilter(context)
Log.d(this.javaClass.name, "onDataSetChanged - got ${it.size} remote stories") Log.d(this.javaClass.name, "onDataSetChanged - got ${it.size} remote stories")
processStories(response.stories) processStories(response.stories)
dbHelper.insertStories(response, true) dbHelper.insertStories(response, stateFilter, true)
} ?: Log.d(this.javaClass.name, "onDataSetChanged - null remote stories") } ?: Log.d(this.javaClass.name, "onDataSetChanged - null remote stories")
} }
} catch (e: TimeoutCancellationException) { } catch (e: TimeoutCancellationException) {

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/gray55"
android:pathData="M19,18l2,1V3c0,-1.1 -0.9,-2 -2,-2H8.99C7.89,1 7,1.9 7,3h10c1.1,0 2,0.9 2,2v13zM15,5H5c-1.1,0 -2,0.9 -2,2v16l7,-3 7,3V7c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/gray55"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/gray55"
android:pathData="M6.18,17.82m-2.18,0a2.18,2.18 0,1 1,4.36 0a2.18,2.18 0,1 1,-4.36 0" />
<path
android:fillColor="@color/gray55"
android:pathData="M4,4.44v2.83c7.03,0 12.73,5.7 12.73,12.73h2.83c0,-8.59 -6.97,-15.56 -15.56,-15.56zM4,10.1v2.83c3.9,0 7.07,3.17 7.07,7.07h2.83c0,-5.47 -4.43,-9.9 -9.9,-9.9z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/gray55"
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/gray55"
android:pathData="M14,17L4,17v2h10v-2zM20,9L4,9v2h16L20,9zM4,15h16v-2L4,13v2zM4,5v2h16L20,5L4,5z" />
</vector>

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true" > android:animateLayoutChanges="true"
android:descendantFocusability="blocksDescendants">
<RelativeLayout <RelativeLayout
android:id="@+id/main_top_bar" android:id="@+id/main_top_bar"
@ -103,7 +103,7 @@
to be defined first so that other things can be placed above it. --> to be defined first so that other things can be placed above it. -->
<fragment <fragment
android:id="@+id/fragment_feedintelligenceselector" android:id="@+id/fragment_feedintelligenceselector"
android:name="com.newsblur.fragment.FeedIntelligenceSelectorFragment" android:name="com.newsblur.fragment.FeedSelectorFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
@ -175,7 +175,7 @@
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
android:visibility="gone" android:visibility="gone"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
/> android:importantForAutofill="no" />
<!-- The scrollable and pull-able feed list. --> <!-- The scrollable and pull-able feed list. -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout

View file

@ -2,7 +2,8 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants">
<include layout="@layout/toolbar_newsblur" /> <include layout="@layout/toolbar_newsblur" />

View file

@ -41,7 +41,8 @@
android:hint="@string/share_comment_hint" android:hint="@string/share_comment_hint"
android:inputType="textCapSentences|textMultiLine" android:inputType="textCapSentences|textMultiLine"
android:singleLine="false" android:singleLine="false"
android:textSize="15sp" /> android:textSize="15sp"
android:importantForAutofill="no" />
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/container_buttons" android:id="@+id/container_buttons"

View file

@ -6,17 +6,33 @@
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:orientation="vertical"> android:orientation="vertical">
<TextView <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/text_sync_status" android:id="@+id/container_sync_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/status_overlay_background" android:background="@color/status_overlay_background"
android:gravity="center" android:gravity="center"
android:padding="2dp" android:orientation="horizontal"
android:text="@string/sync_status_feed_add" android:paddingVertical="1dp"
android:textColor="@color/status_overlay_text" android:visibility="gone">
android:textSize="14sp"
android:visibility="gone" /> <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/sync_status_feed_add"
android:textColor="@color/status_overlay_text"
android:textSize="14sp" />
<com.google.android.material.progressindicator.CircularProgressIndicator
style="?circleProgressIndicator"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:layout_marginStart="4dp"
android:indeterminate="true" />
</androidx.appcompat.widget.LinearLayoutCompat>
<TextView <TextView
android:id="@+id/text_add_folder_title" android:id="@+id/text_add_folder_title"
@ -45,10 +61,10 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_weight="1" android:layout_weight="1"
android:autofillHints="@null" android:autofillHints="@null"
android:textSize="14sp"
android:hint="@string/new_folder_name_hint" android:hint="@string/new_folder_name_hint"
android:inputType="textCapSentences" android:inputType="textCapSentences"
android:maxLines="1" /> android:maxLines="1"
android:textSize="14sp" />
<ImageView <ImageView
android:id="@+id/ic_create_folder" android:id="@+id/ic_create_folder"

View file

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="25dp"
android:paddingBottom="16dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/feeds_shortcuts"
android:textSize="20sp"
android:textStyle="bold" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/short_open_all_stories"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_open_all_stories_key"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/short_switch_views"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_switch_views_key_left"
android:textSize="18sp"
android:textStyle="bold" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/short_switch_views"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_switch_views_key_right"
android:textSize="18sp"
android:textStyle="bold" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/short_add_site"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_add_site_key"
android:textSize="15sp" />
</FrameLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView>

View file

@ -24,6 +24,7 @@
android:id="@+id/login_username" android:id="@+id/login_username"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="@string/login_username_hint" android:hint="@string/login_username_hint"
android:inputType="textEmailAddress" android:inputType="textEmailAddress"
android:textSize="22sp" /> android:textSize="22sp" />
@ -33,6 +34,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:autofillHints="password"
android:hint="@string/login_password_hint" android:hint="@string/login_password_hint"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:inputType="textPassword" android:inputType="textPassword"
@ -103,7 +105,8 @@
android:hint="@string/login_custom_server_hint" android:hint="@string/login_custom_server_hint"
android:inputType="textNoSuggestions|textMultiLine" android:inputType="textNoSuggestions|textMultiLine"
android:textSize="17sp" android:textSize="17sp"
android:visibility="invisible" /> android:visibility="invisible"
android:importantForAutofill="no" />
<TextView <TextView
android:id="@+id/button_reset_url" android:id="@+id/button_reset_url"
@ -165,6 +168,7 @@
android:id="@+id/registration_username" android:id="@+id/registration_username"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="@string/login_username_hint" android:hint="@string/login_username_hint"
android:inputType="textEmailAddress" android:inputType="textEmailAddress"
android:textSize="22sp"> android:textSize="22sp">
@ -176,6 +180,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:autofillHints="password"
android:hint="@string/login_password_hint" android:hint="@string/login_password_hint"
android:inputType="textPassword" android:inputType="textPassword"
android:nextFocusDown="@+id/registration_email" android:nextFocusDown="@+id/registration_email"
@ -186,6 +191,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:autofillHints="emailAddress"
android:hint="@string/login_registration_email_hint" android:hint="@string/login_registration_email_hint"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:inputType="textEmailAddress" android:inputType="textEmailAddress"

View file

@ -12,6 +12,7 @@
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:singleLine="false" android:singleLine="false"
android:autofillHints="username"
android:inputType="textCapSentences|textMultiLine" /> android:inputType="textCapSentences|textMultiLine" />
</RelativeLayout> </RelativeLayout>

View file

@ -12,6 +12,7 @@
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:singleLine="false" android:singleLine="false"
android:inputType="textCapSentences|textMultiLine" /> android:inputType="textCapSentences|textMultiLine"
android:importantForAutofill="no" />
</RelativeLayout> </RelativeLayout>

View file

@ -13,6 +13,7 @@
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:singleLine="false" android:singleLine="false"
android:inputType="textCapSentences|textMultiLine" android:inputType="textCapSentences|textMultiLine"
android:hint="@string/share_comment_hint"/> android:hint="@string/share_comment_hint"
android:importantForAutofill="no" />
</RelativeLayout> </RelativeLayout>

View file

@ -0,0 +1,397 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="25dp"
android:paddingBottom="16dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/stories_shortcuts"
android:textSize="20sp"
android:textStyle="bold" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_next_story"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_next_story_key_j"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/short_next_story"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_next_story_key_down"
android:textSize="18sp"
android:textStyle="bold" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_previous_story"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_previous_story_key_k"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/short_previous_story"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_previous_story_key_up"
android:textSize="18sp"
android:textStyle="bold" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="7dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/short_text_view"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_text_view_key"
android:textSize="18sp"
android:textStyle="bold" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_page_down"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_page_down_key"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/short_page_up"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_page_up_key"
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_page_up_key"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_next_unread_story"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_next_unread_story_key"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_toggle_read_unread"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_toggle_read_unread_key_u"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_toggle_read_unread"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_toggle_read_unread_key_m"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_save_unsave_story"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_save_unsave_story_key"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_open_in_browser"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_open_in_browser_key_o"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_open_in_browser"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_open_in_browser_key_v"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/short_share_this_story"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_share_story_key"
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_share_this_story_key"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_scroll_to_comments"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_scroll_to_comments_key"
android:textSize="15sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/short_open_story_trainer"
android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatTextView
style="?defaultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/short_open_story_trainer_key"
android:textSize="15sp" />
</FrameLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView>

View file

@ -31,6 +31,11 @@
android:title="@string/menu_newsletters" android:title="@string/menu_newsletters"
app:showAsAction="never" /> app:showAsAction="never" />
<item android:id="@+id/menu_shortcuts"
android:title="@string/menu_shortcuts"
app:showAsAction="never"
android:visible="false" />
<item android:id="@+id/menu_text_size" <item android:id="@+id/menu_text_size"
android:title="@string/menu_text_size" > android:title="@string/menu_text_size" >
<menu> <menu>

View file

@ -22,6 +22,12 @@
app:showAsAction="never" app:showAsAction="never"
android:title="@string/menu_send_story_full"/> android:title="@string/menu_send_story_full"/>
<item
android:id="@+id/menu_shortcuts"
android:title="@string/menu_shortcuts"
android:visible="false"
app:showAsAction="never" />
<item android:id="@+id/menu_text_size" <item android:id="@+id/menu_text_size"
android:title="@string/menu_text_size" > android:title="@string/menu_text_size" >
<menu> <menu>

View file

@ -1,4 +1,4 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="newsblur">NewsBlur</string> <string name="newsblur">NewsBlur</string>
@ -274,6 +274,9 @@
<string name="setup_instructions_details">To read your email newsletters in NewsBlur, forward your newsletters to your custom email address shown above.\n\nIn Gmail, go to Settings > Forwarding and click on Add a forwarding address. Add your custom NewsBlur email address.\n\nGmail will walk you through confirming the email address. You\'ll want to come back to NewsBlur and look for the confirmation email under the "Newsletters" folder.\n\nNext, create a filter with all of your newsletters so that they forward to the custom address on NewsBlur.</string> <string name="setup_instructions_details">To read your email newsletters in NewsBlur, forward your newsletters to your custom email address shown above.\n\nIn Gmail, go to Settings > Forwarding and click on Add a forwarding address. Add your custom NewsBlur email address.\n\nGmail will walk you through confirming the email address. You\'ll want to come back to NewsBlur and look for the confirmation email under the "Newsletters" folder.\n\nNext, create a filter with all of your newsletters so that they forward to the custom address on NewsBlur.</string>
<string name="copy_email">Copy email</string> <string name="copy_email">Copy email</string>
<string name="stories_shortcuts">Stories shortcuts</string>
<string name="feeds_shortcuts">Feeds shortcuts</string>
<string name="import_export">Import/Export…</string> <string name="import_export">Import/Export…</string>
<string name="settings">Preferences…</string> <string name="settings">Preferences…</string>
<string name="menu_mute_sites">Mute Sites…</string> <string name="menu_mute_sites">Mute Sites…</string>
@ -285,6 +288,7 @@
<string name="title_no_subscriptions">No active subscriptions detected</string> <string name="title_no_subscriptions">No active subscriptions detected</string>
<string name="title_widget_loading">Loading…</string> <string name="title_widget_loading">Loading…</string>
<string name="menu_newsletters">Newsletters…</string> <string name="menu_newsletters">Newsletters…</string>
<string name="menu_shortcuts">Shortcuts…</string>
<string name="import_export_title">Import/Export OPML</string> <string name="import_export_title">Import/Export OPML</string>
<string name="notifications_title">Notifications</string> <string name="notifications_title">Notifications</string>
@ -577,7 +581,7 @@
<string name="sync_status_text">Storing text for %s stories…</string> <string name="sync_status_text">Storing text for %s stories…</string>
<string name="sync_status_images">Storing %s images…</string> <string name="sync_status_images">Storing %s images…</string>
<string name="sync_status_offline">Offline</string> <string name="sync_status_offline">Offline</string>
<string name="sync_status_feed_add">Adding feed</string> <string name="sync_status_feed_add">Adding feed</string>
<string name="volume_key_navigation">Volume key navigation…</string> <string name="volume_key_navigation">Volume key navigation…</string>
<string name="off">Off</string> <string name="off">Off</string>
@ -739,4 +743,64 @@
<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="story_saved">Story marked as saved</string>
<string name="story_unsaved">Story marked as unsaved</string>
<string name="story_read">Story marked as read</string>
<string name="story_unread">Story marked as unread</string>
<string name="unread_stories">Unread stories</string>
<string name="focused_stories">Focused stories</string>
<string name="saved_stories">Saved stories</string>
<string name="short_next_story">Next Story</string>
<string name="short_next_story_key_down">\u2193</string>
<string name="short_next_story_key_j">J</string>
<string name="short_previous_story">Previous Story</string>
<string name="short_previous_story_key_up">\u2191</string>
<string name="short_previous_story_key_k">K</string>
<string name="short_text_view">Text View</string>
<string name="short_text_view_key">\u21E7 \u23CE</string>
<string name="short_page_down">Page Down</string>
<string name="short_page_down_key">space</string>
<string name="short_page_up">Page Up</string>
<string name="short_page_up_key">\u21E7 space</string>
<string name="short_next_unread_story">Next Unread Story</string>
<string name="short_next_unread_story_key">N</string>
<string name="short_toggle_read_unread">Toggle Read/Unread</string>
<string name="short_toggle_read_unread_key_u">U</string>
<string name="short_toggle_read_unread_key_m">M</string>
<string name="short_save_unsave_story">Save/Unsave Story</string>
<string name="short_save_unsave_story_key">S</string>
<string name="short_open_in_browser">Open in Browser</string>
<string name="short_open_in_browser_key_o">O</string>
<string name="short_open_in_browser_key_v">V</string>
<string name="short_share_this_story">Share this Story</string>
<string name="short_share_this_story_key">\u21E7 S</string>
<string name="short_scroll_to_comments">Scroll to Comments</string>
<string name="short_scroll_to_comments_key">C</string>
<string name="short_open_story_trainer">Open Story Trainer</string>
<string name="short_open_story_trainer_key">T</string>
<string name="short_open_all_stories">Open All Stories</string>
<string name="short_open_all_stories_key">\u2325 E</string>
<string name="short_switch_views">Switch Views</string>
<string name="short_switch_views_key_left">\u2190</string>
<string name="short_switch_views_key_right">\u2192</string>
<string name="short_add_site">Add Site</string>
<string name="short_add_site_key">\u2325 A</string>
</resources> </resources>

View file

@ -46,6 +46,7 @@
<item name="android:background">@color/bar_background</item> <item name="android:background">@color/bar_background</item>
<item name="android:textColor">@color/gray30</item> <item name="android:textColor">@color/gray30</item>
</style> </style>
<style name="subscriptionHeader.dark"> <style name="subscriptionHeader.dark">
<item name="android:background">@color/dark_bar_background</item> <item name="android:background">@color/dark_bar_background</item>
<item name="android:textColor">@color/gray55</item> <item name="android:textColor">@color/gray55</item>
@ -54,6 +55,7 @@
<style name="subscriptionIcon"> <style name="subscriptionIcon">
<item name="tint">@color/gray30</item> <item name="tint">@color/gray30</item>
</style> </style>
<style name="subscriptionIcon.dark"> <style name="subscriptionIcon.dark">
<item name="tint">@color/gray55</item> <item name="tint">@color/gray55</item>
</style> </style>
@ -540,4 +542,24 @@
<item name="android:layout_height">40dp</item> <item name="android:layout_height">40dp</item>
</style> </style>
<style name="materialSnackBarTextView" parent="@style/Widget.MaterialComponents.Snackbar.TextView">
<item name="android:textStyle">bold</item>
<item name="android:gravity">center</item>
<item name="android:textColor">@color/text</item>
</style>
<style name="materialSnackBarTextView.dark" parent="@style/Widget.MaterialComponents.Snackbar.TextView">
<item name="android:textStyle">bold</item>
<item name="android:gravity">center</item>
<item name="android:textColor">@color/white</item>
</style>
<style name="materialSnackBarTheme" parent="@style/Widget.MaterialComponents.Snackbar">
<item name="android:background">@color/nb_green_gray91</item>
</style>
<style name="materialSnackBarTheme.dark" parent="@style/Widget.MaterialComponents.Snackbar">
<item name="android:background">@color/gray13</item>
</style>
</resources> </resources>

View file

@ -61,6 +61,8 @@
<item name="fontFamily">@font/whitney</item> <item name="fontFamily">@font/whitney</item>
<item name="circleProgressIndicator">@style/circleProgressIndicator</item> <item name="circleProgressIndicator">@style/circleProgressIndicator</item>
<item name="toggleButton">@style/toggleButton</item> <item name="toggleButton">@style/toggleButton</item>
<item name="snackbarStyle">@style/materialSnackBarTheme</item>
<item name="snackbarTextViewStyle">@style/materialSnackBarTextView</item>
</style> </style>
<style name="NewsBlurDarkTheme" parent="Theme.MaterialComponents.NoActionBar"> <style name="NewsBlurDarkTheme" parent="Theme.MaterialComponents.NoActionBar">
@ -124,6 +126,8 @@
<item name="fontFamily">@font/whitney</item> <item name="fontFamily">@font/whitney</item>
<item name="circleProgressIndicator">@style/circleProgressIndicator</item> <item name="circleProgressIndicator">@style/circleProgressIndicator</item>
<item name="toggleButton">@style/toggleButton.dark</item> <item name="toggleButton">@style/toggleButton.dark</item>
<item name="snackbarStyle">@style/materialSnackBarTheme.dark</item>
<item name="snackbarTextViewStyle">@style/materialSnackBarTextView.dark</item>
</style> </style>
<style name="NewsBlurBlackTheme" parent="Theme.MaterialComponents.NoActionBar" > <style name="NewsBlurBlackTheme" parent="Theme.MaterialComponents.NoActionBar" >
@ -187,6 +191,8 @@
<item name="fontFamily">@font/whitney</item> <item name="fontFamily">@font/whitney</item>
<item name="circleProgressIndicator">@style/circleProgressIndicator</item> <item name="circleProgressIndicator">@style/circleProgressIndicator</item>
<item name="toggleButton">@style/toggleButton.dark</item> <item name="toggleButton">@style/toggleButton.dark</item>
<item name="snackbarStyle">@style/materialSnackBarTheme.dark</item>
<item name="snackbarTextViewStyle">@style/materialSnackBarTextView.dark</item>
</style> </style>
<style name="NewsBlurTheme.Translucent" parent="NewsBlurTheme"> <style name="NewsBlurTheme.Translucent" parent="NewsBlurTheme">

View file

@ -0,0 +1,90 @@
package com.newsblur
import android.view.KeyEvent
import com.newsblur.keyboard.KeyboardEvent
import com.newsblur.keyboard.KeyboardListener
import com.newsblur.keyboard.KeyboardManager
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.After
import org.junit.Assert
import org.junit.Test
class HomeKeyboardShortcutsTest {
private val manager = KeyboardManager()
@After
fun afterTest() {
manager.removeListener()
}
@Test
fun openAllStoriesTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isAltPressed } returns true
val handled = manager.onKeyUp(KeyEvent.KEYCODE_E, keyEvent)
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.OpenAllStories) }
}
@Test
fun notOpenAllStoriesTest() {
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isAltPressed } returns false
val handled = manager.onKeyUp(KeyEvent.KEYCODE_E, keyEvent)
Assert.assertFalse(handled)
}
@Test
fun addSiteTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isAltPressed } returns true
val handled = manager.onKeyUp(KeyEvent.KEYCODE_A, keyEvent)
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.AddFeed) }
}
@Test
fun notAddSiteTest() {
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isAltPressed } returns false
val handled = manager.onKeyUp(KeyEvent.KEYCODE_A, keyEvent)
Assert.assertFalse(handled)
}
@Test
fun switchViewLeftTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_DPAD_LEFT, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.SwitchViewLeft) }
}
@Test
fun switchViewRightTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_DPAD_RIGHT, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.SwitchViewRight) }
}
}

View file

@ -0,0 +1,222 @@
package com.newsblur
import android.view.KeyEvent
import com.newsblur.keyboard.KeyboardEvent
import com.newsblur.keyboard.KeyboardListener
import com.newsblur.keyboard.KeyboardManager
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.After
import org.junit.Assert
import org.junit.Test
class ReadingKeyboardShortcutsTest {
private val manager = KeyboardManager()
@After
fun afterTest() {
manager.removeListener()
}
@Test
fun previousStoryTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_J, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.PreviousStory) }
}
@Test
fun previousStoryArrowTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_DPAD_DOWN, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.PreviousStory) }
}
@Test
fun nextStoryTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_K, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.NextStory) }
}
@Test
fun nextStoryArrowTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_DPAD_UP, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.NextStory) }
}
@Test
fun toggleTextViewTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isShiftPressed } returns true
val handled = manager.onKeyUp(KeyEvent.KEYCODE_ENTER, keyEvent)
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.ToggleTextView) }
}
@Test
fun noToggleTextViewTest() {
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isShiftPressed } returns false
val handled = manager.onKeyUp(KeyEvent.KEYCODE_ENTER, keyEvent)
Assert.assertFalse(handled)
}
@Test
fun pageDownTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isShiftPressed } returns false
val handled = manager.onKeyUp(KeyEvent.KEYCODE_SPACE, keyEvent)
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.PageDown) }
}
@Test
fun pageUpTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isShiftPressed } returns true
val handled = manager.onKeyUp(KeyEvent.KEYCODE_SPACE, keyEvent)
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.PageUp) }
}
@Test
fun nextUnreadStoryTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_N, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.NextUnreadStory) }
}
@Test
fun toggleReadUnreadUTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_U, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.ToggleReadUnread) }
}
@Test
fun toggleReadUnreadMTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_M, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.ToggleReadUnread) }
}
@Test
fun saveUnsaveStoryTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isShiftPressed } returns false
val handled = manager.onKeyUp(KeyEvent.KEYCODE_S, keyEvent)
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.SaveUnsaveStory) }
}
@Test
fun shareStoryTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val keyEvent = mockk<KeyEvent>()
every { keyEvent.isShiftPressed } returns true
val handled = manager.onKeyUp(KeyEvent.KEYCODE_S, keyEvent)
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.ShareStory) }
}
@Test
fun openInBrowserOTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_O, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.OpenInBrowser) }
}
@Test
fun openInBrowserVTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_V, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.OpenInBrowser) }
}
@Test
fun scrollToCommentsTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_C, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.ScrollToComments) }
}
@Test
fun openStoryTrainerTest() {
val listener = mockk<KeyboardListener>()
every { listener.onKeyboardEvent(any()) } returns Unit
manager.addListener(listener)
val handled = manager.onKeyUp(KeyEvent.KEYCODE_T, mockk())
Assert.assertTrue(handled)
verify { listener.onKeyboardEvent(KeyboardEvent.OpenStoryTrainer) }
}
}

View file

@ -1,22 +0,0 @@
plugins {
id 'com.android.application' version '7.4.0' apply false
id 'com.android.library' version '7.4.0' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
id 'org.jetbrains.kotlin.kapt' version '1.7.20' apply false
id 'com.google.dagger.hilt.android' version '2.44.2' apply false
id 'com.android.test' version '7.4.0' apply false
}
allprojects {
repositories {
mavenCentral()
maven {
url 'https://maven.google.com'
}
google()
}
}
task clear(type: Delete) {
delete rootProject.buildDir
}

View file

@ -0,0 +1,19 @@
plugins {
id(Plugins.androidApplication) version Version.android apply false
id(Plugins.androidLibrary) version Version.android apply false
kotlin(Plugins.kotlinAndroid) version Version.kotlin apply false
kotlin(Plugins.kotlinKapt) version Version.kotlin apply false
id(Plugins.hiltAndroid) version Version.hilt apply false
id(Plugins.androidTest) version Version.android apply false
}
allprojects {
repositories {
mavenCentral()
google()
}
}
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
}

View file

@ -0,0 +1,9 @@
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
google()
mavenCentral()
}

View file

@ -0,0 +1,14 @@
import org.gradle.api.JavaVersion
object Config {
const val compileSdk = 33
const val minSdk = 23
const val targetSdk = 33
const val versionCode = 213
const val versionName = "13.1.0"
const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner"
val javaVersion = JavaVersion.VERSION_17
}

View file

@ -0,0 +1,15 @@
object Const {
const val namespace = "com.newsblur"
const val namespaceBenchmark = "com.newsblur.benchmark"
const val release = "release"
const val debug = "debug"
const val benchmark = "benchmark"
const val selfInstrumenting = "android.experimental.self-instrumenting"
const val benchmarkProguard = "benchmark-rules.pro"
const val appProguard = "proguard-rules.pro"
const val defaultProguard = "proguard-android.txt"
}

View file

@ -0,0 +1,30 @@
object Dependencies {
const val fragment = "androidx.fragment:fragment-ktx:${Version.fragment}"
const val recyclerView = "androidx.recyclerview:recyclerview:${Version.recyclerView}"
const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:${Version.swipeRefreshLayout}"
const val okHttp = "com.squareup.okhttp3:okhttp:${Version.okHttp}"
const val gson = "com.google.code.gson:gson:${Version.gson}"
const val billing = "com.android.billingclient:billing:${Version.billing}"
const val playCore = "com.google.android.play:core:${Version.playCore}"
const val material = "com.google.android.material:material:${Version.material}"
const val preference = "androidx.preference:preference-ktx:${Version.preference}"
const val browser = "androidx.browser:browser:${Version.browser}"
const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Version.lifecycle}"
const val lifecycleProcess = "androidx.lifecycle:lifecycle-process:${Version.lifecycle}"
const val splashScreen = "androidx.core:core-splashscreen:${Version.splashScreen}"
const val hiltAndroid = "com.google.dagger:hilt-android:${Version.hilt}"
const val hiltCompiler = "com.google.dagger:hilt-compiler:${Version.hilt}"
const val profileInstaller = "androidx.profileinstaller:profileinstaller:${Version.profileInstaller}"
// test
const val junit = "junit:junit:${Version.junit}"
const val mockk = "io.mockk:mockk:${Version.mockk}"
// android test
const val junitExt = "androidx.test.ext:junit:${Version.junitExt}"
const val espressoCore = "androidx.test.espresso:espresso-core:${Version.espresso}"
const val uiAutomator = "androidx.test.uiautomator:uiautomator:${Version.uiAutomator}"
const val benchmarkMacroJunit4 = "androidx.benchmark:benchmark-macro-junit4:${Version.benchmarkMacroJunit4}"
}

View file

@ -0,0 +1,16 @@
object Plugins {
const val kotlinAndroid = "android"
const val kotlinKapt = "kapt"
const val androidApplication = "com.android.application"
const val androidLibrary = "com.android.library"
const val hiltAndroid = "com.google.dagger.hilt.android"
const val androidTest = "com.android.test"
// id("com.android.application") version "8.1.0" apply false
// id("com.android.library") version "8.1.0" apply false
// id("org.jetbrains.kotlin.android") version "1.8.10" apply false
// id("org.jetbrains.kotlin.kapt") version "1.8.10" apply false
// id("com.google.dagger.hilt.android") version "2.44.2" apply false
// id("com.android.test") version "8.1.0" apply false
}

View file

@ -0,0 +1,33 @@
object Version {
const val android = "8.1.0"
const val kotlin = "1.8.10"
const val fragment = "1.6.1"
const val recyclerView = "1.3.1"
const val swipeRefreshLayout = "1.1.0"
const val okHttp = "4.11.0"
const val gson = "2.10"
const val billing = "6.0.1"
const val playCore = "1.10.3"
const val material = "1.9.0"
const val preference = "1.2.0"
const val browser = "1.5.0"
const val lifecycle = "2.6.1"
const val splashScreen = "1.0.1"
const val hilt = "2.44.2"
const val profileInstaller = "1.3.1"
const val junit = "4.13.2"
const val mockk = "1.13.4"
const val junitExt = "1.1.5"
const val espresso = "3.5.1"
const val uiAutomator = "2.2.0"
const val benchmarkMacroJunit4 = "1.1.1"
}

View file

@ -1,3 +1,6 @@
org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M" org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M"
kotlin.code.style=obsolete kotlin.code.style=obsolete
android.useAndroidX=true android.useAndroidX=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=true
android.nonFinalResIds=true

View file

@ -1,6 +1,6 @@
#Thu Oct 27 16:20:00 PDT 2022 #Thu Oct 27 16:20:00 PDT 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View file

@ -6,5 +6,5 @@ pluginManagement {
} }
} }
rootProject.name = "NewsBlur" rootProject.name = "NewsBlur"
include ':app' include("app")
include ':app:benchmark' include("app:benchmark")