mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-08-31 21:41:33 +00:00
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:
commit
25a5929f56
81 changed files with 2090 additions and 708 deletions
2
.github/workflows/android-actions.yml
vendored
2
.github/workflows/android-actions.yml
vendored
|
@ -19,7 +19,7 @@ jobs:
|
|||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
|
||||
- name: Unit Test
|
||||
run: ./gradlew -Pci --console=plain :app:testDebugUnitTest
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
49
clients/android/NewsBlur/app/benchmark/build.gradle.kts
Normal file
49
clients/android/NewsBlur/app/benchmark/build.gradle.kts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
75
clients/android/NewsBlur/app/build.gradle.kts
Normal file
75
clients/android/NewsBlur/app/build.gradle.kts
Normal 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)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# 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
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
@ -41,3 +41,7 @@
|
|||
# can be commented out to help diagnose shrinkage errors.
|
||||
-dontwarn **
|
||||
-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
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.newsblur">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
|
|
@ -101,44 +101,44 @@ abstract public class FeedChooser extends NbActivity {
|
|||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.menu_sort_order_ascending:
|
||||
replaceListOrderFilter(ListOrderFilter.ASCENDING);
|
||||
return true;
|
||||
case R.id.menu_sort_order_descending:
|
||||
replaceListOrderFilter(ListOrderFilter.DESCENDING);
|
||||
return true;
|
||||
case R.id.menu_sort_by_name:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.NAME);
|
||||
return true;
|
||||
case R.id.menu_sort_by_subs:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS);
|
||||
return true;
|
||||
case R.id.menu_sort_by_recent_story:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY);
|
||||
return true;
|
||||
case R.id.menu_sort_by_stories_month:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH);
|
||||
return true;
|
||||
case R.id.menu_sort_by_number_opens:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.OPENS);
|
||||
return true;
|
||||
case R.id.menu_folder_view_nested:
|
||||
replaceFolderView(FolderViewFilter.NESTED);
|
||||
return true;
|
||||
case R.id.menu_folder_view_flat:
|
||||
replaceFolderView(FolderViewFilter.FLAT);
|
||||
return true;
|
||||
case R.id.menu_widget_background_default:
|
||||
setWidgetBackground(WidgetBackground.DEFAULT);
|
||||
return true;
|
||||
case R.id.menu_widget_background_transparent:
|
||||
setWidgetBackground(WidgetBackground.TRANSPARENT);
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_sort_order_ascending) {
|
||||
replaceListOrderFilter(ListOrderFilter.ASCENDING);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_sort_order_descending) {
|
||||
replaceListOrderFilter(ListOrderFilter.DESCENDING);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_sort_by_name) {
|
||||
replaceFeedOrderFilter(FeedOrderFilter.NAME);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_sort_by_subs) {
|
||||
replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_sort_by_recent_story) {
|
||||
replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_sort_by_stories_month) {
|
||||
replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_sort_by_number_opens) {
|
||||
replaceFeedOrderFilter(FeedOrderFilter.OPENS);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_folder_view_nested) {
|
||||
replaceFolderView(FolderViewFilter.NESTED);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_folder_view_flat) {
|
||||
replaceFolderView(FolderViewFilter.FLAT);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_widget_background_default) {
|
||||
setWidgetBackground(WidgetBackground.DEFAULT);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_widget_background_transparent) {
|
||||
setWidgetBackground(WidgetBackground.TRANSPARENT);
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
// 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
|
||||
feedUtils.prepareReadingSession(fs, false);
|
||||
feedUtils.prepareReadingSession(this, fs, false);
|
||||
if (getIntent().getBooleanExtra(EXTRA_WIDGET_STORY, false)) {
|
||||
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
|
||||
UIUtils.startReadingActivity(fs, hash, this);
|
||||
|
@ -206,7 +206,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe
|
|||
if (session != null) {
|
||||
// set the next session on the parent activity
|
||||
fs = session.getFeedSet();
|
||||
feedUtils.prepareReadingSession(fs, false);
|
||||
feedUtils.prepareReadingSession(this, fs, false);
|
||||
triggerSync();
|
||||
|
||||
// set the next session on the child activity
|
||||
|
@ -248,7 +248,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe
|
|||
String oldQuery = fs.getSearchQuery();
|
||||
fs.setSearchQuery(q);
|
||||
if (!TextUtils.equals(q, oldQuery)) {
|
||||
feedUtils.prepareReadingSession(fs, true);
|
||||
feedUtils.prepareReadingSession(this, fs, true);
|
||||
triggerSync();
|
||||
itemSetFragment.resetEmptyState();
|
||||
itemSetFragment.hasUpdated();
|
||||
|
@ -278,7 +278,7 @@ public abstract class ItemsList extends NbActivity implements ReadingActionListe
|
|||
|
||||
protected void restartReadingSession() {
|
||||
NBSyncService.resetFetchState(fs);
|
||||
feedUtils.prepareReadingSession(fs, true);
|
||||
feedUtils.prepareReadingSession(this, fs, true);
|
||||
triggerSync();
|
||||
itemSetFragment.resetEmptyState();
|
||||
itemSetFragment.hasUpdated();
|
||||
|
|
|
@ -10,10 +10,6 @@ import android.graphics.Bitmap;
|
|||
import android.os.Bundle;
|
||||
import android.os.Trace;
|
||||
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.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
|
@ -22,13 +18,23 @@ import android.view.View;
|
|||
import android.view.View.OnKeyListener;
|
||||
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.database.BlurDatabaseHelper;
|
||||
import com.newsblur.databinding.ActivityMainBinding;
|
||||
import com.newsblur.delegate.MainContextMenuDelegate;
|
||||
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.keyboard.KeyboardEvent;
|
||||
import com.newsblur.keyboard.KeyboardListener;
|
||||
import com.newsblur.keyboard.KeyboardManager;
|
||||
import com.newsblur.service.BootReceiver;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.AppConstants;
|
||||
|
@ -45,7 +51,7 @@ import javax.inject.Inject;
|
|||
import dagger.hilt.android.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
|
||||
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";
|
||||
|
||||
private FolderListFragment folderFeedList;
|
||||
private FolderListFragment folderFeedList;
|
||||
private FeedSelectorFragment feedSelectorFragment;
|
||||
private boolean wasSwipeEnabled = false;
|
||||
private ActivityMainBinding binding;
|
||||
private MainContextMenuDelegate contextMenuDelegate;
|
||||
private KeyboardManager keyboardManager;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -69,7 +77,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
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
|
||||
// that something is displayed while the service warms up
|
||||
|
@ -82,7 +91,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
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
|
||||
BootReceiver.scheduleSyncService(this);
|
||||
|
@ -127,12 +137,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
// Check whether it's a shortcut intent
|
||||
String shortcutExtra = getIntent().getStringExtra(ShortcutUtils.SHORTCUT_EXTRA);
|
||||
if (shortcutExtra != null && shortcutExtra.startsWith(ShortcutUtils.SHORTCUT_ALL_STORIES)) {
|
||||
Intent intent = new Intent(this, AllStoriesItemsList.class);
|
||||
intent.putExtra(ItemsList.EXTRA_FEED_SET, FeedSet.allFeeds());
|
||||
if (shortcutExtra.equals(ShortcutUtils.SHORTCUT_ALL_STORIES_SEARCH)) {
|
||||
intent.putExtra(ItemsList.EXTRA_VISIBLE_SEARCH, true);
|
||||
}
|
||||
startActivity(intent);
|
||||
boolean isAllStoriesSearch = shortcutExtra.equals(ShortcutUtils.SHORTCUT_ALL_STORIES_SEARCH);
|
||||
openAllStories(isAllStoriesSearch);
|
||||
}
|
||||
|
||||
Trace.endSection();
|
||||
|
@ -177,10 +183,17 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
updateStatusIndicators();
|
||||
folderFeedList.pushUnreadCounts();
|
||||
folderFeedList.checkOpenFolderPreferences();
|
||||
keyboardManager.addListener(this);
|
||||
triggerSync();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
protected void onPause() {
|
||||
keyboardManager.removeListener();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedState(StateFilter state) {
|
||||
if ( !( (state == StateFilter.ALL) ||
|
||||
(state == StateFilter.SOME) ||
|
||||
|
@ -211,7 +224,27 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
if ((updateType & UPDATE_METADATA) != 0) {
|
||||
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) {
|
||||
binding.mainUnreadCountNeutText.setText(Integer.toString(neutCount));
|
||||
|
@ -325,4 +358,58 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,15 +59,14 @@ public class MuteConfig extends FeedChooser implements MuteConfigAdapter.FeedSta
|
|||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_mute_all:
|
||||
setFeedsState(true);
|
||||
return true;
|
||||
case R.id.menu_mute_none:
|
||||
setFeedsState(false);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.menu_mute_all) {
|
||||
setFeedsState(true);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_mute_none) {
|
||||
setFeedsState(false);
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.newsblur.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.database.Cursor
|
||||
|
@ -22,7 +23,11 @@ import com.newsblur.databinding.ActivityReadingBinding
|
|||
import com.newsblur.di.IconLoader
|
||||
import com.newsblur.domain.Story
|
||||
import com.newsblur.fragment.ReadingItemFragment
|
||||
import com.newsblur.fragment.ReadingItemFragment.Companion.VERTICAL_SCROLL_DISTANCE_DP
|
||||
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_STATUS
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
|
||||
|
@ -42,7 +47,7 @@ import javax.inject.Inject
|
|||
import kotlin.math.abs
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListener {
|
||||
abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListener, KeyboardListener {
|
||||
|
||||
@Inject
|
||||
lateinit var feedUtils: FeedUtils
|
||||
|
@ -84,6 +89,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
|
|||
private var isMultiWindowModeHack = false
|
||||
|
||||
private val pageHistory = mutableListOf<Story>()
|
||||
private val keyboardManager = KeyboardManager()
|
||||
|
||||
private lateinit var volumeKeyNavigation: VolumeKeyNavigation
|
||||
private lateinit var intelState: StateFilter
|
||||
|
@ -136,7 +142,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
|
|||
setupViews()
|
||||
setupListeners()
|
||||
setupObservers()
|
||||
getActiveStoriesCursor(true)
|
||||
getActiveStoriesCursor(this, true)
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
feedUtils.prepareReadingSession(fs, false)
|
||||
feedUtils.prepareReadingSession(this, fs, false)
|
||||
keyboardManager.addListener(this)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
keyboardManager.removeListener()
|
||||
if (isMultiWindowModeHack) {
|
||||
isMultiWindowModeHack = false
|
||||
} 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 {
|
||||
storiesViewModel.getActiveStories(it)
|
||||
val cursorFilters = CursorFilters(context, it)
|
||||
storiesViewModel.getActiveStories(it, cursorFilters)
|
||||
} ?: run {
|
||||
if (finishOnInvalidFs) {
|
||||
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) {
|
||||
getActiveStoriesCursor()
|
||||
getActiveStoriesCursor(this)
|
||||
updateOverlayNav()
|
||||
}
|
||||
|
||||
|
@ -737,6 +746,10 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
|
|||
return if (isVolumeKeyNavigationEvent(keyCode)) {
|
||||
processVolumeKeyNavigationEvent(keyCode)
|
||||
true
|
||||
} else if (KeyboardManager.hasHardwareKeyboard(this)) {
|
||||
val isKnownKeyCode = keyboardManager.isKnownKeyCode(keyCode)
|
||||
if (isKnownKeyCode) true
|
||||
else super.onKeyDown(keyCode, event)
|
||||
} else {
|
||||
super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
@ -748,24 +761,32 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
|
|||
private fun processVolumeKeyNavigationEvent(keyCode: Int) {
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && volumeKeyNavigation == VolumeKeyNavigation.DOWN_NEXT ||
|
||||
keyCode == KeyEvent.KEYCODE_VOLUME_UP && volumeKeyNavigation == VolumeKeyNavigation.UP_NEXT) {
|
||||
if (pager == null) return
|
||||
val nextPosition = pager!!.currentItem + 1
|
||||
if (nextPosition < readingAdapter!!.count) {
|
||||
try {
|
||||
pager!!.currentItem = nextPosition
|
||||
} catch (e: Exception) {
|
||||
// Just in case cursor changes.
|
||||
}
|
||||
}
|
||||
nextStory()
|
||||
} else {
|
||||
if (pager == null) return
|
||||
val nextPosition = pager!!.currentItem - 1
|
||||
if (nextPosition >= 0) {
|
||||
try {
|
||||
pager!!.currentItem = nextPosition
|
||||
} catch (e: Exception) {
|
||||
// Just in case cursor changes.
|
||||
}
|
||||
previousStory()
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextStory() {
|
||||
if (pager == null) return
|
||||
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
|
||||
return if (isVolumeKeyNavigationEvent(keyCode)) {
|
||||
true
|
||||
} else if (KeyboardManager.hasHardwareKeyboard(this)) {
|
||||
val handledKeyCode = keyboardManager.onKeyUp(keyCode, event)
|
||||
if (handledKeyCode) true
|
||||
else super.onKeyUp(keyCode, event)
|
||||
} else {
|
||||
super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
@ -797,6 +822,27 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
|
|||
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 {
|
||||
const val EXTRA_FEEDSET = "feed_set"
|
||||
const val EXTRA_STORY_HASH = "story_hash"
|
||||
|
|
|
@ -62,15 +62,14 @@ public class WidgetConfig extends FeedChooser {
|
|||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_select_all:
|
||||
selectAllFeeds();
|
||||
return true;
|
||||
case R.id.menu_select_none:
|
||||
replaceWidgetFeedIds(Collections.emptySet());
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.menu_select_all) {
|
||||
selectAllFeeds();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_select_none) {
|
||||
replaceWidgetFeedIds(Collections.emptySet());
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,13 +24,12 @@ import com.newsblur.domain.UserProfile;
|
|||
import com.newsblur.network.domain.CommentResponse;
|
||||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.CursorFilters;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadingAction;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
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
|
||||
public final static Object RW_MUTEX = new Object();
|
||||
|
||||
private Context context;
|
||||
private final BlurDatabase dbWrapper;
|
||||
private SQLiteDatabase dbRO;
|
||||
private SQLiteDatabase dbRW;
|
||||
private final SQLiteDatabase dbRO;
|
||||
private final SQLiteDatabase dbRW;
|
||||
|
||||
public BlurDatabaseHelper(Context context) {
|
||||
com.newsblur.util.Log.d(this.getClass().getName(), "new DB conn requested");
|
||||
this.context = context;
|
||||
synchronized (RW_MUTEX) {
|
||||
dbWrapper = new BlurDatabase(context);
|
||||
dbRO = dbWrapper.getRO();
|
||||
|
@ -328,8 +325,7 @@ public class BlurDatabaseHelper {
|
|||
return urls;
|
||||
}
|
||||
|
||||
public void insertStories(StoriesResponse apiResponse, boolean forImmediateReading) {
|
||||
StateFilter intelState = PrefsUtils.getStateFilter(context);
|
||||
public void insertStories(StoriesResponse apiResponse, StateFilter stateFilter, boolean forImmediateReading) {
|
||||
synchronized (RW_MUTEX) {
|
||||
// do not attempt to use beginTransactionNonExclusive() to reduce lock time for this very heavy set
|
||||
// of calls. most versions of Android incorrectly implement the underlying SQLite calls and will
|
||||
|
@ -369,7 +365,7 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
insertSingleStoryExtSync(story);
|
||||
// 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();
|
||||
sessionHashValues.put(DatabaseConstants.READING_SESSION_STORY_HASH, story.storyHash);
|
||||
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
|
||||
* 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) {
|
||||
com.newsblur.util.Log.e(this, "updateStory called on response with missing single story");
|
||||
return;
|
||||
|
@ -500,7 +496,7 @@ public class BlurDatabaseHelper {
|
|||
apiResponse.story.starredTimestamp = oldStory.starredTimestamp;
|
||||
apiResponse.story.read = oldStory.read;
|
||||
}
|
||||
insertStories(apiResponse, forImmediateReading);
|
||||
insertStories(apiResponse, stateFilter, forImmediateReading);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -754,8 +750,8 @@ public class BlurDatabaseHelper {
|
|||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.STORY_READ, true);
|
||||
String rangeSelection = null;
|
||||
if (olderThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " <= " + olderThan.toString();
|
||||
if (newerThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " >= " + newerThan.toString();
|
||||
if (olderThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " <= " + olderThan;
|
||||
if (newerThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " >= " + newerThan;
|
||||
StringBuilder feedSelection = null;
|
||||
if (fs.isAllNormal()) {
|
||||
// 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
|
||||
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE,
|
||||
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)), ",");
|
||||
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
|
||||
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) {
|
||||
newIds.add(currentUser);
|
||||
newIds.add(currentUserId);
|
||||
} else {
|
||||
newIds.remove(currentUser);
|
||||
newIds.remove(currentUserId);
|
||||
}
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.STORY_SHARED_USER_IDS, TextUtils.join(",", newIds));
|
||||
|
@ -1146,16 +1140,16 @@ public class BlurDatabaseHelper {
|
|||
return rawQuery(q.toString(), null, cancellationSignal);
|
||||
}
|
||||
|
||||
public Cursor getActiveStoriesCursor(FeedSet fs, CancellationSignal cancellationSignal) {
|
||||
final StoryOrder order = PrefsUtils.getStoryOrder(context, fs);
|
||||
public Cursor getActiveStoriesCursor(FeedSet fs, CursorFilters cursorFilters, CancellationSignal cancellationSignal) {
|
||||
// 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
|
||||
// are offline, but if a session is started, just use what was there so offsets don't change.
|
||||
if (result.getCount() < 1) {
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "priming reading session");
|
||||
prepareReadingSession(fs);
|
||||
result = getActiveStoriesCursorNoPrep(fs, order, cancellationSignal);
|
||||
prepareReadingSession(fs, cursorFilters.getStateFilter(), cursorFilters.getReadFilter());
|
||||
|
||||
result = getActiveStoriesCursorNoPrep(fs, cursorFilters.getStoryOrder(), cancellationSignal);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -1183,18 +1177,12 @@ public class BlurDatabaseHelper {
|
|||
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
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
StringBuilder sel = new StringBuilder();
|
||||
// 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
|
||||
* an ID at which time the placeholder will be removed.
|
||||
*/
|
||||
public void insertCommentPlaceholder(String storyId, String feedId, String commentText) {
|
||||
String userId = PrefsUtils.getUserId(context);
|
||||
public void insertCommentPlaceholder(String storyId, @Nullable String userId, String commentText) {
|
||||
Comment comment = new Comment();
|
||||
comment.isPlaceholder = true;
|
||||
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});}
|
||||
}
|
||||
|
||||
public void clearSelfComments(String storyId) {
|
||||
String userId = PrefsUtils.getUserId(context);
|
||||
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.COMMENT_TABLE,
|
||||
public void clearSelfComments(String storyId, @Nullable String userId) {
|
||||
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.COMMENT_TABLE,
|
||||
DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?",
|
||||
new String[]{storyId, userId});}
|
||||
}
|
||||
|
||||
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
|
||||
Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE,
|
||||
null,
|
||||
DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?",
|
||||
new String[]{storyId, userId},
|
||||
new String[]{storyId, commentUserId},
|
||||
null, null, null);
|
||||
if ((c == null)||(c.getCount() < 1)) {
|
||||
Log.w(this.getClass().getName(), "comment removed before finishing mark-liked");
|
||||
|
@ -1422,15 +1408,13 @@ public class BlurDatabaseHelper {
|
|||
Comment comment = Comment.fromCursor(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
|
||||
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) {
|
||||
newIds.add(currentUser);
|
||||
newIds.add(currentUserId);
|
||||
} else {
|
||||
newIds.remove(currentUser);
|
||||
newIds.remove(currentUserId);
|
||||
}
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.COMMENT_LIKING_USERS, TextUtils.join(",", newIds));
|
||||
|
@ -1458,7 +1442,7 @@ public class BlurDatabaseHelper {
|
|||
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
|
||||
Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE,
|
||||
null,
|
||||
|
@ -1477,7 +1461,7 @@ public class BlurDatabaseHelper {
|
|||
Reply reply = new Reply();
|
||||
reply.commentId = comment.id;
|
||||
reply.text = replyText;
|
||||
reply.userId = PrefsUtils.getUserId(context);
|
||||
reply.userId = userId;
|
||||
reply.date = new Date();
|
||||
reply.id = Reply.PLACEHOLDER_COMMENT_ID + storyId + comment.id + reply.userId;
|
||||
synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE);}
|
||||
|
@ -1582,11 +1566,8 @@ public class BlurDatabaseHelper {
|
|||
|
||||
public static void closeQuietly(Cursor c) {
|
||||
if (c == null) return;
|
||||
try {c.close();} catch (Exception e) {;}
|
||||
}
|
||||
|
||||
public void sendSyncUpdate(int updateType) {
|
||||
UIUtils.syncUpdateStatus(context, updateType);
|
||||
try {c.close();} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
private static String conjoinSelections(CharSequence... args) {
|
||||
|
|
|
@ -413,52 +413,42 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_mark_story_as_read:
|
||||
if (item.getItemId() == R.id.menu_mark_story_as_read) {
|
||||
feedUtils.markStoryAsRead(story, context);
|
||||
return true;
|
||||
|
||||
case R.id.menu_mark_story_as_unread:
|
||||
} else if (item.getItemId() == R.id.menu_mark_story_as_unread) {
|
||||
feedUtils.markStoryUnread(story, context);
|
||||
return true;
|
||||
|
||||
case R.id.menu_mark_older_stories_as_read:
|
||||
} else if (item.getItemId() == R.id.menu_mark_older_stories_as_read) {
|
||||
feedUtils.markRead(context, fs, story.timestamp, null, R.array.mark_older_read_options);
|
||||
return true;
|
||||
|
||||
case R.id.menu_mark_newer_stories_as_read:
|
||||
} else if (item.getItemId() == R.id.menu_mark_newer_stories_as_read) {
|
||||
feedUtils.markRead(context, fs, null, story.timestamp, R.array.mark_newer_read_options);
|
||||
return true;
|
||||
|
||||
case R.id.menu_send_story:
|
||||
} else if (item.getItemId() == R.id.menu_send_story) {
|
||||
feedUtils.sendStoryUrl(story, context);
|
||||
return true;
|
||||
|
||||
case R.id.menu_send_story_full:
|
||||
} else if (item.getItemId() == R.id.menu_send_story_full) {
|
||||
feedUtils.sendStoryFull(story, context);
|
||||
return true;
|
||||
|
||||
case R.id.menu_save_story:
|
||||
} else if (item.getItemId() == R.id.menu_save_story) {
|
||||
//TODO get folder name
|
||||
feedUtils.setStorySaved(story, true, context, null);
|
||||
return true;
|
||||
|
||||
case R.id.menu_unsave_story:
|
||||
} else if (item.getItemId() == R.id.menu_unsave_story) {
|
||||
feedUtils.setStorySaved(story, false, context, null);
|
||||
return true;
|
||||
|
||||
case R.id.menu_intel:
|
||||
} else if (item.getItemId() == R.id.menu_intel) {
|
||||
if (story.feedId.equals("0")) return true; // cannot train on feedless stories
|
||||
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
|
||||
intelFrag.show(context.getSupportFragmentManager(), StoryIntelTrainerFragment.class.getName());
|
||||
return true;
|
||||
|
||||
case R.id.menu_go_to_feed:
|
||||
} else if (item.getItemId() == R.id.menu_go_to_feed) {
|
||||
FeedSet fs = FeedSet.singleFeed(story.feedId);
|
||||
FeedItemsList.startActivity(context, fs,
|
||||
feedUtils.getFeed(story.feedId), null, null);
|
||||
return true;
|
||||
default:
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -303,7 +303,7 @@ open class ItemListContextMenuDelegateImpl(
|
|||
|
||||
private fun restartReadingSession(fragment: ItemSetFragment, fs: FeedSet) {
|
||||
NBSyncService.resetFetchState(fs)
|
||||
feedUtils.prepareReadingSession(fs, true)
|
||||
feedUtils.prepareReadingSession(activity, fs, true)
|
||||
triggerSync(activity)
|
||||
fragment.resetEmptyState()
|
||||
fragment.hasUpdated()
|
||||
|
|
|
@ -10,10 +10,8 @@ import androidx.fragment.app.DialogFragment
|
|||
import com.newsblur.R
|
||||
import com.newsblur.activity.*
|
||||
import com.newsblur.database.BlurDatabaseHelper
|
||||
import com.newsblur.fragment.FolderListFragment
|
||||
import com.newsblur.fragment.LoginAsDialogFragment
|
||||
import com.newsblur.fragment.LogoutDialogFragment
|
||||
import com.newsblur.fragment.NewslettersFragment
|
||||
import com.newsblur.fragment.*
|
||||
import com.newsblur.keyboard.KeyboardManager
|
||||
import com.newsblur.service.NBSyncService
|
||||
import com.newsblur.util.ListTextSize
|
||||
import com.newsblur.util.ListTextSize.Companion.fromSize
|
||||
|
@ -44,6 +42,10 @@ class MainContextMenuDelegateImpl(
|
|||
menu.findItem(R.id.menu_loginas).isVisible = true
|
||||
}
|
||||
|
||||
if (KeyboardManager.hasHardwareKeyboard(activity)) {
|
||||
menu.findItem(R.id.menu_shortcuts).isVisible = true
|
||||
}
|
||||
|
||||
when (PrefsUtils.getSelectedTheme(activity)) {
|
||||
ThemeValue.LIGHT -> menu.findItem(R.id.menu_theme_light).isChecked = true
|
||||
ThemeValue.DARK -> menu.findItem(R.id.menu_theme_dark).isChecked = true
|
||||
|
@ -184,10 +186,15 @@ class MainContextMenuDelegateImpl(
|
|||
true
|
||||
}
|
||||
R.id.menu_newsletters -> {
|
||||
val newFragment: DialogFragment = NewslettersFragment()
|
||||
val newFragment = NewslettersFragment()
|
||||
newFragment.show(activity.supportFragmentManager, NewslettersFragment::class.java.name)
|
||||
true
|
||||
}
|
||||
R.id.menu_shortcuts -> {
|
||||
val newFragment = FeedsShortcutFragment()
|
||||
newFragment.show(activity.supportFragmentManager, FeedsShortcutFragment::class.java.name)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
|
@ -238,7 +238,7 @@ public class Story implements Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean isStoryVisibileInState(StateFilter state) {
|
||||
public boolean isStoryVisibleInState(StateFilter state) {
|
||||
int score = intelligence.calcTotalIntel();
|
||||
switch (state) {
|
||||
case ALL:
|
||||
|
@ -252,7 +252,7 @@ public class Story implements Serializable {
|
|||
case NEG:
|
||||
return (score < 0);
|
||||
case SAVED:
|
||||
return (starred == true);
|
||||
return (starred);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -24,10 +24,9 @@ import com.newsblur.fragment.AddFeedFragment.AddFeedAdapter.FolderViewHolder
|
|||
import com.newsblur.network.APIManager
|
||||
import com.newsblur.service.NBSyncService
|
||||
import com.newsblur.util.AppConstants
|
||||
import com.newsblur.util.UIUtils
|
||||
import com.newsblur.util.executeAsyncTask
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -88,14 +87,14 @@ class AddFeedFragment : DialogFragment() {
|
|||
binding.inputFolderName.text.clear()
|
||||
addFeed(activity, apiManager, folderName)
|
||||
} 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?) {
|
||||
binding.textSyncStatus.visibility = View.VISIBLE
|
||||
binding.containerSyncStatus.visibility = View.VISIBLE
|
||||
lifecycleScope.executeAsyncTask(
|
||||
doInBackground = {
|
||||
(activity as AddFeedProgressListener).addFeedStarted()
|
||||
|
@ -103,7 +102,7 @@ class AddFeedFragment : DialogFragment() {
|
|||
apiManager.addFeed(feedUrl, folderName)
|
||||
},
|
||||
onPostExecute = {
|
||||
binding.textSyncStatus.visibility = View.GONE
|
||||
binding.containerSyncStatus.visibility = View.GONE
|
||||
val intent = Intent(activity, Main::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
if (!it.isError) {
|
||||
|
@ -111,7 +110,7 @@ class AddFeedFragment : DialogFragment() {
|
|||
NBSyncService.forceFeedsFolders()
|
||||
intent.putExtra(Main.EXTRA_FORCE_SHOW_FEED_ID, it.feed.feedId)
|
||||
} 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.finish()
|
||||
|
@ -119,8 +118,7 @@ class AddFeedFragment : DialogFragment() {
|
|||
)
|
||||
}
|
||||
|
||||
private class AddFeedAdapter
|
||||
constructor(private val listener: OnFolderClickListener) : RecyclerView.Adapter<FolderViewHolder>() {
|
||||
private class AddFeedAdapter(private val listener: OnFolderClickListener) : RecyclerView.Adapter<FolderViewHolder>() {
|
||||
|
||||
private val folders: MutableList<Folder> = ArrayList()
|
||||
|
||||
|
@ -145,7 +143,7 @@ class AddFeedFragment : DialogFragment() {
|
|||
Collections.sort(folders, Folder.FolderComparator)
|
||||
this.folders.clear()
|
||||
this.folders.addAll(folders)
|
||||
notifyDataSetChanged()
|
||||
this.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
|
|
@ -11,10 +11,10 @@ import com.newsblur.view.StateToggleButton;
|
|||
import com.newsblur.view.StateToggleButton.StateChangedListener;
|
||||
import com.newsblur.util.StateFilter;
|
||||
|
||||
public class FeedIntelligenceSelectorFragment extends Fragment implements StateChangedListener {
|
||||
|
||||
public class FeedSelectorFragment extends Fragment implements StateChangedListener {
|
||||
|
||||
private StateToggleButton button;
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View v = inflater.inflate(R.layout.fragment_intelligenceselector, null);
|
||||
|
@ -27,7 +27,7 @@ public class FeedIntelligenceSelectorFragment extends Fragment implements StateC
|
|||
public void changedState(StateFilter state) {
|
||||
((StateChangedListener) getActivity()).changedState(state);
|
||||
}
|
||||
|
||||
|
||||
public void setState(StateFilter state) {
|
||||
button.setState(state);
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import android.os.Parcelable;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
@ -31,6 +30,7 @@ import com.newsblur.di.IconLoader;
|
|||
import com.newsblur.di.ThumbnailLoader;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.CursorFilters;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
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
|
||||
boolean isDisableAnimations = ViewUtils.isPowerSaveMode(requireContext());
|
||||
|
||||
int[] colorsArray = {ContextCompat.getColor(requireContext(), R.color.refresh_1),
|
||||
ContextCompat.getColor(requireContext(), R.color.refresh_2),
|
||||
ContextCompat.getColor(requireContext(), R.color.refresh_3),
|
||||
ContextCompat.getColor(requireContext(), R.color.refresh_4)};
|
||||
int[] colorsArray = UIUtils.getLoadingColorsArray(requireContext());
|
||||
binding.topLoadingThrob.setEnabled(!isDisableAnimations);
|
||||
binding.topLoadingThrob.setColors(colorsArray);
|
||||
|
||||
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.setColors(colorsArray);
|
||||
|
||||
|
@ -271,7 +268,7 @@ public class ItemSetFragment extends NbFragment {
|
|||
public void hasUpdated() {
|
||||
FeedSet fs = getFeedSet();
|
||||
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
|
||||
if (itemGridWidthPx > 0) {
|
||||
int itemGridWidthDp = Math.round(UIUtils.px2dp(getActivity(), itemGridWidthPx));
|
||||
int itemGridWidthDp = Math.round(UIUtils.px2dp(requireContext(), itemGridWidthPx));
|
||||
colsCoarse = itemGridWidthDp / 300;
|
||||
colsMed = itemGridWidthDp / 200;
|
||||
colsFine = itemGridWidthDp / 150;
|
||||
|
@ -416,7 +413,7 @@ public class ItemSetFragment extends NbFragment {
|
|||
if (listStyle == StoryListStyle.LIST) {
|
||||
gridSpacingPx = 0;
|
||||
} 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();
|
||||
anim.setAddDuration((long) ((anim.getAddDuration() + targetAddDuration)/2L));
|
||||
anim.setMoveDuration((long) ((anim.getMoveDuration() + targetMovDuration)/2L));
|
||||
anim.setAddDuration((anim.getAddDuration() + targetAddDuration)/2L);
|
||||
anim.setMoveDuration((anim.getMoveDuration() + targetMovDuration)/2L);
|
||||
}
|
||||
|
||||
private void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
|
|
|
@ -15,8 +15,7 @@ import com.newsblur.util.setViewVisible
|
|||
class NewslettersFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val view = layoutInflater.inflate(R.layout.newsletter_dialog, null)
|
||||
val binding: NewsletterDialogBinding = NewsletterDialogBinding.bind(view)
|
||||
val binding = NewsletterDialogBinding.inflate(layoutInflater)
|
||||
val emailAddress = generateEmail()
|
||||
|
||||
binding.txtEmail.text = emailAddress
|
||||
|
|
|
@ -47,10 +47,8 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
|
|||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_profileactivity, null)
|
||||
binding = FragmentProfileactivityBinding.bind(view)
|
||||
val colorsArray = intArrayOf(ContextCompat.getColor(requireContext(), R.color.refresh_1),
|
||||
ContextCompat.getColor(requireContext(), R.color.refresh_2),
|
||||
ContextCompat.getColor(requireContext(), R.color.refresh_3),
|
||||
ContextCompat.getColor(requireContext(), R.color.refresh_4))
|
||||
val colorsArray = UIUtils.getLoadingColorsArray(requireContext())
|
||||
|
||||
binding.emptyViewLoadingThrob.setColors(*colorsArray)
|
||||
binding.profileDetailsActivitylist.setFooterDividersEnabled(false)
|
||||
binding.profileDetailsActivitylist.emptyView = binding.emptyView
|
||||
|
|
|
@ -15,6 +15,7 @@ import android.webkit.WebView.HitTestResult
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.Story
|
||||
import com.newsblur.domain.UserDetails
|
||||
import com.newsblur.keyboard.KeyboardManager
|
||||
import com.newsblur.network.APIManager
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_INTEL
|
||||
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_SOCIAL
|
||||
|
@ -198,10 +200,10 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.storyContextMenuButton.setOnClickListener { onClickMenuButton() }
|
||||
readingItemActionsBinding.markReadStoryButton.setOnClickListener { clickMarkStoryRead() }
|
||||
readingItemActionsBinding.trainStoryButton.setOnClickListener { clickTrain() }
|
||||
readingItemActionsBinding.saveStoryButton.setOnClickListener { clickSave() }
|
||||
readingItemActionsBinding.shareStoryButton.setOnClickListener { clickShare() }
|
||||
readingItemActionsBinding.markReadStoryButton.setOnClickListener { switchMarkStoryReadState() }
|
||||
readingItemActionsBinding.trainStoryButton.setOnClickListener { openStoryTrainer() }
|
||||
readingItemActionsBinding.saveStoryButton.setOnClickListener { switchStorySavedState() }
|
||||
readingItemActionsBinding.shareStoryButton.setOnClickListener { openShareDialog() }
|
||||
}
|
||||
|
||||
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)
|
||||
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())) {
|
||||
ThemeValue.LIGHT -> menu.findItem(R.id.menu_theme_light).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) {
|
||||
R.id.menu_reading_original -> {
|
||||
val uri = Uri.parse(story!!.permalink)
|
||||
UIUtils.handleUri(requireContext(), uri)
|
||||
openBrowser()
|
||||
true
|
||||
}
|
||||
R.id.menu_reading_sharenewsblur -> {
|
||||
|
@ -317,6 +322,10 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
|
|||
feedUtils.sendStoryFull(story, requireContext())
|
||||
true
|
||||
}
|
||||
R.id.menu_shortcuts -> {
|
||||
showStoryShortcuts()
|
||||
true
|
||||
}
|
||||
R.id.menu_text_size_xs -> {
|
||||
setTextSizeStyle(ReadingTextSize.XS)
|
||||
true
|
||||
|
@ -408,7 +417,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
|
|||
R.id.menu_intel -> {
|
||||
// check against training on feedless stories
|
||||
if (story!!.feedId != "0") {
|
||||
clickTrain()
|
||||
openStoryTrainer()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
@ -421,9 +430,19 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
|
|||
}
|
||||
}
|
||||
|
||||
private fun clickMarkStoryRead() {
|
||||
if (story!!.read) feedUtils.markStoryUnread(story!!, requireContext())
|
||||
else feedUtils.markStoryAsRead(story!!, requireContext())
|
||||
fun switchMarkStoryReadState(notifyUser: Boolean = false) {
|
||||
story?.let {
|
||||
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() {
|
||||
|
@ -437,7 +456,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
|
|||
sampledQueue?.add { updateStoryReadTitleState.invoke() } ?: updateStoryReadTitleState.invoke()
|
||||
}
|
||||
|
||||
private fun clickTrain() {
|
||||
fun openStoryTrainer() {
|
||||
val intelFrag = StoryIntelTrainerFragment.newInstance(story, fs)
|
||||
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
|
||||
}
|
||||
|
||||
private fun clickSave() {
|
||||
if (story!!.starred) {
|
||||
feedUtils.setStorySaved(story!!.storyHash, false, requireContext())
|
||||
} else {
|
||||
feedUtils.setStorySaved(story!!.storyHash, true, requireContext())
|
||||
}
|
||||
fun switchStorySavedState(notifyUser: Boolean = false) {
|
||||
story?.let {
|
||||
val msg = if (it.starred) {
|
||||
feedUtils.setStorySaved(it.storyHash, false, 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() {
|
||||
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)
|
||||
newFragment.show(parentFragmentManager, "dialog")
|
||||
}
|
||||
|
@ -530,17 +555,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
|
|||
val intelFrag = StoryIntelTrainerFragment.newInstance(story, fs)
|
||||
intelFrag.show(parentFragmentManager, StoryIntelTrainerFragment::class.java.name)
|
||||
})
|
||||
binding.readingItemTitle.setOnClickListener(object : View.OnClickListener {
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
binding.readingItemTitle.setOnClickListener { openBrowser() }
|
||||
|
||||
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() {
|
||||
// TODO: enable a selective reload mechanism on load failures?
|
||||
}
|
||||
|
@ -989,8 +1009,31 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
|
|||
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 {
|
||||
private const val BUNDLE_SCROLL_POS_REL = "scrollStateRel"
|
||||
const val VERTICAL_SCROLL_DISTANCE_DP = 240
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(story: Story?, feedTitle: String?, feedFaviconColor: String?, feedFaviconFade: String?, feedFaviconBorder: String?, faviconText: String?, faviconUrl: String?, classifier: Classifier?, displayFeedDetails: Boolean, sourceUserId: String?): ReadingItemFragment {
|
||||
|
|
|
@ -2,17 +2,16 @@ package com.newsblur.fragment;
|
|||
|
||||
import java.util.Map;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.TextView;
|
||||
|
@ -57,6 +56,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
|
|||
return fragment;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -64,9 +64,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
|
|||
fs = (FeedSet) getArguments().getSerializable("feedset");
|
||||
classifier = dbHelper.getClassifierForFeed(story.feedId);
|
||||
|
||||
final Activity activity = getActivity();
|
||||
LayoutInflater inflater = LayoutInflater.from(activity);
|
||||
View v = inflater.inflate(R.layout.dialog_trainstory, null);
|
||||
View v = getLayoutInflater().inflate(R.layout.dialog_trainstory, null);
|
||||
binding = DialogTrainstoryBinding.bind(v);
|
||||
|
||||
// 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
|
||||
for (Map.Entry<String, Integer> rule : classifier.title.entrySet()) {
|
||||
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);
|
||||
label.setText(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
|
||||
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);
|
||||
label.setText(tag);
|
||||
UIUtils.setupIntelDialogRow(row, classifier.tags, tag);
|
||||
|
@ -131,7 +129,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
|
|||
|
||||
// there is a single author per story
|
||||
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);
|
||||
labelAuthor.setText(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
|
||||
// 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);
|
||||
labelFeed.setText(feedUtils.getFeedTitle(story.feedId));
|
||||
UIUtils.setupIntelDialogRow(rowFeed, classifier.feeds, story.feedId);
|
||||
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.setView(v);
|
||||
|
||||
|
@ -164,7 +162,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
|
|||
if ((newTitleTraining != null) && (!TextUtils.isEmpty(binding.intelTitleSelection.getSelection()))) {
|
||||
classifier.title.put(binding.intelTitleSelection.getSelection(), newTitleTraining);
|
||||
}
|
||||
feedUtils.updateClassifier(story.feedId, classifier, fs, activity);
|
||||
feedUtils.updateClassifier(story.feedId, classifier, fs, requireActivity());
|
||||
StoryIntelTrainerFragment.this.dismiss();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -205,7 +205,7 @@ public class APIManager {
|
|||
public ProfileResponse updateUserProfile() {
|
||||
final APIResponse response = get(buildUrl(APIConstants.PATH_MY_PROFILE));
|
||||
if (!response.isError()) {
|
||||
ProfileResponse profileResponse = (ProfileResponse) response.getResponse(gson, ProfileResponse.class);
|
||||
ProfileResponse profileResponse = response.getResponse(gson, ProfileResponse.class);
|
||||
PrefsUtils.saveUserDetails(context, profileResponse.user);
|
||||
return profileResponse;
|
||||
} else {
|
||||
|
@ -234,14 +234,14 @@ public class APIManager {
|
|||
values.put(APIConstants.PARAMETER_FEEDID, id);
|
||||
}
|
||||
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() {
|
||||
ValueMultimap values = new ValueMultimap();
|
||||
values.put(APIConstants.PARAMETER_INCLUDE_TIMESTAMPS, "1");
|
||||
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() {
|
||||
|
@ -256,7 +256,7 @@ public class APIManager {
|
|||
}
|
||||
values.put(APIConstants.PARAMETER_INCLUDE_HIDDEN, APIConstants.VALUE_TRUE);
|
||||
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.
|
||||
*/
|
||||
public StoriesResponse getStories(FeedSet fs, int pageNumber, StoryOrder order, ReadFilter filter) {
|
||||
Uri uri = null;
|
||||
Uri uri;
|
||||
ValueMultimap values = new ValueMultimap();
|
||||
|
||||
// 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);
|
||||
return (StoriesResponse) response.getResponse(gson, StoriesResponse.class);
|
||||
return response.getResponse(gson, StoriesResponse.class);
|
||||
}
|
||||
|
||||
public boolean followUser(final String userId) {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(APIConstants.PARAMETER_USERID, userId);
|
||||
final APIResponse response = post(buildUrl(APIConstants.PATH_FOLLOW), values);
|
||||
if (!response.isError()) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return !response.isError();
|
||||
}
|
||||
|
||||
public boolean unfollowUser(final String userId) {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(APIConstants.PARAMETER_USERID, userId);
|
||||
final APIResponse response = post(buildUrl(APIConstants.PATH_UNFOLLOW), values);
|
||||
if (!response.isError()) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return !response.isError();
|
||||
}
|
||||
|
||||
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);
|
||||
// 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) {
|
||||
|
@ -396,7 +388,7 @@ public class APIManager {
|
|||
|
||||
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
|
||||
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);
|
||||
final APIResponse response = get(buildUrl(APIConstants.PATH_USER_PROFILE), values);
|
||||
if (!response.isError()) {
|
||||
ProfileResponse profileResponse = (ProfileResponse) response.getResponse(gson, ProfileResponse.class);
|
||||
return profileResponse;
|
||||
return response.getResponse(gson, ProfileResponse.class);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -452,8 +443,7 @@ public class APIManager {
|
|||
values.put(APIConstants.PARAMETER_PAGE_NUMBER, Integer.toString(pageNumber));
|
||||
final APIResponse response = get(buildUrl(APIConstants.PATH_USER_ACTIVITIES), values);
|
||||
if (!response.isError()) {
|
||||
ActivitiesResponse activitiesResponse = (ActivitiesResponse) response.getResponse(gson, ActivitiesResponse.class);
|
||||
return activitiesResponse;
|
||||
return response.getResponse(gson, ActivitiesResponse.class);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -466,8 +456,7 @@ public class APIManager {
|
|||
values.put(APIConstants.PARAMETER_PAGE_NUMBER, Integer.toString(pageNumber));
|
||||
final APIResponse response = get(buildUrl(APIConstants.PATH_USER_INTERACTIONS), values);
|
||||
if (!response.isError()) {
|
||||
InteractionsResponse interactionsResponse = (InteractionsResponse) response.getResponse(gson, InteractionsResponse.class);
|
||||
return interactionsResponse;
|
||||
return response.getResponse(gson, InteractionsResponse.class);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -479,8 +468,7 @@ public class APIManager {
|
|||
values.put(APIConstants.PARAMETER_STORYID, storyId);
|
||||
final APIResponse response = get(buildUrl(APIConstants.PATH_STORY_TEXT), values);
|
||||
if (!response.isError()) {
|
||||
StoryTextResponse storyTextResponse = (StoryTextResponse) response.getResponse(gson, StoryTextResponse.class);
|
||||
return storyTextResponse;
|
||||
return response.getResponse(gson, StoryTextResponse.class);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -520,7 +508,7 @@ public class APIManager {
|
|||
values.put(APIConstants.PARAMETER_REPLY_TEXT, reply);
|
||||
APIResponse response = post(buildUrl(APIConstants.PATH_REPLY_TO), values);
|
||||
// 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) {
|
||||
|
@ -532,7 +520,7 @@ public class APIManager {
|
|||
values.put(APIConstants.PARAMETER_REPLY_TEXT, reply);
|
||||
APIResponse response = post(buildUrl(APIConstants.PATH_EDIT_REPLY), values);
|
||||
// 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) {
|
||||
|
@ -543,7 +531,7 @@ public class APIManager {
|
|||
values.put(APIConstants.PARAMETER_REPLY_ID, replyId);
|
||||
APIResponse response = post(buildUrl(APIConstants.PATH_DELETE_REPLY), values);
|
||||
// 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) {
|
||||
|
@ -711,10 +699,10 @@ public class APIManager {
|
|||
}
|
||||
|
||||
private String builderGetParametersString(ContentValues values) {
|
||||
List<String> parameters = new ArrayList<String>();
|
||||
List<String> parameters = new ArrayList<>();
|
||||
for (Entry<String, Object> entry : values.valueSet()) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append((String) entry.getKey());
|
||||
builder.append(entry.getKey());
|
||||
builder.append("=");
|
||||
builder.append(NetworkUtils.encodeURL((String) entry.getValue()));
|
||||
parameters.add(builder.toString());
|
||||
|
@ -749,7 +737,7 @@ public class APIManager {
|
|||
formBody.writeTo(buffer);
|
||||
body = buffer.readUtf8();
|
||||
} 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);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.newsblur.network;
|
|||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
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
|
||||
* return data are expected.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends NewsBlurResponse> T getResponse(Gson gson, Class<T> classOfT) {
|
||||
if (this.isError) {
|
||||
// if we encountered an error, make a generic response type and populate
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.newsblur.service;
|
||||
|
||||
import com.newsblur.util.ExtensionsKt;
|
||||
import com.newsblur.util.PrefConstants;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
||||
|
@ -8,7 +9,7 @@ public class CleanupService extends SubService {
|
|||
public static boolean activelyRunning = false;
|
||||
|
||||
public CleanupService(NBSyncService parent) {
|
||||
super(parent);
|
||||
super(parent, ExtensionsKt.NBScope);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.newsblur.service;
|
|||
import android.util.Log;
|
||||
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.ExtensionsKt;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
|
@ -19,7 +20,7 @@ public class ImagePrefetchService extends SubService {
|
|||
static Set<String> ThumbnailQueue = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
public ImagePrefetchService(NBSyncService parent) {
|
||||
super(parent);
|
||||
super(parent, ExtensionsKt.NBScope);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -33,7 +34,6 @@ public class ImagePrefetchService extends SubService {
|
|||
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return;
|
||||
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
|
||||
|
||||
startExpensiveCycle();
|
||||
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
|
||||
// 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.isBackgroundNetworkAllowed(parent)) return;
|
||||
|
||||
startExpensiveCycle();
|
||||
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
|
||||
// this is a bit expensive, but we are running totally async at a really low priority
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.newsblur.network.domain.NewsBlurResponse;
|
|||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.network.domain.UnreadCountResponse;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.CursorFilters;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FileCache;
|
||||
|
@ -49,6 +50,7 @@ import com.newsblur.util.ReadingAction;
|
|||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
import com.newsblur.util.UIUtils;
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -411,6 +413,8 @@ public class NBSyncService extends JobService {
|
|||
|
||||
ActionsRunning = true;
|
||||
|
||||
StateFilter stateFilter = PrefsUtils.getStateFilter(this);
|
||||
|
||||
actionsloop : while (c.moveToNext()) {
|
||||
sendSyncUpdate(UPDATE_STATUS);
|
||||
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;
|
||||
|
||||
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) {
|
||||
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");
|
||||
int impactFlags = 0;
|
||||
for (ReadingAction ra : FollowupActions) {
|
||||
int impact = ra.doLocal(dbHelper, true);
|
||||
int impact = ra.doLocal(this, dbHelper, true);
|
||||
impactFlags |= impact;
|
||||
}
|
||||
sendSyncUpdate(impactFlags);
|
||||
|
@ -766,7 +770,7 @@ public class NBSyncService extends JobService {
|
|||
return;
|
||||
}
|
||||
|
||||
prepareReadingSession(dbHelper, fs);
|
||||
prepareReadingSession(this, dbHelper, fs);
|
||||
|
||||
LastFeedSet = fs;
|
||||
|
||||
|
@ -785,8 +789,7 @@ public class NBSyncService extends JobService {
|
|||
int pageNumber = FeedPagesSeen.get(fs);
|
||||
int totalStoriesSeen = FeedStoriesSeen.get(fs);
|
||||
|
||||
StoryOrder order = PrefsUtils.getStoryOrder(this, fs);
|
||||
ReadFilter filter = PrefsUtils.getReadFilter(this, fs);
|
||||
CursorFilters cursorFilters = new CursorFilters(this, fs);
|
||||
|
||||
StorySyncRunning = true;
|
||||
sendSyncUpdate(UPDATE_STATUS);
|
||||
|
@ -802,7 +805,7 @@ public class NBSyncService extends JobService {
|
|||
}
|
||||
|
||||
pageNumber++;
|
||||
StoriesResponse apiResponse = apiManager.getStories(fs, pageNumber, order, filter);
|
||||
StoriesResponse apiResponse = apiManager.getStories(fs, pageNumber, cursorFilters.getStoryOrder(), cursorFilters.getReadFilter());
|
||||
|
||||
if (! isStoryResponseGood(apiResponse)) return;
|
||||
|
||||
|
@ -810,7 +813,7 @@ public class NBSyncService extends JobService {
|
|||
return;
|
||||
}
|
||||
|
||||
insertStories(apiResponse, fs);
|
||||
insertStories(apiResponse, fs, cursorFilters.getStateFilter());
|
||||
// re-do any very recent actions that were incorrectly overwritten by this page
|
||||
finishActions();
|
||||
sendSyncUpdate(UPDATE_STORY | UPDATE_STATUS);
|
||||
|
@ -855,7 +858,7 @@ public class NBSyncService extends JobService {
|
|||
private long workaroundReadStoryTimestamp;
|
||||
private long workaroundGloblaSharedStoryTimestamp;
|
||||
|
||||
private void insertStories(StoriesResponse apiResponse, FeedSet fs) {
|
||||
private void insertStories(StoriesResponse apiResponse, FeedSet fs, StateFilter stateFilter) {
|
||||
if (fs.isAllRead()) {
|
||||
// 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
|
||||
|
@ -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);
|
||||
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);
|
||||
dbHelper.insertStories(apiResponse, false);
|
||||
dbHelper.insertStories(apiResponse, stateFilter, false);
|
||||
}
|
||||
|
||||
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
|
||||
* 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) {
|
||||
CursorFilters cursorFilters = new CursorFilters(context, fs);
|
||||
if (! fs.equals(dbHelper.getSessionFeedSet())) {
|
||||
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
|
||||
|
@ -1153,10 +1157,10 @@ public class NBSyncService extends JobService {
|
|||
dbHelper.clearStorySession();
|
||||
// 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
|
||||
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
|
||||
dbHelper.setSessionFeedSet(fs);
|
||||
dbHelper.sendSyncUpdate(UPDATE_STORY | UPDATE_STATUS);
|
||||
UIUtils.syncUpdateStatus(context, UPDATE_STORY | UPDATE_STATUS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_TEXT;
|
|||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.network.domain.StoryTextResponse;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.ExtensionsKt;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
|
||||
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);
|
||||
|
||||
/** story hashes we need to fetch (from newly found stories) */
|
||||
private static Set<String> Hashes;
|
||||
static {Hashes = new HashSet<String>();}
|
||||
private static final Set<String> Hashes = new HashSet<>();
|
||||
/** story hashes we should fetch ASAP (they are waiting on-screen) */
|
||||
private static Set<String> PriorityHashes;
|
||||
static {PriorityHashes = new HashSet<String>();}
|
||||
private static final Set<String> PriorityHashes = new HashSet<>();
|
||||
|
||||
public OriginalTextService(NBSyncService parent) {
|
||||
super(parent);
|
||||
super(parent, ExtensionsKt.NBScope);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -24,8 +24,6 @@ import kotlinx.coroutines.launch
|
|||
*/
|
||||
class SubscriptionSyncService : JobService() {
|
||||
|
||||
private val scope = NBScope
|
||||
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
Log.d(this, "onStartJob")
|
||||
if (!PrefsUtils.hasCookie(this)) {
|
||||
|
@ -33,10 +31,10 @@ class SubscriptionSyncService : JobService() {
|
|||
return false
|
||||
}
|
||||
|
||||
val subscriptionManager = SubscriptionManagerImpl(this@SubscriptionSyncService, scope)
|
||||
val subscriptionManager = SubscriptionManagerImpl(this@SubscriptionSyncService)
|
||||
subscriptionManager.startBillingConnection(object : SubscriptionsListener {
|
||||
override fun onBillingConnectionReady() {
|
||||
scope.launch {
|
||||
NBScope.launch {
|
||||
subscriptionManager.syncActiveSubscription()
|
||||
Log.d(this, "sync active subscription completed.")
|
||||
// manually call jobFinished after work is done
|
||||
|
|
|
@ -3,8 +3,10 @@ package com.newsblur.service;
|
|||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.network.domain.UnreadStoryHashesResponse;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.ExtensionsKt;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -25,7 +27,7 @@ public class UnreadsService extends SubService {
|
|||
static { StoryHashQueue = new ArrayList<String>(); }
|
||||
|
||||
public UnreadsService(NBSyncService parent) {
|
||||
super(parent);
|
||||
super(parent, ExtensionsKt.NBScope);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -137,8 +139,6 @@ public class UnreadsService extends SubService {
|
|||
boolean isTextPrefetchEnabled = PrefsUtils.isTextPrefetchEnabled(parent);
|
||||
if (! (isOfflineEnabled || isEnableNotifications)) return;
|
||||
|
||||
startExpensiveCycle();
|
||||
|
||||
List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
|
||||
List<String> hashSkips = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
|
||||
batchloop: for (String hash : StoryHashQueue) {
|
||||
|
@ -156,7 +156,8 @@ public class UnreadsService extends SubService {
|
|||
break unreadsyncloop;
|
||||
}
|
||||
|
||||
parent.insertStories(response);
|
||||
StateFilter stateFilter = PrefsUtils.getStateFilter(parent);
|
||||
parent.insertStories(response, stateFilter);
|
||||
for (String hash : hashBatch) {
|
||||
StoryHashQueue.remove(hash);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -20,6 +20,7 @@ fun <R> CoroutineScope.executeAsyncTask(
|
|||
withContext(Dispatchers.Main) { onPostExecute(result) }
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val NBScope = CoroutineScope(
|
||||
CoroutineName(TAG) +
|
||||
Dispatchers.Default +
|
||||
|
|
|
@ -34,12 +34,14 @@ class FeedUtils(
|
|||
@JvmField
|
||||
var currentFolderName: String? = null
|
||||
|
||||
fun prepareReadingSession(fs: FeedSet?, resetFirst: Boolean) {
|
||||
fun prepareReadingSession(context: Context, fs: FeedSet?, resetFirst: Boolean) {
|
||||
NBScope.executeAsyncTask(
|
||||
doInBackground = {
|
||||
try {
|
||||
if (resetFirst) NBSyncService.resetReadingSession(dbHelper)
|
||||
NBSyncService.prepareReadingSession(dbHelper, fs)
|
||||
fs?.let {
|
||||
NBSyncService.prepareReadingSession(context, dbHelper, it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 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(
|
||||
doInBackground = {
|
||||
val ra = if (saved) ReadingAction.saveStory(storyHash, userTags) else ReadingAction.unsaveStory(storyHash)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_STORY)
|
||||
dbHelper.enqueueAction(ra)
|
||||
triggerSync(context)
|
||||
|
@ -306,7 +308,7 @@ class FeedUtils(
|
|||
NBScope.executeAsyncTask(
|
||||
doInBackground = {
|
||||
dbHelper.enqueueAction(ra)
|
||||
val impact = ra.doLocal(dbHelper)
|
||||
val impact = ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, impact)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
@ -347,7 +349,7 @@ class FeedUtils(
|
|||
}
|
||||
val ra = ReadingAction.shareStory(story.storyHash, story.id, story.feedId, sourceUserId, comment)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
@ -355,7 +357,7 @@ class FeedUtils(
|
|||
fun renameFeed(context: Context, feedId: String?, newFeedName: String?) {
|
||||
val ra = ReadingAction.renameFeed(feedId, newFeedName)
|
||||
dbHelper.enqueueAction(ra)
|
||||
val impact = ra.doLocal(dbHelper)
|
||||
val impact = ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, impact)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
@ -363,7 +365,7 @@ class FeedUtils(
|
|||
fun unshareStory(story: Story, context: Context) {
|
||||
val ra = ReadingAction.unshareStory(story.storyHash, story.id, story.feedId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
@ -371,7 +373,7 @@ class FeedUtils(
|
|||
fun likeComment(story: Story, commentUserId: String?, context: Context) {
|
||||
val ra = ReadingAction.likeComment(story.id, commentUserId, story.feedId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
@ -379,7 +381,7 @@ class FeedUtils(
|
|||
fun unlikeComment(story: Story, commentUserId: String?, context: Context) {
|
||||
val ra = ReadingAction.unlikeComment(story.id, commentUserId, story.feedId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
@ -387,7 +389,7 @@ class FeedUtils(
|
|||
fun replyToComment(storyId: String?, feedId: String?, commentUserId: String?, replyText: String?, context: Context) {
|
||||
val ra = ReadingAction.replyToComment(storyId, feedId, commentUserId, replyText)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
@ -395,7 +397,7 @@ class FeedUtils(
|
|||
fun updateReply(context: Context, story: Story, commentUserId: String?, replyId: String?, replyText: String?) {
|
||||
val ra = ReadingAction.updateReply(story.id, story.feedId, commentUserId, replyId, replyText)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
@ -403,7 +405,7 @@ class FeedUtils(
|
|||
fun deleteReply(context: Context, story: Story, commentUserId: String?, replyId: String?) {
|
||||
val ra = ReadingAction.deleteReply(story.id, story.feedId, commentUserId, replyId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_SOCIAL)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
@ -448,7 +450,7 @@ class FeedUtils(
|
|||
}
|
||||
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
|
||||
syncUpdateStatus(context, UPDATE_METADATA)
|
||||
triggerSync(context)
|
||||
|
@ -459,7 +461,7 @@ class FeedUtils(
|
|||
fun instaFetchFeed(context: Context, feedId: String?) {
|
||||
val ra = ReadingAction.instaFetch(feedId)
|
||||
dbHelper.enqueueAction(ra)
|
||||
ra.doLocal(dbHelper)
|
||||
ra.doLocal(context, dbHelper)
|
||||
syncUpdateStatus(context, UPDATE_METADATA)
|
||||
triggerSync(context)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
Log.d(NotificationUtils.class.getName(), "Building notification");
|
||||
Intent i = new Intent(context, FeedReading.class);
|
||||
|
|
|
@ -22,6 +22,8 @@ import android.graphics.BitmapFactory;
|
|||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.FileProvider;
|
||||
import android.util.Log;
|
||||
|
@ -40,7 +42,7 @@ public class PrefsUtils {
|
|||
|
||||
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.length() <= 0) return;
|
||||
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);
|
||||
// store the current version
|
||||
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();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getVersion(Context context) {
|
||||
try {
|
||||
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);
|
||||
s.append("<give us some feedback!>%0A%0A%0A");
|
||||
String info = getDebugInfo(context, dbHelper);
|
||||
|
@ -113,7 +116,7 @@ public class PrefsUtils {
|
|||
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();
|
||||
if (f == null) return;
|
||||
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();
|
||||
s.append("app version: ").append(getVersion(context));
|
||||
s.append("\n");
|
||||
|
@ -167,7 +170,7 @@ public class PrefsUtils {
|
|||
return s.toString();
|
||||
}
|
||||
|
||||
public static void logout(Context context, BlurDatabaseHelper dbHelper) {
|
||||
public static void logout(Context context, @NonNull BlurDatabaseHelper dbHelper) {
|
||||
NBSyncService.softInterrupt();
|
||||
NBSyncService.clearState();
|
||||
|
||||
|
@ -194,7 +197,7 @@ public class PrefsUtils {
|
|||
context.startActivity(i);
|
||||
}
|
||||
|
||||
public static void clearPrefsAndDbForLoginAs(Context context, BlurDatabaseHelper dbHelper) {
|
||||
public static void clearPrefsAndDbForLoginAs(Context context, @NonNull BlurDatabaseHelper dbHelper) {
|
||||
NBSyncService.softInterrupt();
|
||||
NBSyncService.clearState();
|
||||
|
||||
|
@ -252,6 +255,7 @@ public class PrefsUtils {
|
|||
saveUserImage(context, profile.photoUrl);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getUserId(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return preferences.getString(PrefConstants.USER_ID, null);
|
||||
|
@ -282,7 +286,7 @@ public class PrefsUtils {
|
|||
}
|
||||
|
||||
private static void saveUserImage(final Context context, String pictureUrl) {
|
||||
Bitmap bitmap = null;
|
||||
Bitmap bitmap;
|
||||
try {
|
||||
URL url = new URL(pictureUrl);
|
||||
URLConnection connection;
|
||||
|
|
|
@ -6,6 +6,7 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_SOCIAL;
|
|||
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
@ -277,7 +278,7 @@ public class ReadingAction implements Serializable {
|
|||
/**
|
||||
* 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
|
||||
NewsBlurResponse result = null;
|
||||
// optional specific responses that are locally actionable
|
||||
|
@ -370,7 +371,7 @@ public class ReadingAction implements Serializable {
|
|||
if (storiesResponse != null) {
|
||||
result = storiesResponse;
|
||||
if (storiesResponse.story != null) {
|
||||
dbHelper.updateStory(storiesResponse, true);
|
||||
dbHelper.updateStory(storiesResponse, stateFilter, true);
|
||||
} else {
|
||||
com.newsblur.util.Log.w(this, "failed to refresh story data after action");
|
||||
}
|
||||
|
@ -391,8 +392,8 @@ public class ReadingAction implements Serializable {
|
|||
return result;
|
||||
}
|
||||
|
||||
public int doLocal(BlurDatabaseHelper dbHelper) {
|
||||
return doLocal(dbHelper, false);
|
||||
public int doLocal(Context context, BlurDatabaseHelper dbHelper) {
|
||||
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.
|
||||
*/
|
||||
public int doLocal(BlurDatabaseHelper dbHelper, boolean isFollowup) {
|
||||
public int doLocal(Context context, BlurDatabaseHelper dbHelper, boolean isFollowup) {
|
||||
String userId = PrefsUtils.getUserId(context);
|
||||
int impact = 0;
|
||||
switch (type) {
|
||||
|
||||
|
@ -434,32 +436,32 @@ public class ReadingAction implements Serializable {
|
|||
|
||||
case SHARE:
|
||||
if (isFollowup) break; // shares are only placeholders
|
||||
dbHelper.setStoryShared(storyHash, true);
|
||||
dbHelper.insertCommentPlaceholder(storyId, feedId, commentReplyText);
|
||||
dbHelper.setStoryShared(storyHash, userId, true);
|
||||
dbHelper.insertCommentPlaceholder(storyId, userId, commentReplyText);
|
||||
impact |= UPDATE_SOCIAL;
|
||||
impact |= UPDATE_STORY;
|
||||
break;
|
||||
|
||||
case UNSHARE:
|
||||
dbHelper.setStoryShared(storyHash, false);
|
||||
dbHelper.clearSelfComments(storyId);
|
||||
dbHelper.setStoryShared(storyHash, userId, false);
|
||||
dbHelper.clearSelfComments(storyId, userId);
|
||||
impact |= UPDATE_SOCIAL;
|
||||
impact |= UPDATE_STORY;
|
||||
break;
|
||||
|
||||
case LIKE_COMMENT:
|
||||
dbHelper.setCommentLiked(storyId, commentUserId, feedId, true);
|
||||
dbHelper.setCommentLiked(storyId, commentUserId, userId, true);
|
||||
impact |= UPDATE_SOCIAL;
|
||||
break;
|
||||
|
||||
case UNLIKE_COMMENT:
|
||||
dbHelper.setCommentLiked(storyId, commentUserId, feedId, false);
|
||||
dbHelper.setCommentLiked(storyId, commentUserId, userId, false);
|
||||
impact |= UPDATE_SOCIAL;
|
||||
break;
|
||||
|
||||
case REPLY:
|
||||
if (isFollowup) break; // replies are only placeholders
|
||||
dbHelper.insertReplyPlaceholder(storyId, feedId, commentUserId, commentReplyText);
|
||||
dbHelper.insertReplyPlaceholder(storyId, userId, commentUserId, commentReplyText);
|
||||
break;
|
||||
|
||||
case EDIT_REPLY:
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.newsblur.util;
|
|||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static android.graphics.Bitmap.Config.ARGB_8888;
|
||||
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.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
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.MaterialToolbar;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.newsblur.NbApplication;
|
||||
import com.newsblur.R;
|
||||
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
|
||||
* 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);
|
||||
row.findViewById(R.id.intel_row_like).setOnClickListener(v -> {
|
||||
classifier.put(key, Classifier.LIKE);
|
||||
|
@ -433,7 +436,11 @@ public class UIUtils {
|
|||
colourIntelDialogRow(row, classifier, key);
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
@ -599,4 +606,15 @@ public class UIUtils {
|
|||
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)};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.newsblur.database.BlurDatabaseHelper
|
||||
import com.newsblur.util.CursorFilters
|
||||
import com.newsblur.util.FeedSet
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -21,9 +22,9 @@ class StoriesViewModel
|
|||
private val _activeStoriesLiveData = MutableLiveData<Cursor>()
|
||||
val activeStoriesLiveData: LiveData<Cursor> = _activeStoriesLiveData
|
||||
|
||||
fun getActiveStories(fs: FeedSet) {
|
||||
fun getActiveStories(fs: FeedSet, cursorFilters: CursorFilters) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
dbHelper.getActiveStoriesCursor(fs, cancellationSignal).let {
|
||||
dbHelper.getActiveStoriesCursor(fs, cursorFilters, cancellationSignal).let {
|
||||
_activeStoriesLiveData.postValue(it)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,9 +149,10 @@ class WidgetRemoteViewsFactory(context: Context, intent: Intent) : RemoteViewsFa
|
|||
Log.d(this.javaClass.name, "onDataSetChanged - get remote stories")
|
||||
val response = apiManager.getStories(fs, 1, StoryOrder.NEWEST, ReadFilter.ALL)
|
||||
response.stories?.let {
|
||||
val stateFilter = PrefsUtils.getStateFilter(context)
|
||||
Log.d(this.javaClass.name, "onDataSetChanged - got ${it.size} remote stories")
|
||||
processStories(response.stories)
|
||||
dbHelper.insertStories(response, true)
|
||||
dbHelper.insertStories(response, stateFilter, true)
|
||||
} ?: Log.d(this.javaClass.name, "onDataSetChanged - null remote stories")
|
||||
}
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true" >
|
||||
android:animateLayoutChanges="true"
|
||||
android:descendantFocusability="blocksDescendants">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/main_top_bar"
|
||||
|
@ -103,7 +103,7 @@
|
|||
to be defined first so that other things can be placed above it. -->
|
||||
<fragment
|
||||
android:id="@+id/fragment_feedintelligenceselector"
|
||||
android:name="com.newsblur.fragment.FeedIntelligenceSelectorFragment"
|
||||
android:name="com.newsblur.fragment.FeedSelectorFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
|
@ -175,7 +175,7 @@
|
|||
android:imeOptions="actionSearch"
|
||||
android:visibility="gone"
|
||||
android:animateLayoutChanges="true"
|
||||
/>
|
||||
android:importantForAutofill="no" />
|
||||
|
||||
<!-- The scrollable and pull-able feed list. -->
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:descendantFocusability="blocksDescendants">
|
||||
|
||||
<include layout="@layout/toolbar_newsblur" />
|
||||
|
||||
|
|
|
@ -41,7 +41,8 @@
|
|||
android:hint="@string/share_comment_hint"
|
||||
android:inputType="textCapSentences|textMultiLine"
|
||||
android:singleLine="false"
|
||||
android:textSize="15sp" />
|
||||
android:textSize="15sp"
|
||||
android:importantForAutofill="no" />
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/container_buttons"
|
||||
|
|
|
@ -6,17 +6,33 @@
|
|||
android:animateLayoutChanges="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_sync_status"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/container_sync_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/status_overlay_background"
|
||||
android:gravity="center"
|
||||
android:padding="2dp"
|
||||
android:text="@string/sync_status_feed_add"
|
||||
android:textColor="@color/status_overlay_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="1dp"
|
||||
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
|
||||
android:id="@+id/text_add_folder_title"
|
||||
|
@ -45,10 +61,10 @@
|
|||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:autofillHints="@null"
|
||||
android:textSize="14sp"
|
||||
android:hint="@string/new_folder_name_hint"
|
||||
android:inputType="textCapSentences"
|
||||
android:maxLines="1" />
|
||||
android:maxLines="1"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ic_create_folder"
|
||||
|
|
|
@ -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>
|
|
@ -24,6 +24,7 @@
|
|||
android:id="@+id/login_username"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:hint="@string/login_username_hint"
|
||||
android:inputType="textEmailAddress"
|
||||
android:textSize="22sp" />
|
||||
|
@ -33,6 +34,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/login_password_hint"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
|
@ -103,7 +105,8 @@
|
|||
android:hint="@string/login_custom_server_hint"
|
||||
android:inputType="textNoSuggestions|textMultiLine"
|
||||
android:textSize="17sp"
|
||||
android:visibility="invisible" />
|
||||
android:visibility="invisible"
|
||||
android:importantForAutofill="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/button_reset_url"
|
||||
|
@ -165,6 +168,7 @@
|
|||
android:id="@+id/registration_username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:hint="@string/login_username_hint"
|
||||
android:inputType="textEmailAddress"
|
||||
android:textSize="22sp">
|
||||
|
@ -176,6 +180,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/login_password_hint"
|
||||
android:inputType="textPassword"
|
||||
android:nextFocusDown="@+id/registration_email"
|
||||
|
@ -186,6 +191,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
android:autofillHints="emailAddress"
|
||||
android:hint="@string/login_registration_email_hint"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textEmailAddress"
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:singleLine="false"
|
||||
android:autofillHints="username"
|
||||
android:inputType="textCapSentences|textMultiLine" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:singleLine="false"
|
||||
android:inputType="textCapSentences|textMultiLine" />
|
||||
android:inputType="textCapSentences|textMultiLine"
|
||||
android:importantForAutofill="no" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
android:layout_marginTop="5dp"
|
||||
android:singleLine="false"
|
||||
android:inputType="textCapSentences|textMultiLine"
|
||||
android:hint="@string/share_comment_hint"/>
|
||||
android:hint="@string/share_comment_hint"
|
||||
android:importantForAutofill="no" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
@ -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>
|
|
@ -31,6 +31,11 @@
|
|||
android:title="@string/menu_newsletters"
|
||||
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"
|
||||
android:title="@string/menu_text_size" >
|
||||
<menu>
|
||||
|
|
|
@ -22,6 +22,12 @@
|
|||
app:showAsAction="never"
|
||||
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"
|
||||
android:title="@string/menu_text_size" >
|
||||
<menu>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<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="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="settings">Preferences…</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_widget_loading">Loading…</string>
|
||||
<string name="menu_newsletters">Newsletters…</string>
|
||||
<string name="menu_shortcuts">Shortcuts…</string>
|
||||
|
||||
<string name="import_export_title">Import/Export OPML</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_images">Storing %s images…</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="off">Off</string>
|
||||
|
@ -739,4 +743,64 @@
|
|||
|
||||
<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="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>
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
<item name="android:background">@color/bar_background</item>
|
||||
<item name="android:textColor">@color/gray30</item>
|
||||
</style>
|
||||
|
||||
<style name="subscriptionHeader.dark">
|
||||
<item name="android:background">@color/dark_bar_background</item>
|
||||
<item name="android:textColor">@color/gray55</item>
|
||||
|
@ -54,6 +55,7 @@
|
|||
<style name="subscriptionIcon">
|
||||
<item name="tint">@color/gray30</item>
|
||||
</style>
|
||||
|
||||
<style name="subscriptionIcon.dark">
|
||||
<item name="tint">@color/gray55</item>
|
||||
</style>
|
||||
|
@ -540,4 +542,24 @@
|
|||
<item name="android:layout_height">40dp</item>
|
||||
</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>
|
||||
|
|
|
@ -61,6 +61,8 @@
|
|||
<item name="fontFamily">@font/whitney</item>
|
||||
<item name="circleProgressIndicator">@style/circleProgressIndicator</item>
|
||||
<item name="toggleButton">@style/toggleButton</item>
|
||||
<item name="snackbarStyle">@style/materialSnackBarTheme</item>
|
||||
<item name="snackbarTextViewStyle">@style/materialSnackBarTextView</item>
|
||||
</style>
|
||||
|
||||
<style name="NewsBlurDarkTheme" parent="Theme.MaterialComponents.NoActionBar">
|
||||
|
@ -124,6 +126,8 @@
|
|||
<item name="fontFamily">@font/whitney</item>
|
||||
<item name="circleProgressIndicator">@style/circleProgressIndicator</item>
|
||||
<item name="toggleButton">@style/toggleButton.dark</item>
|
||||
<item name="snackbarStyle">@style/materialSnackBarTheme.dark</item>
|
||||
<item name="snackbarTextViewStyle">@style/materialSnackBarTextView.dark</item>
|
||||
</style>
|
||||
|
||||
<style name="NewsBlurBlackTheme" parent="Theme.MaterialComponents.NoActionBar" >
|
||||
|
@ -187,6 +191,8 @@
|
|||
<item name="fontFamily">@font/whitney</item>
|
||||
<item name="circleProgressIndicator">@style/circleProgressIndicator</item>
|
||||
<item name="toggleButton">@style/toggleButton.dark</item>
|
||||
<item name="snackbarStyle">@style/materialSnackBarTheme.dark</item>
|
||||
<item name="snackbarTextViewStyle">@style/materialSnackBarTextView.dark</item>
|
||||
</style>
|
||||
|
||||
<style name="NewsBlurTheme.Translucent" parent="NewsBlurTheme">
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
19
clients/android/NewsBlur/build.gradle.kts
Normal file
19
clients/android/NewsBlur/build.gradle.kts
Normal 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)
|
||||
}
|
9
clients/android/NewsBlur/buildSrc/build.gradle.kts
Normal file
9
clients/android/NewsBlur/buildSrc/build.gradle.kts
Normal file
|
@ -0,0 +1,9 @@
|
|||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
14
clients/android/NewsBlur/buildSrc/src/main/java/Config.kt
Normal file
14
clients/android/NewsBlur/buildSrc/src/main/java/Config.kt
Normal 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
|
||||
}
|
15
clients/android/NewsBlur/buildSrc/src/main/java/Const.kt
Normal file
15
clients/android/NewsBlur/buildSrc/src/main/java/Const.kt
Normal 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"
|
||||
}
|
|
@ -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}"
|
||||
}
|
16
clients/android/NewsBlur/buildSrc/src/main/java/Plugins.kt
Normal file
16
clients/android/NewsBlur/buildSrc/src/main/java/Plugins.kt
Normal 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
|
||||
}
|
33
clients/android/NewsBlur/buildSrc/src/main/java/Version.kt
Normal file
33
clients/android/NewsBlur/buildSrc/src/main/java/Version.kt
Normal 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"
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M"
|
||||
kotlin.code.style=obsolete
|
||||
android.useAndroidX=true
|
||||
android.useAndroidX=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=true
|
|
@ -1,6 +1,6 @@
|
|||
#Thu Oct 27 16:20:00 PDT 2022
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -6,5 +6,5 @@ pluginManagement {
|
|||
}
|
||||
}
|
||||
rootProject.name = "NewsBlur"
|
||||
include ':app'
|
||||
include ':app:benchmark'
|
||||
include("app")
|
||||
include("app:benchmark")
|
Loading…
Add table
Reference in a new issue