Remove old app module

Time to say goodbye, see you old friend
This commit is contained in:
Shinokuni 2024-07-02 18:18:39 +02:00
parent 8aac6e4bf4
commit 4388615a59
152 changed files with 1 additions and 12299 deletions

1
app/.gitignore vendored
View File

@ -1 +0,0 @@
/build

View File

@ -1,118 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.readrops.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
versionCode 14
versionName "1.3.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
testOptions {
unitTests.returnDefaultValues = true
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
shrinkResources false
testCoverageEnabled true
applicationIdSuffix ".debug"
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
viewBinding true
buildConfig true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0"
}
lint {
abortOnError false
}
namespace 'com.readrops.app'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':api')
implementation project(':db')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.palette:palette-ktx:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.preference:preference:1.1.1'
implementation "androidx.work:work-runtime-ktx:2.8.0"
implementation "androidx.fragment:fragment-ktx:1.3.5"
implementation "androidx.browser:browser:1.3.0"
implementation(libs.bundles.koin)
testImplementation(libs.bundles.kointest)
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.12.0'
implementation('com.github.bumptech.glide:recyclerview-integration:4.12.0') {
transitive = false
}
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
kapt 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
implementation 'com.mikepenz:fastadapter:3.2.9'
implementation 'com.mikepenz:fastadapter-commons:3.3.0'
implementation 'com.mikepenz:materialdrawer:6.1.2'
implementation "com.mikepenz:aboutlibraries:6.2.3"
implementation "com.mikepenz:iconics-views:3.2.5"
implementation "com.mikepenz:iconics-core:3.2.5"
debugImplementation 'com.facebook.flipper:flipper:0.96.1'
debugImplementation 'com.facebook.soloader:soloader:0.10.1'
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.96.1'
implementation(libs.bundles.room)
implementation(libs.bundles.paging)
}

View File

@ -1,51 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn org.xmlpull.v1.XmlPullParser
-dontwarn org.xmlpull.v1.XmlSerializer
-keep class org.xmlpull.v1.* {*;}
-keep class org.simpleframework.xml.** { *; }
-keep class com.readrops.api.services.freshrss.json.** { *; }
-keep class com.readrops.api.services.nextcloudnews.json.** { *; }
-keep class com.readrops.api.localfeed.** { *; }
-keep class com.readrops.api.opml.model.** { *; }
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
-dontwarn javax.xml.stream.Location
-dontwarn javax.xml.stream.XMLInputFactory
-dontwarn javax.xml.stream.XMLStreamReader
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.joda.convert.FromString
-dontwarn org.joda.convert.ToString
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE

View File

@ -1,306 +0,0 @@
package com.readrops.app
import android.content.Context
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.readrops.app.notifications.sync.SyncResultAnalyser
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Item
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import com.readrops.api.services.SyncResult
import org.joda.time.LocalDateTime
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SyncResultAnalyserTest {
private lateinit var database: Database
private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
private val account1 = Account().apply {
accountName = "test account 1"
accountType = AccountType.FRESHRSS
isNotificationsEnabled = true
}
private val account2 = Account().apply {
accountName = "test account 2"
accountType = AccountType.NEXTCLOUD_NEWS
isNotificationsEnabled = false
}
private val account3 = Account().apply {
accountName = "test account 3"
accountType = AccountType.LOCAL
isNotificationsEnabled = true
}
@Before
fun setupDb() {
database = Room.inMemoryDatabaseBuilder(context, Database::class.java)
.build()
var account1Id = 0
database.accountDao().insert(account1).subscribe { id -> account1Id = id.toInt() }
account1.id = account1Id
var account2Id = 0
database.accountDao().insert(account2).subscribe { id -> account2Id = id.toInt() }
account2.id = account2Id
var account3Id = 0
database.accountDao().insert(account3).subscribe { id -> account3Id = id.toInt() }
account3.id = account3Id
val accountIds = listOf(account1Id, account2Id, account3Id)
for (i in 0..2) {
val feed = Feed().apply {
name = "feed ${i + 1}"
iconUrl = "https://i0.wp.com/mrmondialisation.org/wp-content/uploads/2017/05/ico_final.gif"
this.accountId = accountIds.find { it == (i + 1) }!!
isNotificationEnabled = i % 2 == 0
}
database.feedDao().insert(feed).subscribe()
}
}
@After
fun closeDb() {
database.close()
}
@Test
fun testOneElementEveryWhere() {
val item = Item().apply {
title = "caseOneElementEveryWhere"
feedId = 1
remoteId = "item 1"
pubDate = LocalDateTime.now()
}
database.itemDao()
.insert(item)
.subscribe()
val syncResult = SyncResult().apply { items = mutableListOf(item) }
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
assertEquals("caseOneElementEveryWhere", notifContent.content)
assertEquals("feed 1", notifContent.title)
assertTrue(notifContent.largeIcon != null)
assertTrue(notifContent.accountId!! > 0)
database.itemDao()
.delete(item)
.subscribe()
}
@Test
fun testTwoItemsOneFeed() {
val item = Item().apply {
title = "caseTwoItemsOneFeed"
feedId = 1
}
val syncResult = SyncResult().apply { items = listOf(item, item, item) }
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
assertEquals(context.getString(R.string.new_items, 3), notifContent.content)
assertEquals("feed 1", notifContent.title)
assertTrue(notifContent.largeIcon != null)
assertTrue(notifContent.accountId!! > 0)
}
@Test
fun testMultipleFeeds() {
val item = Item().apply { feedId = 1 }
val item2 = Item().apply { feedId = 3 }
val syncResult = SyncResult().apply { items = listOf(item, item2) }
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
assertEquals(context.getString(R.string.new_items, 2), notifContent.content)
assertEquals(account1.accountName, notifContent.title)
assertTrue(notifContent.largeIcon != null)
assertTrue(notifContent.accountId!! > 0)
}
@Test
fun testMultipleAccounts() {
val item = Item().apply { feedId = 1 }
val item2 = Item().apply { feedId = 3 }
val syncResult = SyncResult().apply { items = listOf(item, item2) }
val syncResult2 = SyncResult().apply { items = listOf(item, item2) }
val syncResults = mutableMapOf<Account, SyncResult>().apply {
put(account1, syncResult)
put(account3, syncResult2)
}
val notifContent = SyncResultAnalyser(context, syncResults, database).getSyncNotifContent()
assertEquals(context.getString(R.string.new_items, 4), notifContent.title)
}
@Test
fun testAccountNotificationsDisabled() {
val item1 = Item().apply {
title = "testAccountNotificationsDisabled"
feedId = 1
}
val item2 = Item().apply {
title = "testAccountNotificationsDisabled2"
feedId = 1
}
val syncResult = SyncResult().apply { items = listOf(item1, item2) }
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account2, syncResult)), database).getSyncNotifContent()
assert(notifContent.title == null)
assert(notifContent.content == null)
assert(notifContent.largeIcon == null)
}
@Test
fun testFeedNotificationsDisabled() {
val item1 = Item().apply {
title = "testAccountNotificationsDisabled"
feedId = 2
}
val item2 = Item().apply {
title = "testAccountNotificationsDisabled2"
feedId = 2
}
val syncResult = SyncResult().apply { items = listOf(item1, item2) }
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
assert(notifContent.title == null)
assert(notifContent.content == null)
assert(notifContent.largeIcon == null)
}
@Test
fun testTwoAccountsWithOneAccountNotificationsEnabled() {
val item1 = Item().apply {
title = "testTwoAccountsWithOneAccountNotificationsEnabled"
feedId = 1
remoteId = "remoteId 1"
pubDate = LocalDateTime.now()
}
val item2 = Item().apply {
title = "testTwoAccountsWithOneAccountNotificationsEnabled2"
feedId = 3
}
val item3 = Item().apply {
title = "testTwoAccountsWithOneAccountNotificationsEnabled3"
feedId = 3
}
database.itemDao().insert(item1).subscribe()
val syncResult1 = SyncResult().apply { items = listOf(item1) }
val syncResult2 = SyncResult().apply { items = listOf(item2, item3) }
val syncResults = mutableMapOf<Account, SyncResult>().apply {
put(account1, syncResult1)
put(account2, syncResult2)
}
val notifContent = SyncResultAnalyser(context, syncResults, database).getSyncNotifContent()
assertEquals("testTwoAccountsWithOneAccountNotificationsEnabled", notifContent.content)
assertEquals("feed 1", notifContent.title)
assertTrue(notifContent.largeIcon != null)
assertTrue(notifContent.item != null)
database.itemDao().delete(item1).subscribe()
}
@Test
fun testTwoAccountsWithOneFeedNotificationEnabled() {
val item1 = Item().apply {
title = "testTwoAccountsWithOneAccountNotificationsEnabled"
feedId = 1
remoteId = "remoteId 1"
pubDate = LocalDateTime.now()
}
val item2 = Item().apply {
title = "testTwoAccountsWithOneAccountNotificationsEnabled2"
feedId = 2
}
val item3 = Item().apply {
title = "testTwoAccountsWithOneAccountNotificationsEnabled3"
feedId = 2
}
database.itemDao().insert(item1).subscribe()
val syncResult1 = SyncResult().apply { items = listOf(item1) }
val syncResult2 = SyncResult().apply { items = listOf(item2, item3) }
val syncResults = mutableMapOf<Account, SyncResult>().apply {
put(account1, syncResult1)
put(account2, syncResult2)
}
val notifContent = SyncResultAnalyser(context, syncResults, database).getSyncNotifContent()
assertEquals("testTwoAccountsWithOneAccountNotificationsEnabled", notifContent.content)
assertEquals("feed 1", notifContent.title)
assertTrue(notifContent.largeIcon != null)
assertTrue(notifContent.item != null)
database.itemDao().delete(item1).subscribe()
}
@Test
fun testOneAccountTwoFeedsWithOneFeedNotificationEnabled() {
val item1 = Item().apply {
title = "testTwoAccountsWithOneAccountNotificationsEnabled"
feedId = 1
remoteId = "remoteId 1"
pubDate = LocalDateTime.now()
}
val item2 = Item().apply {
title = "testTwoAccountsWithOneAccountNotificationsEnabled2"
feedId = 2
}
val item3 = Item().apply {
title = "testTwoAccountsWithOneAccountNotificationsEnabled3"
feedId = 2
}
database.itemDao().insert(item1).subscribe()
val syncResult = SyncResult().apply { items = listOf(item1, item2, item3) }
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
assertEquals("testTwoAccountsWithOneAccountNotificationsEnabled", notifContent.content)
assertEquals("feed 1", notifContent.title)
assertTrue(notifContent.largeIcon != null)
assertTrue(notifContent.item != null)
assertTrue(notifContent.accountId!! > 0)
database.itemDao().delete(item1).subscribe()
}
}

View File

@ -1,18 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".ReadropsDebugApp"
tools:ignore="AllowBackup,GoogleAppIndexingWarning"
tools:replace="android:name">
<meta-data android:name="com.niddler.icon" android:value="android"/>
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove"
android:exported="false" />
</application>
</manifest>

View File

@ -1,55 +0,0 @@
package com.readrops.app;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Configuration;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.android.utils.FlipperUtils;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin;
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.soloader.SoLoader;
public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provider {
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
//initFlipper();
}
private void initFlipper() {
if (FlipperUtils.shouldEnableFlipper(this)) {
FlipperClient client = AndroidFlipperClient.getInstance(this);
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
NetworkFlipperPlugin networkPlugin = new NetworkFlipperPlugin();
client.addPlugin(networkPlugin);
client.addPlugin(new DatabasesFlipperPlugin(this));
client.addPlugin(CrashReporterPlugin.getInstance());
client.addPlugin(NavigationFlipperPlugin.getInstance());
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
client.start();
}
}
@NonNull
@Override
public Configuration getWorkManagerConfiguration() {
return new Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.build();
}
}

View File

@ -1,3 +0,0 @@
<resources>
<string name="app_name" translatable="false">ReadropsDebug</string>
</resources>

View File

@ -1,90 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application
android:name=".ReadropsApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity
android:name=".notifications.NotificationPermissionActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".item.WebViewActivity"
android:theme="@style/AppTheme.NoActionBar" />
<service android:name=".utils.feedscolors.FeedsColorsIntentService" />
<receiver android:name=".notifications.sync.SyncWorker$MarkReadReceiver" />
<receiver android:name=".notifications.sync.SyncWorker$ReadLaterReceiver" />
<activity android:name=".settings.SettingsActivity" />
<activity android:name=".account.AccountTypeListActivity" />
<activity
android:name=".account.AddAccountActivity"
android:label="@string/add_account" />
<activity
android:name=".feedsfolders.ManageFeedsFoldersActivity"
android:label="@string/manage_feeds_folders"
android:parentActivityName=".itemslist.MainActivity"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".itemslist.MainActivity"
android:label="@string/articles"
android:launchMode="singleTask"
android:theme="@style/SplashTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".item.ItemActivity"
android:parentActivityName=".itemslist.MainActivity"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".addfeed.AddFeedActivity"
android:label="@string/add_feed_title"
android:parentActivityName=".itemslist.MainActivity"
android:exported="true">
<intent-filter android:label="@string/new_feed">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,75 +0,0 @@
package com.readrops.app
import androidx.preference.PreferenceManager
import com.readrops.api.services.Credentials
import com.readrops.app.account.AccountViewModel
import com.readrops.app.addfeed.AddFeedsViewModel
import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel
import com.readrops.app.item.ItemViewModel
import com.readrops.app.itemslist.MainViewModel
import com.readrops.app.notifications.NotificationPermissionViewModel
import com.readrops.app.repositories.FreshRSSRepository
import com.readrops.app.repositories.LocalFeedRepository
import com.readrops.app.repositories.NextNewsRepository
import com.readrops.app.utils.GlideApp
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module
val appModule = module {
factory { (account: Account) ->
when (account.accountType) {
AccountType.LOCAL -> LocalFeedRepository(get(), get(), androidContext(), account)
AccountType.NEXTCLOUD_NEWS -> NextNewsRepository(get(parameters = { parametersOf(Credentials.toCredentials(account)) }),
get(), androidContext(), account)
AccountType.FRESHRSS -> FreshRSSRepository(get(parameters = { parametersOf(Credentials.toCredentials(account)) }),
get(), androidContext(), account)
else -> throw IllegalArgumentException("Account type not supported")
}
}
viewModel {
MainViewModel(get())
}
viewModel {
AddFeedsViewModel(get(), get())
}
viewModel {
ItemViewModel(get())
}
viewModel {
ManageFeedsFoldersViewModel(get())
}
viewModel {
NotificationPermissionViewModel(get())
}
viewModel {
AccountViewModel(get())
}
single { GlideApp.with(androidApplication()) }
single { PreferenceManager.getDefaultSharedPreferences(androidContext()) }
/* single<Niddler> {
val niddler = AndroidNiddler.Builder()
.setNiddlerInformation(AndroidNiddler.fromApplication(get()))
.setPort(0)
.setMaxStackTraceSize(10)
.build()
niddler.attachToApplication(get())
niddler.apply { start() }
}*/
}

View File

@ -1,71 +0,0 @@
package com.readrops.app
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceManager
import com.readrops.api.apiModule
import com.readrops.app.utils.SharedPreferencesManager
import com.readrops.db.dbModule
import io.reactivex.plugins.RxJavaPlugins
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.core.logger.Level
open class ReadropsApp : Application() {
override fun onCreate() {
super.onCreate()
RxJavaPlugins.setErrorHandler { e: Throwable? -> }
createNotificationChannels()
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
startKoin {
androidLogger(Level.ERROR)
androidContext(this@ReadropsApp)
modules(apiModule, dbModule, appModule)
}
val theme = when (SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME)) {
getString(R.string.theme_value_light) -> AppCompatDelegate.MODE_NIGHT_NO
getString(R.string.theme_value_dark) -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
AppCompatDelegate.setDefaultNightMode(theme)
}
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val feedsColorsChannel = NotificationChannel(FEEDS_COLORS_CHANNEL_ID,
getString(R.string.feeds_colors), NotificationManager.IMPORTANCE_DEFAULT)
feedsColorsChannel.description = getString(R.string.get_feeds_colors)
val opmlExportChannel = NotificationChannel(OPML_EXPORT_CHANNEL_ID,
getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT)
opmlExportChannel.description = getString(R.string.opml_export_description)
val syncChannel = NotificationChannel(SYNC_CHANNEL_ID,
getString(R.string.auto_synchro), NotificationManager.IMPORTANCE_LOW)
syncChannel.description = getString(R.string.account_synchro)
val manager = getSystemService(NotificationManager::class.java)!!
manager.createNotificationChannel(feedsColorsChannel)
manager.createNotificationChannel(opmlExportChannel)
manager.createNotificationChannel(syncChannel)
}
}
companion object {
const val FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel"
const val OPML_EXPORT_CHANNEL_ID = "opmlExportChannel"
const val SYNC_CHANNEL_ID = "syncChannel"
}
}

View File

@ -1,196 +0,0 @@
package com.readrops.app.account;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.app.utils.OPMLHelper;
import com.readrops.app.R;
import com.readrops.app.databinding.ActivityAccountTypeListBinding;
import com.readrops.app.itemslist.MainActivity;
import com.readrops.app.utils.Utils;
import com.readrops.db.entities.account.Account;
import com.readrops.db.entities.account.AccountType;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.OPMLHelper.OPEN_OPML_FILE_REQUEST;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_TYPE;
import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY;
import org.koin.android.compat.ViewModelCompat;
public class AccountTypeListActivity extends AppCompatActivity {
private static final String TAG = AccountTypeListActivity.class.getSimpleName();
private ActivityAccountTypeListBinding binding;
private AccountTypeListAdapter adapter;
private AccountViewModel viewModel;
private boolean fromMainActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityAccountTypeListBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
setTitle(R.string.new_account);
binding.accountTypeRecyclerview.setLayoutManager(new LinearLayoutManager(this));
binding.accountTypeRecyclerview.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
fromMainActivity = getIntent().getBooleanExtra(FROM_MAIN_ACTIVITY, false);
if (fromMainActivity)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
adapter = new AccountTypeListAdapter(accountType -> {
if (accountType != AccountType.LOCAL) {
Intent intent = new Intent(getApplicationContext(), AddAccountActivity.class);
if (fromMainActivity)
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
intent.putExtra(ACCOUNT_TYPE, (Parcelable) accountType);
startActivity(intent);
finish();
} else {
Account account = new Account(null, getString(AccountType.LOCAL.getTypeName()), AccountType.LOCAL);
account.setCurrentAccount(true);
viewModel.insert(account)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableSingleObserver<Long>() {
@Override
public void onSuccess(Long id) {
account.setId(id.intValue());
goToNextActivity(account);
}
@Override
public void onError(Throwable e) {
Log.e(TAG, e.getMessage());
Utils.showSnackbar(binding.accountTypeListRoot, e.getMessage());
}
});
}
});
binding.accountTypeRecyclerview.setAdapter(adapter);
adapter.setAccountTypes(getData());
}
private List<AccountType> getData() {
List<AccountType> accountTypes = new ArrayList<>();
accountTypes.add(AccountType.LOCAL);
accountTypes.add(AccountType.NEXTCLOUD_NEWS);
accountTypes.add(AccountType.FRESHRSS);
return accountTypes;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
public void openOPMLFile(View view) {
OPMLHelper.openFileIntent(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
MaterialDialog dialog = new MaterialDialog.Builder(this)
.title(R.string.opml_processing)
.content(R.string.operation_takes_time)
.progress(true, 100)
.cancelable(false)
.show();
parseOPMLFile(uri, dialog);
}
super.onActivityResult(requestCode, resultCode, data);
}
private void parseOPMLFile(Uri uri, MaterialDialog dialog) {
Account account = new Account(null, getString(AccountType.LOCAL.getTypeName()), AccountType.LOCAL);
account.setCurrentAccount(true);
viewModel.insert(account)
.flatMapCompletable(id -> {
account.setId(id.intValue());
viewModel.setAccount(account);
return viewModel.parseOPMLFile(uri, this);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
dialog.dismiss();
goToNextActivity(account);
}
@Override
public void onError(Throwable e) {
Log.e(TAG, e.getMessage());
dialog.dismiss();
Utils.showSnackbar(binding.accountTypeListRoot, e.getMessage());
}
});
}
private void goToNextActivity(Account account) {
if (fromMainActivity) {
Intent intent = new Intent();
intent.putExtra(ACCOUNT, account);
setResult(RESULT_OK, intent);
} else {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.putExtra(ACCOUNT, account);
startActivity(intent);
}
finish();
}
}

View File

@ -1,66 +0,0 @@
package com.readrops.app.account;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.readrops.app.databinding.AccountTypeItemBinding;
import com.readrops.db.entities.account.AccountType;
import java.util.List;
public class AccountTypeListAdapter extends RecyclerView.Adapter<AccountTypeListAdapter.AccountTypeViewHolder> {
private List<AccountType> accountTypes;
private OnItemClickListener listener;
public AccountTypeListAdapter(OnItemClickListener listener) {
this.listener = listener;
}
@NonNull
@Override
public AccountTypeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
AccountTypeItemBinding binding = AccountTypeItemBinding.inflate(LayoutInflater.from(parent.getContext()),
parent, false);
return new AccountTypeViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull AccountTypeViewHolder holder, int position) {
AccountType accountType = accountTypes.get(position);
holder.binding.accountTypeName.setText(accountType.getTypeName());
holder.binding.accountTypeLogo.setImageResource(accountType.getIconRes());
holder.binding.getRoot().setOnClickListener(v -> listener.onItemClick(accountType));
}
@Override
public int getItemCount() {
return accountTypes.size();
}
public void setAccountTypes(List<AccountType> accountTypes) {
this.accountTypes = accountTypes;
notifyDataSetChanged();
}
public interface OnItemClickListener {
void onItemClick(AccountType accountType);
}
public class AccountTypeViewHolder extends RecyclerView.ViewHolder {
private AccountTypeItemBinding binding;
public AccountTypeViewHolder(AccountTypeItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}

View File

@ -1,71 +0,0 @@
package com.readrops.app.account;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import com.readrops.api.opml.OPMLParser;
import com.readrops.app.repositories.ARepository;
import com.readrops.db.Database;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import org.koin.core.parameter.ParametersHolderKt;
import org.koin.java.KoinJavaComponent;
import java.io.FileNotFoundException;
import java.util.List;
import java.util.Map;
import io.reactivex.Completable;
import io.reactivex.Single;
public class AccountViewModel extends ViewModel {
private ARepository repository;
private final Database database;
public AccountViewModel(@NonNull Database database) {
this.database = database;
}
public void setAccount(Account account) {
repository = KoinJavaComponent.get(ARepository.class, null,
() -> ParametersHolderKt.parametersOf(account));
}
public Completable login(Account account, boolean insert) {
setAccount(account);
return repository.login(account, insert);
}
public Single<Long> insert(Account account) {
return database.accountDao().insert(account);
}
public Completable update(Account account) {
return database.accountDao().update(account);
}
public Completable delete(Account account) {
return database.accountDao().delete(account);
}
public Single<Integer> getAccountCount() {
return database.accountDao().getAccountCount();
}
@SuppressWarnings("unchecked")
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
return repository.getFoldersWithFeeds();
}
public Completable parseOPMLFile(Uri uri, Context context) throws FileNotFoundException {
/*return OPMLParser.read(context.getContentResolver().openInputStream(uri))
.flatMapCompletable(foldersAndFeeds -> repository.insertOPMLFoldersAndFeeds(foldersAndFeeds));*/
return Completable.complete();
}
}

View File

@ -1,239 +0,0 @@
package com.readrops.app.account;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.util.Patterns;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import com.readrops.app.R;
import com.readrops.app.databinding.ActivityAddAccountBinding;
import com.readrops.app.itemslist.MainActivity;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.db.entities.account.Account;
import com.readrops.db.entities.account.AccountType;
import io.reactivex.CompletableObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_TYPE;
import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
import org.koin.android.compat.ViewModelCompat;
public class AddAccountActivity extends AppCompatActivity {
private static final String TAG = AddAccountActivity.class.getSimpleName();
private ActivityAddAccountBinding binding;
private AccountViewModel viewModel;
private AccountType accountType;
private boolean forwardResult, editAccount;
private Account accountToEdit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityAddAccountBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
accountType = getIntent().getParcelableExtra(ACCOUNT_TYPE);
int flag = getIntent().getFlags();
forwardResult = flag == Intent.FLAG_ACTIVITY_FORWARD_RESULT;
accountToEdit = getIntent().getParcelableExtra(EDIT_ACCOUNT);
if (forwardResult || accountToEdit != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (accountToEdit != null) {
editAccount = true;
fillFields();
} else {
binding.providerImage.setImageResource(accountType.getIconRes());
binding.providerName.setText(accountType.getTypeName());
binding.addAccountName.setText(accountType.getTypeName());
if (accountType == AccountType.FRESHRSS) {
binding.addAccountPasswordLayout.setHelperText(getString(R.string.password_helper));
}
}
}
public void createAccount(View view) {
if (fieldsAreValid()) {
String url = binding.addAccountUrl.getText().toString().trim();
String name = binding.addAccountName.getText().toString().trim();
String login = binding.addAccountLogin.getText().toString().trim();
String password = binding.addAccountPassword.getText().toString().trim();
if (!(url.toLowerCase().contains(Utils.HTTP_PREFIX) || url.toLowerCase().contains(Utils.HTTPS_PREFIX))) {
url = Utils.HTTPS_PREFIX + url;
}
if (editAccount) {
accountToEdit.setUrl(url);
accountToEdit.setAccountName(name);
accountToEdit.setLogin(login);
accountToEdit.setPassword(password);
updateAccount();
} else {
Account account = new Account(url, name, accountType);
account.setLogin(login);
account.setPassword(password);
viewModel.login(account, true)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new CompletableObserver() {
@Override
public void onSubscribe(Disposable d) {
binding.addAccountLoading.setVisibility(View.VISIBLE);
binding.addAccountValidate.setEnabled(false);
}
@Override
public void onComplete() {
saveLoginPassword(account);
if (forwardResult) {
Intent intent = new Intent();
intent.putExtra(ACCOUNT, account);
setResult(RESULT_OK, intent);
} else {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.putExtra(ACCOUNT, account);
startActivity(intent);
}
finish();
}
@Override
public void onError(Throwable e) {
Log.d(TAG, e.getMessage());
binding.addAccountLoading.setVisibility(View.GONE);
binding.addAccountValidate.setEnabled(true);
Utils.showSnackbar(binding.addAccountRoot, e.getMessage());
}
});
}
}
}
private boolean fieldsAreValid() {
boolean valid = true;
if (binding.addAccountUrl.getText().toString().trim().isEmpty()) {
binding.addAccountUrl.setError(getString(R.string.empty_field));
valid = false;
} else if (!Patterns.WEB_URL.matcher(binding.addAccountUrl.getText().toString().trim()).matches()) {
binding.addAccountUrl.setError(getString(R.string.wrong_url));
valid = false;
}
if (binding.addAccountName.getText().toString().trim().isEmpty()) {
binding.addAccountName.setError(getString(R.string.empty_field));
valid = false;
}
if (binding.addAccountLogin.getText().toString().trim().isEmpty()) {
binding.addAccountLogin.setError(getString(R.string.empty_field));
valid = false;
}
if (binding.addAccountPassword.getText().toString().trim().isEmpty()) {
binding.addAccountPassword.setError(getString(R.string.empty_field));
valid = false;
}
return valid;
}
private void saveLoginPassword(Account account) {
SharedPreferencesManager.writeValue(account.getLoginKey(), account.getLogin());
SharedPreferencesManager.writeValue(account.getPasswordKey(), account.getPassword());
account.setLogin(null);
account.setPassword(null);
}
private void fillFields() {
binding.providerImage.setImageResource(accountToEdit.getAccountType().getIconRes());
binding.providerName.setText(accountToEdit.getAccountType().getTypeName());
binding.addAccountUrl.setText(accountToEdit.getUrl());
binding.addAccountName.setText(accountToEdit.getAccountName());
binding.addAccountLogin.setText(SharedPreferencesManager.readString(accountToEdit.getLoginKey()));
binding.addAccountPassword.setText(SharedPreferencesManager.readString(accountToEdit.getPasswordKey()));
}
private void updateAccount() {
viewModel.login(accountToEdit, false)
.doOnError(throwable -> Utils.showSnackbar(binding.addAccountRoot, throwable.getMessage()))
.doAfterTerminate(() -> saveLoginPassword(accountToEdit))
.andThen(viewModel.update(accountToEdit))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new CompletableObserver() {
@Override
public void onSubscribe(Disposable d) {
binding.addAccountLoading.setVisibility(View.VISIBLE);
binding.addAccountValidate.setEnabled(false);
}
@Override
public void onComplete() {
finish();
}
@Override
public void onError(Throwable e) {
binding.addAccountLoading.setVisibility(View.GONE);
binding.addAccountValidate.setEnabled(true);
Utils.showSnackbar(binding.addAccountRoot, e.getMessage());
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
createAccount(null);
return true;
}
return super.onKeyUp(keyCode, event);
}
}

View File

@ -1,51 +0,0 @@
package com.readrops.app.addfeed;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.app.R;
import com.readrops.db.entities.account.Account;
import java.util.List;
public class AccountArrayAdapter extends ArrayAdapter<Account> {
public AccountArrayAdapter(@NonNull Context context, @NonNull List<Account> objects) {
super(context, 0, objects);
}
@Override
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
return createItemView(position, convertView, parent);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
return createItemView(position, convertView, parent);
}
private View createItemView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.account_type_item, parent, false);
}
Account account = getItem(position);
ImageView accountIcon = convertView.findViewById(R.id.account_type_logo);
TextView accountName = convertView.findViewById(R.id.account_type_name);
accountIcon.setImageResource(account.getAccountType().getIconRes());
accountName.setText(account.getAccountType().getTypeName());
return convertView;
}
}

View File

@ -1,333 +0,0 @@
package com.readrops.app.addfeed;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Patterns;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.mikepenz.fastadapter.FastAdapter;
import com.mikepenz.fastadapter.adapters.ItemAdapter;
import com.mikepenz.fastadapter.commons.utils.DiffCallback;
import com.mikepenz.fastadapter.commons.utils.FastAdapterDiffUtil;
import com.readrops.app.R;
import com.readrops.app.databinding.ActivityAddFeedBinding;
import com.readrops.app.utils.customviews.ReadropsItemTouchCallback;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.account.Account;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
import org.koin.android.compat.ViewModelCompat;
public class AddFeedActivity extends AppCompatActivity implements View.OnClickListener {
private AccountArrayAdapter arrayAdapter;
private ItemAdapter<ParsingResult> parseItemsAdapter;
private ItemAdapter<FeedInsertionResult> insertionResultsAdapter;
FastAdapter<ParsingResult> fastAdapter;
private AddFeedsViewModel viewModel;
private ArrayList<Feed> feedsToUpdate;
private ActivityAddFeedBinding binding;
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityAddFeedBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.addFeedLoad.setOnClickListener(this);
binding.addFeedOk.setOnClickListener(this);
binding.addFeedOk.setEnabled(false);
viewModel = ViewModelCompat.getViewModel(this, AddFeedsViewModel.class);
parseItemsAdapter = new ItemAdapter<>();
fastAdapter = FastAdapter.with(parseItemsAdapter);
fastAdapter.withSelectable(true);
fastAdapter.withOnClickListener((v, adapter, item, position) -> {
item.setChecked(!item.isChecked());
fastAdapter.notifyAdapterItemChanged(position);
binding.addFeedOk.setEnabled(recyclerViewHasCheckedItems());
return true;
});
binding.addFeedResults.setAdapter(fastAdapter);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
binding.addFeedResults.setLayoutManager(layoutManager);
new ItemTouchHelper(new ReadropsItemTouchCallback(this,
new ReadropsItemTouchCallback.Config.Builder()
.swipeDirs(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)
.leftDraw(Color.RED, R.drawable.ic_delete, null)
.rightDraw(Color.RED, R.drawable.ic_delete, null)
.swipeCallback((viewHolder, direction) -> {
parseItemsAdapter.remove(viewHolder.getAdapterPosition());
if (parseItemsAdapter.getAdapterItemCount() == 0) {
binding.addFeedResultsTextView.setVisibility(View.GONE);
binding.addFeedResults.setVisibility(View.GONE);
}
})
.build()))
.attachToRecyclerView(binding.addFeedResults);
insertionResultsAdapter = new ItemAdapter<>();
RecyclerView.LayoutManager layoutManager1 = new LinearLayoutManager(this);
binding.addFeedInsertedResultsRecyclerview.setAdapter(FastAdapter.with(insertionResultsAdapter));
binding.addFeedInsertedResultsRecyclerview.setLayoutManager(layoutManager1);
viewModel.getAccounts().observe(this, accounts -> {
// set the current account at the top of the list
int currentAccountId = getIntent().getIntExtra(ACCOUNT_ID, 1);
Collections.sort(accounts, (o1, o2) -> {
if (o1.getId() == currentAccountId) {
return -1;
} else if (o2.getId() == currentAccountId) {
return 1;
} else {
return 0;
}
});
arrayAdapter = new AccountArrayAdapter(this, accounts);
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.addFeedAccountSpinner.setAdapter(arrayAdapter);
});
feedsToUpdate = new ArrayList<>();
// new feed intent
if (getIntent().getAction() != null && getIntent().getAction().equals(Intent.ACTION_SEND)) {
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
binding.addFeedTextInput.setText(text);
onClick(binding.addFeedLoad);
}
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.add_feed_load) {
if (isValidUrl()) {
binding.addFeedLoadingMessage.setVisibility(View.GONE);
binding.addFeedLoading.setVisibility(View.VISIBLE);
loadFeed();
}
} else if (v.getId() == R.id.add_feed_ok) {
insertionResultsAdapter.clear();
insertFeeds();
}
}
private boolean isValidUrl() {
String url = binding.addFeedTextInput.getText().toString().trim();
if (url.isEmpty()) {
binding.addFeedTextInput.setError(getString(R.string.empty_field));
return false;
} else if (!Patterns.WEB_URL.matcher(url).matches()) {
binding.addFeedTextInput.setError(getString(R.string.wrong_url));
return false;
} else
return true;
}
private boolean recyclerViewHasCheckedItems() {
for (ParsingResult result : parseItemsAdapter.getAdapterItems()) {
if (result.isChecked())
return true;
}
return false;
}
private void disableParsingResult(ParsingResult parsingResult) {
for (ParsingResult result : parseItemsAdapter.getAdapterItems()) {
if (result.getUrl().equals(parsingResult.getUrl())) {
result.setChecked(false);
fastAdapter.notifyAdapterItemChanged(parseItemsAdapter.getAdapterPosition(result));
}
}
}
private void loadFeed() {
String url = binding.addFeedTextInput.getText().toString().trim();
final String finalUrl;
if (!(url.contains(Utils.HTTP_PREFIX) || url.contains(Utils.HTTPS_PREFIX)))
finalUrl = Utils.HTTPS_PREFIX + url;
else
finalUrl = url;
viewModel.parseUrl(finalUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableSingleObserver<List<ParsingResult>>() {
@Override
public void onSuccess(List<ParsingResult> parsingResultList) {
displayParseResults(parsingResultList);
}
@Override
public void onError(Throwable e) {
Utils.showSnackbar(binding.addFeedRoot, e.getMessage());
binding.addFeedLoading.setVisibility(View.GONE);
}
});
}
private void displayParseResults(List<ParsingResult> parsingResultList) {
binding.addFeedLoading.setVisibility(View.GONE);
if (!parsingResultList.isEmpty()) {
binding.addFeedResultsTextView.setVisibility(View.VISIBLE);
binding.addFeedResults.setVisibility(View.VISIBLE);
DiffUtil.DiffResult diffResult = FastAdapterDiffUtil.calculateDiff(parseItemsAdapter, parsingResultList, new DiffCallback<ParsingResult>() {
@Override
public boolean areItemsTheSame(ParsingResult oldItem, ParsingResult newItem) {
return oldItem.getUrl().equals(newItem.getUrl());
}
@Override
public boolean areContentsTheSame(ParsingResult oldItem, ParsingResult newItem) {
return oldItem.getUrl().equals(newItem.getUrl()) &&
oldItem.isChecked() == newItem.isChecked();
}
@Nullable
@Override
public Object getChangePayload(ParsingResult oldItem, int oldItemPosition, ParsingResult newItem, int newItemPosition) {
newItem.setChecked(oldItem.isChecked());
return newItem;
}
}, false);
FastAdapterDiffUtil.set(parseItemsAdapter, diffResult);
binding.addFeedOk.setEnabled(recyclerViewHasCheckedItems());
} else {
parseItemsAdapter.clear();
binding.addFeedResultsTextView.setVisibility(View.GONE);
binding.addFeedResults.setVisibility(View.GONE);
binding.addFeedLoadingMessage.setVisibility(View.VISIBLE);
binding.addFeedLoadingMessage.setText(R.string.no_feed_found);
}
}
private void insertFeeds() {
binding.addFeedInsertProgressbar.setVisibility(View.VISIBLE);
binding.addFeedOk.setEnabled(false);
List<ParsingResult> feedsToInsert = new ArrayList<>();
for (ParsingResult result : parseItemsAdapter.getAdapterItems()) {
if (result.isChecked())
feedsToInsert.add(result);
}
Account account = (Account) binding.addFeedAccountSpinner.getSelectedItem();
account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
viewModel.addFeeds(feedsToInsert, account)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableSingleObserver<List<FeedInsertionResult>>() {
@Override
public void onSuccess(List<FeedInsertionResult> feedInsertionResults) {
displayInsertionResults(feedInsertionResults);
}
@Override
public void onError(Throwable e) {
binding.addFeedInsertProgressbar.setVisibility(View.GONE);
binding.addFeedOk.setEnabled(true);
Utils.showSnackbar(binding.addFeedRoot, e.getMessage());
}
});
}
private void displayInsertionResults(List<FeedInsertionResult> feedInsertionResults) {
binding.addFeedInsertProgressbar.setVisibility(View.GONE);
binding.addFeedInsertedResultsRecyclerview.setVisibility(View.VISIBLE);
for (FeedInsertionResult feedInsertionResult : feedInsertionResults) {
if (feedInsertionResult.getFeed() != null)
feedsToUpdate.add(feedInsertionResult.getFeed());
disableParsingResult(feedInsertionResult.getParsingResult());
}
insertionResultsAdapter.add(feedInsertionResults);
binding.addFeedOk.setEnabled(recyclerViewHasCheckedItems());
}
@Override
public void onBackPressed() {
finish();
super.onBackPressed();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void finish() {
if (!feedsToUpdate.isEmpty()) {
Intent intent = new Intent();
intent.putParcelableArrayListExtra(FEEDS, feedsToUpdate);
setResult(RESULT_OK, intent);
}
super.finish();
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
onClick(binding.addFeedLoad);
return true;
}
return super.onKeyUp(keyCode, event);
}
}

View File

@ -1,56 +0,0 @@
package com.readrops.app.addfeed;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.readrops.api.localfeed.LocalRSSDataSource;
import com.readrops.app.repositories.ARepository;
import com.readrops.app.utils.HtmlParser;
import com.readrops.db.Database;
import com.readrops.db.entities.account.Account;
import org.koin.core.parameter.ParametersHolderKt;
import org.koin.java.KoinJavaComponent;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Single;
public class AddFeedsViewModel extends ViewModel {
private final Database database;
private final LocalRSSDataSource localRSSDataSource;
public AddFeedsViewModel(@NonNull Database database, @NonNull LocalRSSDataSource localRSSDataSource) {
this.database = database;
this.localRSSDataSource = localRSSDataSource;
}
public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results, Account account) {
ARepository repository = KoinJavaComponent.get(ARepository.class, null,
() -> ParametersHolderKt.parametersOf(account));
return repository.addFeeds(results);
}
public Single<List<ParsingResult>> parseUrl(String url) {
return Single.create(emitter -> {
List<ParsingResult> results = new ArrayList<>();
if (localRSSDataSource.isUrlRSSResource(url)) {
ParsingResult parsingResult = new ParsingResult(url, null);
results.add(parsingResult);
} else {
results.addAll(HtmlParser.getFeedLink(url));
}
emitter.onSuccess(results);
});
}
public LiveData<List<Account>> getAccounts() {
return database.accountDao().selectAllAsync();
}
}

View File

@ -1,138 +0,0 @@
package com.readrops.app.addfeed;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.mikepenz.fastadapter.FastAdapter;
import com.mikepenz.fastadapter.items.AbstractItem;
import com.readrops.app.R;
import com.readrops.db.entities.Feed;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class FeedInsertionResult extends AbstractItem<FeedInsertionResult, FeedInsertionResult.FeedInsertionViewHolder> {
private Feed feed;
private ParsingResult parsingResult;
private FeedInsertionError insertionError;
public FeedInsertionResult() {
// empty constructor
}
public Feed getFeed() {
return feed;
}
public void setFeed(Feed feed) {
this.feed = feed;
}
public ParsingResult getParsingResult() {
return parsingResult;
}
public void setParsingResult(ParsingResult parsingResult) {
this.parsingResult = parsingResult;
}
public FeedInsertionError getInsertionError() {
return insertionError;
}
public void setInsertionError(FeedInsertionError insertionError) {
this.insertionError = insertionError;
}
@Override
public boolean isSelectable() {
return false;
}
@NonNull
@Override
public FeedInsertionViewHolder getViewHolder(View v) {
return new FeedInsertionViewHolder(v);
}
@Override
public int getType() {
return 0;
}
@Override
public int getLayoutRes() {
return R.layout.feed_insertion_result;
}
public enum FeedInsertionError {
ERROR,
NETWORK_ERROR,
DB_ERROR,
PARSE_ERROR,
FORMAT_ERROR,
UNKNOWN_ERROR
}
class FeedInsertionViewHolder extends FastAdapter.ViewHolder<FeedInsertionResult> {
private TextView feedInsertionRes;
private ImageView feedInsertionIcon;
public FeedInsertionViewHolder(View itemView) {
super(itemView);
feedInsertionRes = itemView.findViewById(R.id.feed_insertion_result_text_view);
feedInsertionIcon = itemView.findViewById(R.id.feed_insertion_result_icon);
}
@Override
public void bindView(FeedInsertionResult item, List<Object> payloads) {
if (item.getInsertionError() == null) {
setText(R.string.feed_insertion_successfull, item.parsingResult);
feedInsertionIcon.setImageResource(R.drawable.ic_check_green);
} else {
switch (item.getInsertionError()) {
case NETWORK_ERROR:
setText(R.string.feed_insertion_network_failed, item.parsingResult);
break;
case DB_ERROR:
break;
case PARSE_ERROR:
setText(R.string.feed_insertion_parse_failed, item.parsingResult);
break;
case FORMAT_ERROR:
setText(R.string.feed_insertion_wrong_format, item.parsingResult);
break;
case UNKNOWN_ERROR:
setText(R.string.feed_insertion_unknown_error, item.parsingResult);
break;
case ERROR:
setText(R.string.feed_insertion_error, item.parsingResult);
}
feedInsertionIcon.setImageResource(R.drawable.ic_warning_red);
}
}
private void setText(@StringRes int stringRes, ParsingResult parsingResult) {
feedInsertionRes.setText(itemView.getContext().getString(stringRes,
parsingResult.getLabel() != null ? parsingResult.getLabel() :
parsingResult.getUrl()));
}
@Override
public void unbindView(@NotNull FeedInsertionResult item) {
// not useful
}
}
}

View File

@ -1,157 +0,0 @@
package com.readrops.app.addfeed;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.mikepenz.fastadapter.FastAdapter;
import com.mikepenz.fastadapter.items.AbstractItem;
import com.readrops.app.R;
import com.readrops.db.entities.Feed;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.ParsingResultViewHolder> {
private String url;
private String label;
private boolean checked;
private Integer folderId;
public ParsingResult(String url, String label) {
this.url = url;
this.label = label;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getLabel() {
return label;
}
public static List<ParsingResult> toParsingResults(List<Feed> feeds) {
List<ParsingResult> parsingResults = new ArrayList<>();
for (Feed feed : feeds) {
ParsingResult parsingResult = new ParsingResult(feed.getUrl(), null);
parsingResult.setFolderId(feed.getFolderId());
parsingResults.add(parsingResult);
}
return parsingResults;
}
public void setLabel(String label) {
this.label = label;
}
public void setChecked(boolean checked) {
this.checked = checked;
}
public boolean isChecked() {
return checked;
}
public Integer getFolderId() {
return folderId;
}
public void setFolderId(Integer folderId) {
this.folderId = folderId;
}
@Override
public boolean isSelectable() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@NonNull
@Override
public ParsingResultViewHolder getViewHolder(View v) {
return new ParsingResultViewHolder(v);
}
@Override
public int getType() {
return R.id.add_feed_main_layout;
}
@Override
public int getLayoutRes() {
return R.layout.add_feed_item;
}
class ParsingResultViewHolder extends FastAdapter.ViewHolder<ParsingResult> {
private TextView feedLabel;
private TextView feedUrl;
private CheckBox checkBox;
public ParsingResultViewHolder(View itemView) {
super(itemView);
feedLabel = itemView.findViewById(R.id.add_feed_item_label);
feedUrl = itemView.findViewById(R.id.add_feed_item_url);
checkBox = itemView.findViewById(R.id.add_feed_checkbox);
}
@Override
public void bindView(@NotNull ParsingResult item, List<Object> payloads) {
if (!payloads.isEmpty()) {
ParsingResult newItem = (ParsingResult) payloads.get(0);
checkBox.setChecked(newItem.isChecked());
} else {
if (item.getLabel() != null && !item.getLabel().isEmpty())
feedLabel.setText(item.getLabel());
else
feedLabel.setVisibility(View.GONE);
feedUrl.setText(item.getUrl());
checkBox.setChecked(item.isChecked());
checkBox.setClickable(false);
}
}
@Override
public void unbindView(@NotNull ParsingResult item) {
// not useful
}
}
@Override
public boolean equals(Object o) {
if (o == null)
return false;
else if (!(o instanceof ParsingResult))
return false;
else {
ParsingResult parsingResult = (ParsingResult) o;
return parsingResult.getUrl().equals(this.getUrl());
}
}
}

View File

@ -1,156 +0,0 @@
package com.readrops.app.feedsfolders;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.api.utils.exceptions.ConflictException;
import com.readrops.api.utils.exceptions.UnknownFormatException;
import com.readrops.app.R;
import com.readrops.app.databinding.ActivityManageFeedsFoldersBinding;
import com.readrops.app.feedsfolders.feeds.FeedsFragment;
import com.readrops.app.feedsfolders.folders.FoldersFragment;
import com.readrops.app.utils.Utils;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import org.koin.android.compat.ViewModelCompat;
public class ManageFeedsFoldersActivity extends AppCompatActivity {
private ActivityManageFeedsFoldersBinding binding;
private ManageFeedsFoldersViewModel viewModel;
private Account account;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityManageFeedsFoldersBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.manageFeedsFoldersToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
account = getIntent().getParcelableExtra(ACCOUNT);
FeedsFoldersPageAdapter pageAdapter = new FeedsFoldersPageAdapter(getSupportFragmentManager());
binding.manageFeedsFoldersViewpager.setAdapter(pageAdapter);
binding.manageFeedsFoldersTablayout.setupWithViewPager(binding.manageFeedsFoldersViewpager);
viewModel = ViewModelCompat.getViewModel(this, ManageFeedsFoldersViewModel.class);
viewModel.setAccount(account);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (account.getAccountType().getAccountConfig().getCanCreateFolder())
getMenuInflater().inflate(R.menu.feeds_menu, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.add_folder:
addFolder();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onBackPressed() {
finish();
super.onBackPressed();
}
private void addFolder() {
new MaterialDialog.Builder(ManageFeedsFoldersActivity.this)
.title(R.string.add_folder)
.positiveText(R.string.validate)
.input(R.string.folder, 0, (dialog, input) -> {
Folder folder = new Folder();
folder.setName(input.toString());
folder.setAccountId(account.getId());
viewModel.addFolder(folder)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> {
String message;
if (throwable instanceof ConflictException)
message = getString(R.string.folder_already_exists);
else if (throwable instanceof UnknownFormatException)
message = getString(R.string.folder_bad_format);
else
message = getString(R.string.error_occured);
Utils.showSnackbar(binding.manageFeedsFoldersRoot, message);
})
.subscribe();
})
.show();
}
public class FeedsFoldersPageAdapter extends FragmentPagerAdapter {
private FeedsFoldersPageAdapter(FragmentManager fragmentManager) {
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
@Override
public int getCount() {
return 2;
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getApplicationContext().getString(R.string.feeds);
case 1:
return getApplicationContext().getString(R.string.folders);
default:
return null;
}
}
@Override
public Fragment getItem(int position) {
Fragment fragment = null;
switch (position) {
case 0:
fragment = FeedsFragment.newInstance(account);
break;
case 1:
fragment = FoldersFragment.newInstance(account);
break;
}
return fragment;
}
}
}

View File

@ -1,88 +0,0 @@
package com.readrops.app.feedsfolders;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.readrops.app.repositories.ARepository;
import com.readrops.db.Database;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import com.readrops.db.pojo.FeedWithFolder;
import com.readrops.db.pojo.FolderWithFeedCount;
import org.koin.core.parameter.ParametersHolderKt;
import org.koin.java.KoinJavaComponent;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Single;
public class ManageFeedsFoldersViewModel extends ViewModel {
private final Database database;
private LiveData<List<FeedWithFolder>> feedsWithFolder;
private LiveData<List<Folder>> folders;
private ARepository repository;
private Account account;
public ManageFeedsFoldersViewModel(@NonNull Database database) {
this.database = database;
}
private void setup() {
repository = KoinJavaComponent.get(ARepository.class, null,
() -> ParametersHolderKt.parametersOf(account));
feedsWithFolder = database.feedDao().getAllFeedsWithFolder(account.getId());
folders = database.folderDao().getAllFolders(account.getId());
}
public LiveData<List<FeedWithFolder>> getFeedsWithFolder() {
return feedsWithFolder;
}
public Completable updateFeedWithFolder(Feed feed) {
return repository.updateFeed(feed);
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
setup();
}
public LiveData<List<Folder>> getFolders() {
return folders;
}
public LiveData<List<FolderWithFeedCount>> getFoldersWithFeedCount() {
return database.folderDao().getFoldersWithFeedCount(account.getId());
}
public Single<Long> addFolder(Folder folder) {
return repository.addFolder(folder);
}
public Completable updateFolder(Folder folder) {
return repository.updateFolder(folder);
}
public Completable deleteFolder(Folder folder) {
return repository.deleteFolder(folder);
}
public Completable deleteFeed(Feed feed) {
return repository.deleteFeed(feed);
}
public Single<Integer> getFeedCountByAccount() {
return database.feedDao().getFeedCount(account.getId());
}
}

View File

@ -1,138 +0,0 @@
package com.readrops.app.feedsfolders.feeds;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.textfield.TextInputEditText;
import com.readrops.app.R;
import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import com.readrops.db.pojo.FeedWithFolder;
import org.koin.android.compat.SharedViewModelCompat;
import java.util.ArrayList;
import java.util.Map;
import java.util.TreeMap;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public class EditFeedDialogFragment extends DialogFragment implements AdapterView.OnItemSelectedListener {
private TextInputEditText feedName;
private TextInputEditText feedUrl;
private Spinner folder;
private Map<String, Integer> values;
private FeedWithFolder feedWithFolder;
private Account account;
private ManageFeedsFoldersViewModel viewModel;
public EditFeedDialogFragment() {
}
public static EditFeedDialogFragment newInstance(FeedWithFolder feedWithFolder, Account account) {
Bundle args = new Bundle();
args.putParcelable("feedWithFolder", feedWithFolder);
args.putParcelable(ACCOUNT, account);
EditFeedDialogFragment fragment = new EditFeedDialogFragment();
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
viewModel = SharedViewModelCompat.sharedViewModel(this, ManageFeedsFoldersViewModel.class).getValue();
feedWithFolder = getArguments().getParcelable("feedWithFolder");
account = getArguments().getParcelable(ACCOUNT);
viewModel.setAccount(account);
View v = getActivity().getLayoutInflater().inflate(R.layout.edit_feed_layout, null);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.edit_feed)
.setPositiveButton(R.string.validate, (dialog, which) -> {
Feed feed = feedWithFolder.getFeed();
feed.setName(feedName.getText().toString().trim());
feed.setUrl(feedUrl.getText().toString().trim());
viewModel.updateFeedWithFolder(feed)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
});
builder.setView(v);
fillData(v);
viewModel.getFolders().observe(this, folders -> {
values = new TreeMap<>(String::compareTo);
if (!account.getAccountType().getAccountConfig().getAddNoFolder())
values.put(getString(R.string.no_folder), 0);
for (Folder folder : folders) {
values.put(folder.getName(), folder.getId());
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(),
android.R.layout.simple_spinner_dropdown_item, new ArrayList<>(values.keySet()));
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
folder.setAdapter(adapter);
folder.setOnItemSelectedListener(this);
if (feedWithFolder.getFolder() != null)
folder.setSelection(adapter.getPosition(feedWithFolder.getFolder().getName()));
else
folder.setSelection(adapter.getPosition(getString(R.string.no_folder)));
});
return builder.create();
}
private void fillData(View v) {
feedName = v.findViewById(R.id.edit_feed_name_edit_text);
feedUrl = v.findViewById(R.id.edit_feed_url_edit_text);
folder = v.findViewById(R.id.edit_feed_folder_spinner);
if (!account.getAccountType().getAccountConfig().isFeedUrlReadOnly())
feedUrl.setEnabled(false);
feedName.setText(feedWithFolder.getFeed().getName());
feedUrl.setText(feedWithFolder.getFeed().getUrl());
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String folderName = (String) parent.getAdapter().getItem(position);
int folderId = values.get(folderName);
feedWithFolder.getFeed().setFolderId(folderId == 0 ? null : folderId);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
}

View File

@ -1,87 +0,0 @@
package com.readrops.app.feedsfolders.feeds
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.readrops.app.databinding.FeedOptionsLayoutBinding
import com.readrops.app.utils.ReadropsKeys.ACCOUNT
import com.readrops.db.entities.account.Account
import com.readrops.db.pojo.FeedWithFolder
class FeedOptionsDialogFragment : BottomSheetDialogFragment() {
private lateinit var feedWithFolder: FeedWithFolder
private lateinit var account: Account
private var _binding: FeedOptionsLayoutBinding? = null
private val binding get() = _binding!!
companion object {
const val FEED_KEY = "FEED_KEY"
fun newInstance(feedWithFolder: FeedWithFolder, account: Account): FeedOptionsDialogFragment {
val bundle = Bundle()
bundle.putParcelable(FEED_KEY, feedWithFolder)
bundle.putParcelable(ACCOUNT, account)
val feedsOptionsDialogFragment = FeedOptionsDialogFragment()
feedsOptionsDialogFragment.arguments = bundle
return feedsOptionsDialogFragment
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
feedWithFolder = arguments?.getParcelable(FEED_KEY)!!
account = arguments?.getParcelable(ACCOUNT)!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FeedOptionsLayoutBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.feedOptionsTitle.text = feedWithFolder.feed.name
binding.feedOptionsEditLayout.setOnClickListener { openEditFeedDialog() }
binding.feedOptionsOpenRootLayout.setOnClickListener { openFeedRootUrl() }
binding.feedOptionsDeleteLayout.setOnClickListener { deleteFeed() }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun openEditFeedDialog() {
dismiss()
val editFeedDialogFragment = EditFeedDialogFragment.newInstance(feedWithFolder, account)
activity
?.supportFragmentManager
?.beginTransaction()
?.add(editFeedDialogFragment, "")
?.commit()
}
private fun openFeedRootUrl() {
dismiss()
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(feedWithFolder.feed.siteUrl)))
}
private fun deleteFeed() {
dismiss()
(parentFragment as FeedsFragment).deleteFeed(feedWithFolder.feed)
}
}

View File

@ -1,130 +0,0 @@
package com.readrops.app.feedsfolders.feeds;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.readrops.app.R;
import com.readrops.app.databinding.FeedLayoutBinding;
import com.readrops.app.utils.GlideRequests;
import com.readrops.db.pojo.FeedWithFolder;
import org.koin.java.KoinJavaComponent;
import java.util.List;
public class FeedsAdapter extends ListAdapter<FeedWithFolder, FeedsAdapter.FeedViewHolder> {
private ManageFeedsListener listener;
private static final DiffUtil.ItemCallback<FeedWithFolder> DIFF_CALLBACK = new DiffUtil.ItemCallback<FeedWithFolder>() {
@Override
public boolean areItemsTheSame(@NonNull FeedWithFolder feedWithFolder, @NonNull FeedWithFolder t1) {
return feedWithFolder.getFeed().getId() == t1.getFeed().getId();
}
@Override
public boolean areContentsTheSame(@NonNull FeedWithFolder feedWithFolder, @NonNull FeedWithFolder t1) {
boolean folder = false;
if (feedWithFolder.getFolder() != null && t1.getFolder() != null)
folder = feedWithFolder.getFolder().getName().equals(t1.getFolder().getName());
return feedWithFolder.getFeed().getName().equals(t1.getFeed().getName())
&& folder;
}
@Nullable
@Override
public Object getChangePayload(@NonNull FeedWithFolder oldItem, @NonNull FeedWithFolder newItem) {
return newItem;
}
};
public FeedsAdapter(ManageFeedsListener listener) {
super(DIFF_CALLBACK);
this.listener = listener;
}
@NonNull
@Override
public FeedViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
FeedLayoutBinding binding = FeedLayoutBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false);
return new FeedViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull FeedViewHolder viewHolder, int i) {
FeedWithFolder feedWithFolder = getItem(i);
if (feedWithFolder.getFeed().getIconUrl() != null) {
KoinJavaComponent.<GlideRequests>get(GlideRequests.class)
.load(feedWithFolder.getFeed().getIconUrl())
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_rss_feed_grey)
.into(viewHolder.binding.feedLayoutIcon);
} else
viewHolder.binding.feedLayoutIcon.setImageResource(R.drawable.ic_rss_feed_grey);
viewHolder.binding.feedLayoutName.setText(feedWithFolder.getFeed().getName());
if (feedWithFolder.getFeed().getDescription() != null) {
viewHolder.binding.feedLayoutDescription.setVisibility(View.VISIBLE);
viewHolder.binding.feedLayoutDescription.setText(feedWithFolder.getFeed().getDescription());
} else
viewHolder.binding.feedLayoutDescription.setVisibility(View.GONE);
if (feedWithFolder.getFolder() != null)
viewHolder.binding.feedLayoutFolder.setText(feedWithFolder.getFolder().getName());
else
viewHolder.binding.feedLayoutFolder.setText(R.string.no_folder);
viewHolder.itemView.setOnClickListener(v -> listener.onEdit(feedWithFolder));
viewHolder.itemView.setOnLongClickListener(v -> {
listener.onOpenLink(feedWithFolder);
return true;
});
}
@Override
public void onBindViewHolder(@NonNull FeedViewHolder holder, int position, @NonNull List<Object> payloads) {
if (!payloads.isEmpty()) {
FeedWithFolder feedWithFolder = (FeedWithFolder) payloads.get(0);
holder.binding.feedLayoutName.setText(feedWithFolder.getFeed().getName());
if (feedWithFolder.getFolder() != null)
holder.binding.feedLayoutName.setText(feedWithFolder.getFolder().getName());
else
holder.binding.feedLayoutName.setText(R.string.no_folder);
} else
onBindViewHolder(holder, position);
}
public interface ManageFeedsListener {
void onOpenLink(FeedWithFolder feedWithFolder);
void onEdit(FeedWithFolder feedWithFolder);
}
protected class FeedViewHolder extends RecyclerView.ViewHolder {
private FeedLayoutBinding binding;
public FeedViewHolder(FeedLayoutBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}

View File

@ -1,153 +0,0 @@
package com.readrops.app.feedsfolders.feeds;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.app.R;
import com.readrops.app.databinding.FragmentFeedsBinding;
import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.account.Account;
import com.readrops.db.pojo.FeedWithFolder;
import org.koin.android.compat.SharedViewModelCompat;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.schedulers.Schedulers;
public class FeedsFragment extends Fragment {
private FeedsAdapter adapter;
private ManageFeedsFoldersViewModel viewModel;
private Account account;
private FragmentFeedsBinding binding;
public FeedsFragment() {
// Required empty public constructor
}
public static FeedsFragment newInstance(Account account) {
FeedsFragment fragment = new FeedsFragment();
Bundle args = new Bundle();
args.putParcelable(ACCOUNT, account);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
account = getArguments().getParcelable(ACCOUNT);
if (account.getLogin() == null)
account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
if (account.getPassword() == null)
account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
viewModel = SharedViewModelCompat.sharedViewModel(this, ManageFeedsFoldersViewModel.class).getValue();
viewModel.setAccount(account);
viewModel.getFeedsWithFolder().observe(this, feedWithFolders -> {
adapter.submitList(feedWithFolders);
if (feedWithFolders.size() > 0) {
binding.feedsEmptyList.setVisibility(View.GONE);
} else {
binding.feedsEmptyList.setVisibility(View.VISIBLE);
}
});
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentFeedsBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.feedsRecyclerview.setLayoutManager(new LinearLayoutManager(getActivity()));
adapter = new FeedsAdapter(new FeedsAdapter.ManageFeedsListener() {
@Override
public void onEdit(FeedWithFolder feedWithFolder) {
openFeedOptionsFragment(feedWithFolder);
}
@Override
public void onOpenLink(FeedWithFolder feedWithFolder) {
}
});
binding.feedsRecyclerview.setAdapter(adapter);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
public void deleteFeed(Feed feed) {
new MaterialDialog.Builder(getContext())
.title(R.string.delete_feed)
.positiveText(R.string.validate)
.negativeText(R.string.cancel)
.onPositive((dialog, which) -> viewModel.deleteFeed(feed)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
Utils.showSnackbar(binding.feedsRoot,
getString(R.string.feed_deleted, feed.getName()));
}
@Override
public void onError(Throwable e) {
String message;
if (e instanceof Resources.NotFoundException)
message = getString(R.string.feed_doesnt_exist, feed.getName());
else
message = getString(R.string.error_occured);
Utils.showSnackbar(binding.feedsRoot, message);
}
}))
.show();
}
private void openFeedOptionsFragment(FeedWithFolder feedWithFolder) {
FeedOptionsDialogFragment dialogFragment = FeedOptionsDialogFragment.Companion.newInstance(feedWithFolder, account);
getChildFragmentManager()
.beginTransaction()
.add(dialogFragment, "")
.commit();
}
}

View File

@ -1,66 +0,0 @@
package com.readrops.app.feedsfolders.folders
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.readrops.app.databinding.FolderOptionsLayoutBinding
import com.readrops.db.entities.Folder
class FolderOptionsDialogFragment : BottomSheetDialogFragment() {
private lateinit var folder: Folder
private var _binding: FolderOptionsLayoutBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
folder = arguments?.getParcelable(FOLDER_KEY)!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FolderOptionsLayoutBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.folderOptionsTitle.text = folder.name
binding.folderOptionsEdit.setOnClickListener { openEditFolderDialog() }
binding.folderOptionsDelete.setOnClickListener { deleteFolder() }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun openEditFolderDialog() {
dismiss()
(parentFragment as FoldersFragment).editFolder(folder)
}
private fun deleteFolder() {
dismiss()
(parentFragment as FoldersFragment).deleteFolder(folder)
}
companion object {
const val FOLDER_KEY = "FOLDER_KEY"
fun newInstance(folder: Folder): FolderOptionsDialogFragment {
val args = Bundle()
args.putParcelable(FOLDER_KEY, folder)
val fragment = FolderOptionsDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -1,106 +0,0 @@
package com.readrops.app.feedsfolders.folders;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.readrops.app.R;
import com.readrops.app.databinding.FolderLayoutBinding;
import com.readrops.db.entities.Folder;
import com.readrops.db.pojo.FolderWithFeedCount;
import java.util.List;
public class FoldersAdapter extends ListAdapter<FolderWithFeedCount, FoldersAdapter.FolderViewHolder> {
private ManageFoldersListener listener;
private int totalFeedCount;
public FoldersAdapter(ManageFoldersListener listener) {
super(DIFF_CALLBACK);
this.listener = listener;
}
public void setTotalFeedCount(int totalFeedCount) {
this.totalFeedCount = totalFeedCount;
}
private static final DiffUtil.ItemCallback<FolderWithFeedCount> DIFF_CALLBACK = new DiffUtil.ItemCallback<FolderWithFeedCount>() {
@Override
public boolean areItemsTheSame(@NonNull FolderWithFeedCount oldItem, @NonNull FolderWithFeedCount newItem) {
return oldItem.getFolder().getId() == newItem.getFolder().getId();
}
@Override
public boolean areContentsTheSame(@NonNull FolderWithFeedCount oldItem, @NonNull FolderWithFeedCount newItem) {
return TextUtils.equals(oldItem.getFolder().getName(), newItem.getFolder().getName()) &&
oldItem.getFeedCount() == newItem.getFeedCount();
}
@Nullable
@Override
public Object getChangePayload(@NonNull FolderWithFeedCount oldItem, @NonNull FolderWithFeedCount newItem) {
return newItem;
}
};
@NonNull
@Override
public FolderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
FolderLayoutBinding binding = FolderLayoutBinding.inflate(LayoutInflater.from(parent.getContext()),
parent, false);
return new FolderViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull FolderViewHolder holder, int position, @NonNull List<Object> payloads) {
if (!payloads.isEmpty()) {
FolderWithFeedCount folderWithFeedCount = (FolderWithFeedCount) payloads.get(0);
holder.bind(folderWithFeedCount);
} else
onBindViewHolder(holder, position);
}
@Override
public void onBindViewHolder(@NonNull FolderViewHolder holder, int position) {
FolderWithFeedCount folderWithFeedCount = getItem(position);
holder.bind(folderWithFeedCount);
holder.itemView.setOnClickListener(v -> listener.onClick(folderWithFeedCount.getFolder()));
}
public interface ManageFoldersListener {
void onClick(Folder folder);
}
public class FolderViewHolder extends RecyclerView.ViewHolder {
private FolderLayoutBinding binding;
public FolderViewHolder(FolderLayoutBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
private void bind(FolderWithFeedCount folderWithFeedCount) {
binding.folderName.setText(folderWithFeedCount.getFolder().getName());
int stringRes = folderWithFeedCount.getFeedCount() > 1 ? R.string.feeds_number : R.string.feed_number;
binding.folderFeedsCount.setText(itemView.getContext().getString(stringRes, String.valueOf(folderWithFeedCount.getFeedCount())));
binding.folderProgressBar.setMax(totalFeedCount);
binding.folderProgressBar.setProgress(folderWithFeedCount.getFeedCount());
}
}
}

View File

@ -1,174 +0,0 @@
package com.readrops.app.feedsfolders.folders;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.api.utils.exceptions.ConflictException;
import com.readrops.api.utils.exceptions.UnknownFormatException;
import com.readrops.app.R;
import com.readrops.app.databinding.FragmentFoldersBinding;
import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import org.koin.android.compat.SharedViewModelCompat;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
public class FoldersFragment extends Fragment {
private FoldersAdapter adapter;
private FragmentFoldersBinding binding;
private ManageFeedsFoldersViewModel viewModel;
private Account account;
public FoldersFragment() {
// Required empty public constructor
}
public static FoldersFragment newInstance(Account account) {
FoldersFragment fragment = new FoldersFragment();
Bundle args = new Bundle();
args.putParcelable(ACCOUNT, account);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
account = getArguments().getParcelable(ACCOUNT);
if (account.getLogin() == null)
account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
if (account.getPassword() == null)
account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
adapter = new FoldersAdapter(this::openFolderOptionsDialog);
viewModel = SharedViewModelCompat.sharedViewModel(this, ManageFeedsFoldersViewModel.class).getValue();
viewModel.setAccount(account);
viewModel.getFeedCountByAccount()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableSingleObserver<Integer>() {
@Override
public void onSuccess(Integer feedCount) {
adapter.setTotalFeedCount(feedCount);
getFoldersWithFeedCount();
}
@Override
public void onError(Throwable e) {
Utils.showSnackbar(binding.foldersRoot, e.getMessage());
}
});
}
private void getFoldersWithFeedCount() {
viewModel.getFoldersWithFeedCount().observe(this, folders -> {
adapter.submitList(folders);
if (!folders.isEmpty()) {
binding.foldersEmptyList.setVisibility(View.GONE);
} else {
binding.foldersEmptyList.setVisibility(View.VISIBLE);
}
});
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentFoldersBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.foldersList.setLayoutManager(new LinearLayoutManager(getContext()));
binding.foldersList.setAdapter(adapter);
}
public void editFolder(Folder folder) {
new MaterialDialog.Builder(getActivity())
.title(R.string.edit_folder)
.positiveText(R.string.validate)
.input(getString(R.string.folder), folder.getName(), false, (dialog, input) -> {
folder.setName(input.toString());
viewModel.updateFolder(folder)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> {
String message;
if (throwable instanceof ConflictException)
message = getString(R.string.folder_already_exists);
else if (throwable instanceof UnknownFormatException)
message = getString(R.string.folder_bad_format);
else if (throwable instanceof Resources.NotFoundException)
message = getString(R.string.folder_doesnt_exist);
else
message = getString(R.string.error_occured);
Utils.showSnackbar(binding.foldersRoot, message);
})
.subscribe();
})
.show();
}
public void deleteFolder(Folder folder) {
new MaterialDialog.Builder(getActivity())
.title(R.string.delete_folder)
.negativeText(R.string.cancel)
.positiveText(R.string.validate)
.onPositive((dialog, which) -> viewModel.deleteFolder(folder)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> {
String message;
if (throwable instanceof Resources.NotFoundException)
message = getString(R.string.folder_doesnt_exist);
else
message = throwable.getMessage();
Utils.showSnackbar(binding.foldersRoot, message);
})
.subscribe())
.show();
}
private void openFolderOptionsDialog(Folder folder) {
FolderOptionsDialogFragment fragment = FolderOptionsDialogFragment.Companion.newInstance(folder);
getChildFragmentManager()
.beginTransaction()
.add(fragment, "")
.commit();
}
}

View File

@ -1,429 +0,0 @@
package com.readrops.app.item;
import android.Manifest;
import android.app.DownloadManager;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.util.Log;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ShareCompat;
import com.afollestad.materialdialogs.MaterialDialog;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.readrops.api.utils.DateUtils;
import com.readrops.app.R;
import com.readrops.app.databinding.ActivityItemBinding;
import com.readrops.app.utils.GlideRequests;
import com.readrops.app.utils.PermissionManager;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import com.readrops.db.pojo.ItemWithFeed;
import org.koin.android.compat.ViewModelCompat;
import org.koin.java.KoinJavaComponent;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR;
import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL;
import static com.readrops.app.utils.ReadropsKeys.ITEM_ID;
import static com.readrops.app.utils.ReadropsKeys.WEB_URL;
public class ItemActivity extends AppCompatActivity {
private static final String TAG = ItemActivity.class.getSimpleName();
private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1;
private ActivityItemBinding binding;
private ItemViewModel viewModel;
private ItemWithFeed itemWithFeed;
private boolean appBarCollapsed;
private String urlToDownload;
private String imageTitle;
private boolean uiBinded;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityItemBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Intent intent = getIntent();
int itemId = intent.getIntExtra(ITEM_ID, 0);
String imageUrl = intent.getStringExtra(IMAGE_URL);
Account account = intent.getParcelableExtra(ACCOUNT);
setSupportActionBar(binding.collapsingLayoutToolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
registerForContextMenu(binding.itemWebview);
if (imageUrl == null) {
getSupportActionBar().setDisplayShowTitleEnabled(false);
binding.collapsingLayout.setTitleEnabled(false);
binding.collapsingLayoutScrim.setVisibility(View.GONE);
} else {
binding.appBarLayout.setExpanded(true);
binding.collapsingLayout.setTitleEnabled(true);
KoinJavaComponent.<GlideRequests>get(GlideRequests.class)
.load(imageUrl)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(binding.collapsingLayoutImage);
}
final TypedArray styledAttributes = getTheme().obtainStyledAttributes(
new int[]{android.R.attr.actionBarSize});
int actionBarSize = (int) styledAttributes.getDimension(0, 0);
styledAttributes.recycle();
binding.appBarLayout.addOnOffsetChangedListener(((appBarLayout1, i) -> {
appBarCollapsed = Math.abs(i) >= (binding.appBarLayout.getTotalScrollRange() -
actionBarSize - ((8 * binding.appBarLayout.getTotalScrollRange()) / 100));
invalidateOptionsMenu();
}));
viewModel = ViewModelCompat.getViewModel(this, ItemViewModel.class);
viewModel.setAccount(account);
viewModel.getItemById(itemId).observe(this, itemWithFeed1 -> {
if (!uiBinded) {
bindUI(itemWithFeed1);
uiBinded = true;
}
});
binding.activityItemFab.setOnClickListener(v -> openInNavigator());
binding.itemStarFab.setOnClickListener(v -> {
Item item = itemWithFeed.getItem();
if (item.isStarred()) {
binding.itemStarFab.setImageResource(R.drawable.ic_empty_star);
} else {
binding.itemStarFab.setImageResource(R.drawable.ic_star);
}
item.setStarred(!item.isStarred());
viewModel.setStarState(item)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> Utils.showSnackbar(binding.itemRoot, throwable.getMessage()))
.subscribe();
});
}
private void bindUI(ItemWithFeed itemWithFeed) {
this.itemWithFeed = itemWithFeed;
Item item = itemWithFeed.getItem();
if (item.isStarred()) {
binding.itemStarFab.setImageResource(R.drawable.ic_star);
}
binding.activityItemDate.setText(DateUtils.formattedDateTimeByLocal(item.getPubDate()));
if (item.getImageLink() == null)
binding.collapsingLayoutToolbar.setTitle(itemWithFeed.getFeedName());
else
binding.collapsingLayout.setTitle(itemWithFeed.getFeedName());
if (itemWithFeed.getFolder() != null) {
binding.collapsingLayoutToolbar.setSubtitle(itemWithFeed.getFolder().getName());
}
binding.activityItemTitle.setText(item.getTitle());
if (itemWithFeed.getBgColor() != 0) {
binding.activityItemTitle.setTextColor(itemWithFeed.getBgColor());
Utils.setDrawableColor(binding.activityItemDateLayout.getBackground(), itemWithFeed.getBgColor());
} else if (itemWithFeed.getColor() != 0) {
binding.activityItemTitle.setTextColor(itemWithFeed.getColor());
Utils.setDrawableColor(binding.activityItemDateLayout.getBackground(), itemWithFeed.getColor());
}
if (item.getAuthor() != null && !item.getAuthor().isEmpty()) {
binding.activityItemAuthor.setText(getString(R.string.by_author, item.getAuthor()));
binding.activityItemAuthor.setVisibility(View.VISIBLE);
}
if (item.getReadTime() > 0) {
int minutes = (int) Math.round(item.getReadTime());
if (minutes < 1)
binding.activityItemReadtime.setText(getResources().getString(R.string.read_time_lower_than_1));
else if (minutes > 1)
binding.activityItemReadtime.setText(getResources().getString(R.string.read_time, String.valueOf(minutes)));
else
binding.activityItemReadtime.setText(getResources().getString(R.string.read_time_one_minute));
binding.activityItemReadtimeLayout.setVisibility(View.VISIBLE);
}
if (itemWithFeed.getBgColor() != 0) {
binding.collapsingLayout.setBackgroundColor(itemWithFeed.getBgColor());
binding.collapsingLayout.setContentScrimColor(itemWithFeed.getBgColor());
binding.collapsingLayout.setStatusBarScrimColor(itemWithFeed.getBgColor());
getWindow().setStatusBarColor(itemWithFeed.getBgColor());
binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor()));
binding.itemStarFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor()));
} else if (itemWithFeed.getColor() != 0) {
binding.collapsingLayout.setBackgroundColor(itemWithFeed.getColor());
binding.collapsingLayout.setContentScrimColor(itemWithFeed.getColor());
binding.collapsingLayout.setStatusBarScrimColor(itemWithFeed.getColor());
getWindow().setStatusBarColor(itemWithFeed.getColor());
binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor()));
binding.itemStarFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor()));
}
binding.itemWebview.setItem(itemWithFeed);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.item_menu, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem item = menu.findItem(R.id.item_open);
item.setVisible(appBarCollapsed);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.item_share:
shareArticle();
return true;
case R.id.item_open:
openUrl();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onBackPressed() {
finish();
super.onBackPressed();
}
private void openUrl() {
int value = Integer.parseInt(SharedPreferencesManager.readString(
SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN));
switch (value) {
case 0:
openInNavigator();
break;
case 1:
openInWebView();
break;
default:
openInCustomTab();
break;
}
}
private void openInNavigator() {
Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(itemWithFeed.getItem().getLink()));
startActivity(urlIntent);
}
private void openInWebView() {
Intent intent = new Intent(this, WebViewActivity.class);
intent.putExtra(WEB_URL, itemWithFeed.getItem().getLink());
intent.putExtra(ACTION_BAR_COLOR, itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor());
startActivity(intent);
}
private void openInCustomTab() {
boolean darkTheme = Boolean.parseBoolean(SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME));
int color = itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor();
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder()
.addDefaultShareMenuItem()
.setToolbarColor(color)
.setSecondaryToolbarColor(color)
.setColorScheme(darkTheme ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT)
.enableUrlBarHiding()
.setShowTitle(true)
.build();
customTabsIntent.launchUrl(this, Uri.parse(itemWithFeed.getItem().getLink()));
}
private void shareArticle() {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, itemWithFeed.getItem().getTitle() + " - " + itemWithFeed.getItem().getLink());
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_article)));
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
WebView.HitTestResult hitTestResult = binding.itemWebview.getHitTestResult();
if (hitTestResult.getType() == WebView.HitTestResult.IMAGE_TYPE ||
hitTestResult.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
new MaterialDialog.Builder(this)
.title(R.string.image_options)
.items(R.array.image_options)
.itemsCallback((dialog, itemView, position, text) -> {
switch (position) {
case 0:
shareImage(hitTestResult.getExtra());
break;
case 1:
if (PermissionManager.isPermissionGranted(this, Manifest.permission.WRITE_EXTERNAL_STORAGE))
downloadImage(hitTestResult.getExtra());
else {
urlToDownload = hitTestResult.getExtra();
PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST, Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
break;
case 2:
urlToDownload = hitTestResult.getExtra();
String content = binding.itemWebview.getItemContent();
Pattern p = Pattern.compile("(<img.*src=\"" + urlToDownload + "\".*>)");
Matcher m = p.matcher(content);
if (m.matches()) {
Pattern p2 = Pattern.compile("<img.*(title|alt)=\"(.*?)\".*>");
Matcher m2 = p2.matcher(content);
if (m2.matches()) {
imageTitle = m2.group(2);
} else {
imageTitle = "";
}
}
new MaterialDialog.Builder(this)
.title(urlToDownload)
.content(imageTitle)
.show();
break;
default:
throw new IllegalStateException("Unexpected value: " + position);
}
})
.show();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadImage(urlToDownload);
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) {
Utils.showSnackBarWithAction(binding.itemRoot, getString(R.string.download_image_permission),
getString(R.string.try_again),
v -> PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST,
Manifest.permission.WRITE_EXTERNAL_STORAGE));
} else {
Utils.showSnackBarWithAction(binding.itemRoot, getString(R.string.download_image_permission),
getString(R.string.permissions), v -> {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", getPackageName(), null));
startActivity(intent);
});
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void downloadImage(String url) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url))
.setTitle(getString(R.string.download_image))
.setMimeType("image/png")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "image.png");
request.allowScanningByMediaScanner();
DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
downloadManager.enqueue(request);
}
private void shareImage(String url) {
KoinJavaComponent.<GlideRequests>get(GlideRequests.class)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.load(url)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
try {
Uri uri = viewModel.saveImageInCache(resource, ItemActivity.this);
Intent intent = ShareCompat.IntentBuilder.from(ItemActivity.this)
.setType("image/png")
.setStream(uri)
.setChooserTitle(R.string.share_image)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
// not useful
}
});
}
}

View File

@ -1,69 +0,0 @@
package com.readrops.app.item;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.readrops.app.repositories.ARepository;
import com.readrops.db.Database;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import com.readrops.db.pojo.ItemWithFeed;
import com.readrops.db.queries.ItemSelectionQueryBuilder;
import org.koin.core.parameter.ParametersHolderKt;
import org.koin.java.KoinJavaComponent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import io.reactivex.Completable;
public class ItemViewModel extends ViewModel {
private final Database database;
private Account account;
public ItemViewModel(@NonNull Database database) {
this.database = database;
}
public void setAccount(Account account) {
this.account = account;
}
public LiveData<ItemWithFeed> getItemById(int id) {
return database.itemDao().getItemById(ItemSelectionQueryBuilder.buildQuery(id,
account.getConfig().getUseSeparateState()));
}
public Completable setStarState(Item item) {
ARepository repository = KoinJavaComponent.get(ARepository.class, null,
() -> ParametersHolderKt.parametersOf(account));
return repository.setItemStarState(item);
}
public Uri saveImageInCache(Bitmap bitmap, Context context) throws IOException {
File imagesFolder = new File(context.getCacheDir().getAbsolutePath(), "images");
if (!imagesFolder.exists())
imagesFolder.mkdirs();
File image = new File(imagesFolder, "shared_image.png");
OutputStream stream = new FileOutputStream(image);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream);
stream.flush();
stream.close();
return FileProvider.getUriForFile(context, context.getPackageName(), image);
}
}

View File

@ -1,133 +0,0 @@
package com.readrops.app.item
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.webkit.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.readrops.app.R
import com.readrops.app.databinding.ActivityWebViewBinding
import com.readrops.app.utils.ReadropsKeys
import com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR
class WebViewActivity : AppCompatActivity() {
private lateinit var binding: ActivityWebViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWebViewBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.activityWebViewToolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
title = ""
val actionBarColor = intent.getIntExtra(ACTION_BAR_COLOR, ContextCompat.getColor(this, R.color.colorPrimary))
supportActionBar?.setBackgroundDrawable(ColorDrawable(actionBarColor))
setWebViewSettings()
with(binding) {
activityWebViewSwipe.setOnRefreshListener { binding.webView.reload() }
activityWebViewProgress.progressTintList = ColorStateList.valueOf(actionBarColor)
activityWebViewProgress.max = 100
val url: String = intent.getStringExtra(ReadropsKeys.WEB_URL)!!
webView.loadUrl(url)
}
}
@SuppressLint("SetJavaScriptEnabled")
fun setWebViewSettings() {
val settings: WebSettings = binding.webView.settings
settings.javaScriptEnabled = true
settings.setSupportZoom(true)
binding.webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
binding.webView.loadUrl(request?.url.toString())
return true
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
with(binding) {
activityWebViewSwipe.isRefreshing = false
activityWebViewProgress.progress = 0
activityWebViewProgress.visibility = View.VISIBLE
}
super.onPageStarted(view, url, favicon)
}
}
binding.webView.webChromeClient = object : WebChromeClient() {
override fun onReceivedTitle(view: WebView?, title: String?) {
setTitle(title)
supportActionBar?.subtitle = Uri.parse(view?.url).host
super.onReceivedTitle(view, title)
}
override fun onProgressChanged(view: WebView?, newProgress: Int) {
with(binding) {
activityWebViewProgress.progress = newProgress
if (newProgress == 100) activityWebViewProgress.visibility = View.GONE
}
super.onProgressChanged(view, newProgress)
}
}
}
override fun onBackPressed() {
if (binding.webView.canGoBack())
binding.webView.goBack()
else
super.onBackPressed()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
if (binding.webView.canGoBack())
binding.webView.goBack()
else
finish()
return true
}
R.id.web_view_refresh -> {
binding.webView.reload()
}
R.id.web_view_share -> {
shareLink()
}
}
return super.onOptionsItemSelected(item)
}
private fun shareLink() {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, binding.webView.url.toString())
}
startActivity(Intent.createChooser(intent, getString(R.string.share_url)))
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.webview_menu, menu)
return true
}
}

View File

@ -1,412 +0,0 @@
package com.readrops.app.itemslist;
import static com.readrops.app.utils.Utils.drawableWithColor;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.mikepenz.fastadapter.FastAdapter;
import com.mikepenz.fastadapter.expandable.ExpandableExtension;
import com.mikepenz.fastadapter.listeners.ClickEventHook;
import com.mikepenz.fastadapter.select.SelectExtension;
import com.mikepenz.materialdrawer.AccountHeader;
import com.mikepenz.materialdrawer.AccountHeaderBuilder;
import com.mikepenz.materialdrawer.Drawer;
import com.mikepenz.materialdrawer.DrawerBuilder;
import com.mikepenz.materialdrawer.holder.ImageHolder;
import com.mikepenz.materialdrawer.model.DividerDrawerItem;
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem;
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
import com.readrops.app.R;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.customviews.CustomExpandableBadgeDrawerItem;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DrawerManager {
public static final int ARTICLES_ITEM_ID = -5;
public static final int READ_LATER_ID = -6;
public static final int STARS_ID = -10;
public static final int ADD_ACCOUNT_ID = -4;
public static final int ABOUT_ID = -7;
public static final int SETTINGS_ID = -8;
public static final int ACCOUNT_SETTINGS_ID = -9;
private Activity activity;
private Toolbar toolbar;
private Drawer drawer;
private FastAdapter<IDrawerItem> adapter;
private AccountHeader header;
private Drawer.OnDrawerItemClickListener listener;
private AccountHeader.OnAccountHeaderListener headerListener;
public DrawerManager(Activity activity, Toolbar toolbar, Drawer.OnDrawerItemClickListener listener) {
this.activity = activity;
this.listener = listener;
this.toolbar = toolbar;
}
public void setHeaderListener(AccountHeader.OnAccountHeaderListener headerListener) {
this.headerListener = headerListener;
}
public Drawer buildDrawer(List<Account> accounts, int currentAccountId) {
createAccountHeader(accounts, currentAccountId);
drawer = new DrawerBuilder()
.withActivity(activity)
.withToolbar(toolbar)
.withAccountHeader(header)
.withSelectedItem(DrawerManager.ARTICLES_ITEM_ID)
.withOnDrawerItemClickListener(listener)
.build();
adapter = drawer.getAdapter();
buildFastAdapter();
addDefaultPlaces();
return drawer;
}
public void buildFastAdapter() {
// Folder click
adapter.withEventHook(new ClickEventHook<IDrawerItem>() {
@Override
public void onClick(@NonNull View v, int position, @NonNull FastAdapter<IDrawerItem> fastAdapter, @NonNull IDrawerItem item) {
SelectExtension selectExtension = adapter.getExtension(SelectExtension.class);
selectExtension.deselect(selectExtension.getSelections());
if (!item.isSelected()) {
selectExtension.select(position);
}
listener.onItemClick(v, position, item);
}
@Override
public List<View> onBindMany(@NonNull RecyclerView.ViewHolder viewHolder) {
if (viewHolder instanceof CustomExpandableBadgeDrawerItem.ViewHolder) {
CustomExpandableBadgeDrawerItem.ViewHolder expandableViewHolder = (CustomExpandableBadgeDrawerItem.ViewHolder) viewHolder;
return Arrays.asList(new View[]{
expandableViewHolder.itemView.findViewById(R.id.expandable_item_container),
expandableViewHolder.itemView.findViewById(R.id.material_drawer_icon),
expandableViewHolder.itemView.findViewById(R.id.material_drawer_name),
expandableViewHolder.itemView.findViewById(R.id.material_drawer_description)
}.clone());
} else {
return Collections.emptyList();
}
}
});
// Expandable click
adapter.withEventHook(new ClickEventHook<IDrawerItem>() {
@Override
public void onClick(@NonNull View v, int position, @NonNull FastAdapter<IDrawerItem> fastAdapter, @NonNull IDrawerItem item) {
ExpandableExtension expandableExtension = adapter.getExtension(ExpandableExtension.class);
expandableExtension.toggleExpandable(position);
}
@Override
public List<View> onBindMany(@NonNull RecyclerView.ViewHolder viewHolder) {
if (viewHolder instanceof CustomExpandableBadgeDrawerItem.ViewHolder) {
CustomExpandableBadgeDrawerItem.ViewHolder expandableViewHolder = (CustomExpandableBadgeDrawerItem.ViewHolder) viewHolder;
return Arrays.asList(new View[]{
expandableViewHolder.badge,
expandableViewHolder.badgeContainer,
expandableViewHolder.arrow,
expandableViewHolder.itemView.findViewById(R.id.material_drawer_arrow_container)
}.clone());
} else {
return Collections.emptyList();
}
}
});
}
public void updateDrawer(Map<Folder, List<Feed>> folderListMap) {
drawer.removeAllItems();
drawer.removeAllStickyFooterItems();
addDefaultPlaces();
Map<SecondaryDrawerItem, Feed> feedsWithoutFolder = new HashMap<>();
boolean hideFeeds = SharedPreferencesManager
.readBoolean(SharedPreferencesManager.SharedPrefKey.HIDE_FEEDS);
for (Map.Entry<Folder, List<Feed>> entry : folderListMap.entrySet()) {
Folder folder = entry.getKey();
if (folder != null) {
CustomExpandableBadgeDrawerItem badgeDrawerItem = new CustomExpandableBadgeDrawerItem()
.withIdentifier(folder.getId() * 1000L) // to avoid any id conflict with other items
.withName(folder.getName())
.withIcon(R.drawable.ic_folder_grey);
List<IDrawerItem> secondaryDrawerItems = new ArrayList<>();
int expandableUnreadCount = 0;
for (Feed feed : entry.getValue()) {
expandableUnreadCount += feed.getUnreadCount();
SecondaryDrawerItem secondaryDrawerItem = createSecondaryItem(feed);
if (hideFeeds) {
if (feed.getUnreadCount() > 0) {
secondaryDrawerItems.add(secondaryDrawerItem);
}
} else {
secondaryDrawerItems.add(secondaryDrawerItem);
}
loadItemIcon(secondaryDrawerItem, feed);
}
boolean showItem;
if (hideFeeds) {
showItem = expandableUnreadCount > 0;
} else {
showItem = true;
}
if (!secondaryDrawerItems.isEmpty() && showItem) {
badgeDrawerItem.withSubItems(secondaryDrawerItems);
badgeDrawerItem.withBadge(String.valueOf(expandableUnreadCount));
drawer.addItem(badgeDrawerItem);
}
} else { // no folder case, items to add after the folders
for (Feed feed : folderListMap.get(folder)) {
SecondaryDrawerItem secondaryItem = createSecondaryItem(feed);
feedsWithoutFolder.put(secondaryItem, feed);
}
}
}
// work-around as MaterialDrawer doesn't accept an item list
for (Map.Entry<SecondaryDrawerItem, Feed> entry : feedsWithoutFolder.entrySet()) {
drawer.addItem(entry.getKey());
loadItemIcon(entry.getKey(), entry.getValue());
}
}
private void createAccountHeader(List<Account> accounts, int currentAccountId) {
ProfileDrawerItem[] profileItems = new ProfileDrawerItem[accounts.size()];
for (int i = 0; i < accounts.size(); i++) {
Account account = accounts.get(i);
// if currentAccount > 0, it means that the current account is no longer
if (account.isCurrentAccount() && currentAccountId == 0)
currentAccountId = account.getId();
ProfileDrawerItem profileItem = createProfileItem(account);
profileItems[i] = profileItem;
}
header = new AccountHeaderBuilder()
.withActivity(activity)
.addProfiles(profileItems)
.withDividerBelowHeader(false)
.withAlternativeProfileHeaderSwitching(true)
.withCurrentProfileHiddenInList(true)
.withTextColorRes(R.color.colorBackground)
.withHeaderBackground(R.drawable.header_background)
.withHeaderBackgroundScaleType(ImageView.ScaleType.CENTER_CROP)
.withOnAccountHeaderListener(headerListener)
.build();
addProfileSettingItems();
header.setActiveProfile(currentAccountId);
}
private ProfileDrawerItem createProfileItem(Account account) {
return new ProfileDrawerItem()
.withIcon(account.getAccountType().getIconRes())
.withName(account.getDisplayedName())
.withEmail(account.getAccountName())
.withIdentifier(account.getId());
}
private SecondaryDrawerItem createSecondaryItem(Feed feed) {
int color = feed.getTextColor();
return new SecondaryDrawerItem()
.withName(feed.getName())
.withBadge(String.valueOf(feed.getUnreadCount()))
.withIcon(color != 0 ? drawableWithColor(color) : drawableWithColor(activity.getResources().getColor(R.color.colorPrimary)))
.withIdentifier(feed.getId());
}
private void loadItemIcon(SecondaryDrawerItem secondaryDrawerItem, Feed feed) {
Glide.with(activity)
.load(feed.getIconUrl())
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(new CustomTarget<Drawable>() {
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
drawer.updateIcon(secondaryDrawerItem.getIdentifier(), new ImageHolder(resource));
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
// no need of this method
}
});
}
private void addDefaultPlaces() {
PrimaryDrawerItem articles = new PrimaryDrawerItem()
.withName(R.string.articles)
.withIcon(R.drawable.ic_rss_feed_grey)
.withSelectable(true)
.withIdentifier(ARTICLES_ITEM_ID);
PrimaryDrawerItem toReadLater = new PrimaryDrawerItem()
.withName(R.string.read_later)
.withIcon(R.drawable.ic_read_later)
.withSelectable(true)
.withIdentifier(READ_LATER_ID);
PrimaryDrawerItem favorites = new PrimaryDrawerItem()
.withName(R.string.favorites)
.withIcon(R.drawable.ic_star)
.withSelectable(true)
.withIdentifier(STARS_ID);
PrimaryDrawerItem aboutItem = new PrimaryDrawerItem()
.withName(R.string.about)
.withIcon(R.drawable.ic_about_grey)
.withSelectable(false)
.withIdentifier(ABOUT_ID);
PrimaryDrawerItem settingsItem = new PrimaryDrawerItem()
.withName(R.string.settings)
.withIcon(R.drawable.ic_settings)
.withSelectable(false)
.withIdentifier(SETTINGS_ID);
drawer.addStickyFooterItem(settingsItem);
drawer.addStickyFooterItem(aboutItem);
drawer.addItem(articles);
drawer.addItem(favorites);
drawer.addItem(toReadLater);
drawer.addItem(new DividerDrawerItem());
}
private void addProfileSettingItems() {
ProfileSettingDrawerItem accountSettingsItem = new ProfileSettingDrawerItem()
.withName(R.string.account_settings)
.withIcon(R.drawable.ic_settings)
.withIdentifier(ACCOUNT_SETTINGS_ID);
ProfileSettingDrawerItem addAccountSettingsItem = new ProfileSettingDrawerItem()
.withName(R.string.add_account)
.withIcon(R.drawable.ic_add_account_grey)
.withIdentifier(ADD_ACCOUNT_ID);
header.addProfiles(accountSettingsItem, addAccountSettingsItem);
}
public void addAccount(Account account, boolean currentProfile) {
ProfileDrawerItem profileItem = createProfileItem(account);
header.addProfiles(profileItem);
if (currentProfile)
header.setActiveProfile(profileItem.getIdentifier());
}
public void setAccount(int accountId) {
header.setActiveProfile(accountId);
}
public void updateHeader(List<Account> accounts) {
header.clear();
addProfileSettingItems();
for (Account account : accounts) {
addAccount(account, account.isCurrentAccount());
}
}
public int getNumberOfProfiles() {
List<IProfile> profiles = header.getProfiles();
int number = 0;
for (IProfile profile : profiles) {
if (profile instanceof ProfileDrawerItem)
number++;
}
return number;
}
public void resetItems() {
drawer.removeAllItems();
drawer.removeAllStickyFooterItems();
addDefaultPlaces();
}
public void disableAccountSelection() {
List<IProfile> profiles = header.getProfiles();
for (IProfile profile : profiles) {
if (profile.getIdentifier() != header.getActiveProfile().getIdentifier() && !(profile instanceof ProfileSettingDrawerItem)) {
profile.withSelectable(false);
header.updateProfile(profile);
}
}
}
public void enableAccountSelection() {
List<IProfile> profiles = header.getProfiles();
for (IProfile profile : profiles) {
if (profile.getIdentifier() != header.getActiveProfile().getIdentifier() && !(profile instanceof ProfileSettingDrawerItem)) {
profile.withSelectable(true);
header.updateProfile(profile);
}
}
}
public void setDrawerSelection(long identifier) {
drawer.setSelection(identifier);
}
public long getCurrentSelection() {
return drawer.getCurrentSelection();
}
}

View File

@ -1,868 +0,0 @@
package com.readrops.app.itemslist;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY;
import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL;
import static com.readrops.app.utils.ReadropsKeys.ITEM_ID;
import static com.readrops.app.utils.ReadropsKeys.SETTINGS;
import static com.readrops.app.utils.ReadropsKeys.SYNCING;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.paging.PagedList;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.afollestad.materialdialogs.MaterialDialog;
import com.bumptech.glide.Glide;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.mikepenz.aboutlibraries.Libs;
import com.mikepenz.aboutlibraries.LibsBuilder;
import com.mikepenz.aboutlibraries.LibsConfiguration;
import com.mikepenz.aboutlibraries.entity.Library;
import com.mikepenz.materialdrawer.Drawer;
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import com.readrops.app.R;
import com.readrops.app.account.AccountTypeListActivity;
import com.readrops.app.account.AccountViewModel;
import com.readrops.app.addfeed.AddFeedActivity;
import com.readrops.app.databinding.ActivityMainBinding;
import com.readrops.app.item.ItemActivity;
import com.readrops.app.settings.SettingsActivity;
import com.readrops.app.utils.GlideRequests;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.app.utils.customviews.CustomExpandableBadgeDrawerItem;
import com.readrops.app.utils.customviews.ReadropsItemTouchCallback;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import com.readrops.db.filters.MainFilter;
import com.readrops.db.filters.ListSortType;
import com.readrops.db.pojo.ItemWithFeed;
import org.jetbrains.annotations.NotNull;
import org.koin.android.compat.ViewModelCompat;
import org.koin.java.KoinJavaComponent;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import io.reactivex.CompletableObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener,
ReadropsItemTouchCallback.SwipeCallback, ActionMode.Callback {
public static final String TAG = MainActivity.class.getSimpleName();
public static final int ADD_FEED_REQUEST = 1;
public static final int MANAGE_ACCOUNT_REQUEST = 2;
public static final int ITEM_REQUEST = 3;
public static final int ADD_ACCOUNT_REQUEST = 4;
public static final int SETTINGS_REQUEST = 5;
private ActivityMainBinding binding;
private MainItemListAdapter adapter;
private Drawer drawer;
private PagedList<ItemWithFeed> allItems;
private MainViewModel viewModel;
private DrawerManager drawerManager;
private int feedCount;
private int feedNb;
private boolean scrollToTop;
private boolean allItemsSelected;
private boolean updating;
private ActionMode actionMode;
private Disposable syncDisposable;
private ItemWithFeed selectedItemWithFeed;
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme_NoActionBar);
super.onCreate(savedInstanceState);
// surely a better way to do this, but hopefully this code will be replaced with jetpack compose
AccountViewModel accountViewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
int accountCount = accountViewModel.getAccountCount()
.subscribeOn(Schedulers.io())
.blockingGet();
if (accountCount == 0) {
Intent intent = new Intent(getApplicationContext(), AccountTypeListActivity.class);
startActivity(intent);
finish();
}
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbarMain);
binding.swipeRefreshLayout.setOnRefreshListener(this);
feedCount = 0;
initRecyclerView();
viewModel = ViewModelCompat.getViewModel(this, MainViewModel.class);
viewModel.getItemsWithFeed().observe(this, itemWithFeeds -> {
allItems = itemWithFeeds;
if (!itemWithFeeds.isEmpty())
binding.emptyListLayout.setVisibility(View.GONE);
else
binding.emptyListLayout.setVisibility(View.VISIBLE);
if (!binding.swipeRefreshLayout.isRefreshing())
adapter.submitList(itemWithFeeds);
});
drawerManager = new DrawerManager(this, binding.toolbarMain, (view, position, drawerItem) -> {
handleDrawerClick(drawerItem);
return true;
});
drawerManager.setHeaderListener((view, profile, current) -> {
if (!current) {
int id = (int) profile.getIdentifier();
switch (id) {
case DrawerManager.ADD_ACCOUNT_ID:
Intent intent = new Intent(this, AccountTypeListActivity.class);
intent.putExtra(FROM_MAIN_ACTIVITY, true);
startActivityForResult(intent, ADD_ACCOUNT_REQUEST);
break;
case DrawerManager.ACCOUNT_SETTINGS_ID:
Intent intent1 = new Intent(this, SettingsActivity.class);
intent1.putExtra(SETTINGS,
SettingsActivity.SettingsKey.ACCOUNT_SETTINGS.ordinal());
intent1.putExtra(ACCOUNT, viewModel.getCurrentAccount());
startActivity(intent1);
break;
default:
if (!updating) {
viewModel.setCurrentAccount(id);
updateDrawerFeeds();
}
break;
}
} else {
Intent intent = new Intent(this, SettingsActivity.class);
intent.putExtra(SETTINGS,
SettingsActivity.SettingsKey.ACCOUNT_SETTINGS.ordinal());
intent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
startActivityForResult(intent, MANAGE_ACCOUNT_REQUEST);
}
return true;
});
Account currentAccount = getIntent().getParcelableExtra(ACCOUNT);
WeakReference<Account> accountWeakReference = new WeakReference<>(currentAccount);
viewModel.getAllAccounts().observe(this, accounts -> {
getAccountCredentials(accounts);
viewModel.setAccounts(accounts);
// the activity was just opened
if (drawer == null) {
int currentAccountId = 0;
if (getIntent().hasExtra(ACCOUNT_ID)) { // coming from a notification
currentAccountId = getIntent().getIntExtra(ACCOUNT_ID, 1);
viewModel.setCurrentAccount(currentAccountId);
}
drawer = drawerManager.buildDrawer(accounts, currentAccountId);
drawer.setSelection(DrawerManager.ARTICLES_ITEM_ID);
updateDrawerFeeds();
openItemActivity(getIntent());
} else if (accounts.size() < drawerManager.getNumberOfProfiles() && !accounts.isEmpty()) {
drawerManager.updateHeader(accounts);
updateDrawerFeeds();
} else if (accounts.isEmpty()) {
Intent intent = new Intent(this, AccountTypeListActivity.class);
startActivity(intent);
finish();
}
if (accountWeakReference.get() != null && !accountWeakReference.get().isLocal()) {
binding.swipeRefreshLayout.setRefreshing(true);
onRefresh();
accountWeakReference.clear();
} else if (currentAccount == null && savedInstanceState != null && savedInstanceState.getBoolean(SYNCING)) {
binding.swipeRefreshLayout.setRefreshing(true);
onRefresh();
savedInstanceState.clear();
}
});
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
openItemActivity(intent);
}
private void openItemActivity(Intent intent) {
if (intent.hasExtra(ITEM_ID) && intent.hasExtra(IMAGE_URL)) {
Intent itemIntent = new Intent(this, ItemActivity.class);
itemIntent.putExtras(intent);
itemIntent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
startActivity(itemIntent);
Item item = new Item();
item.setId(intent.getIntExtra(ITEM_ID, 0));
item.setRead(true);
viewModel.setItemReadState(item)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
.subscribe();
}
}
private void handleDrawerClick(IDrawerItem drawerItem) {
if (drawerItem instanceof PrimaryDrawerItem) {
drawer.closeDrawer();
int id = (int) drawerItem.getIdentifier();
switch (id) {
default:
case DrawerManager.ARTICLES_ITEM_ID:
viewModel.setFilterType(MainFilter.ALL);
scrollToTop = true;
viewModel.invalidate();
setTitle(R.string.articles);
break;
case DrawerManager.READ_LATER_ID:
//viewModel.setFilterType(FilterType.READ_IT_LATER_FILTER);
viewModel.invalidate();
setTitle(R.string.read_later);
break;
case DrawerManager.STARS_ID:
viewModel.setFilterType(MainFilter.STARS);
viewModel.invalidate();
setTitle(R.string.favorites);
break;
case DrawerManager.ABOUT_ID:
startAboutActivity();
break;
case DrawerManager.SETTINGS_ID:
Intent intent = new Intent(getApplication(), SettingsActivity.class);
intent.putExtra(SETTINGS,
SettingsActivity.SettingsKey.SETTINGS.ordinal());
startActivityForResult(intent, SETTINGS_REQUEST);
break;
}
} else if (drawerItem instanceof SecondaryDrawerItem) {
drawer.closeDrawer();
viewModel.setFilterFeedId((int) drawerItem.getIdentifier());
viewModel.setFilterType(MainFilter.ALL);
viewModel.invalidate();
setTitle(((SecondaryDrawerItem) drawerItem).getName().getText());
} else if (drawerItem instanceof CustomExpandableBadgeDrawerItem) {
drawer.closeDrawer();
viewModel.setFilerFolderId((int) (drawerItem.getIdentifier() / 1000));
viewModel.setFilterType(MainFilter.ALL);
viewModel.invalidate();
setTitle(((CustomExpandableBadgeDrawerItem) drawerItem).getName().getText());
}
}
private void updateDrawerFeeds() {
viewModel.getFoldersWithFeeds()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableSingleObserver<Map<Folder, List<Feed>>>() {
@Override
public void onSuccess(Map<Folder, List<Feed>> folderListHashMap) {
drawerManager.updateDrawer(folderListHashMap);
}
@Override
public void onError(Throwable e) {
Utils.showSnackbar(binding.mainRoot, e.getMessage());
}
});
}
@Override
public void onBackPressed() {
if (drawer.isDrawerOpen())
drawer.closeDrawer();
else
super.onBackPressed();
}
private void initRecyclerView() {
ViewPreloadSizeProvider preloadSizeProvider = new ViewPreloadSizeProvider();
adapter = new MainItemListAdapter(KoinJavaComponent.get(GlideRequests.class), preloadSizeProvider);
adapter.setOnItemClickListener(new MainItemListAdapter.OnItemClickListener() {
@Override
public void onItemClick(ItemWithFeed itemWithFeed, int position) {
if (actionMode == null) {
Intent intent = new Intent(getApplicationContext(), ItemActivity.class);
intent.putExtra(ITEM_ID, itemWithFeed.getItem().getId());
intent.putExtra(IMAGE_URL, itemWithFeed.getItem().getImageLink());
intent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
startActivityForResult(intent, ITEM_REQUEST);
itemWithFeed.getItem().setRead(true);
viewModel.setItemReadState(itemWithFeed)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
.subscribe();
adapter.notifyItemChanged(position, itemWithFeed);
updateDrawerFeeds();
} else {
adapter.toggleSelection(position);
int selectionSize = adapter.getSelection().size();
if (selectionSize > 0)
actionMode.setTitle(String.valueOf(selectionSize));
else
actionMode.finish();
}
}
@Override
public void onItemLongClick(ItemWithFeed itemWithFeed, int position) {
if (actionMode != null || binding.swipeRefreshLayout.isRefreshing())
return;
selectedItemWithFeed = itemWithFeed;
adapter.toggleSelection(position);
actionMode = startActionMode(MainActivity.this);
actionMode.setTitle(String.valueOf(adapter.getSelection().size()));
}
});
RecyclerViewPreloader<String> preloader = new RecyclerViewPreloader<String>(Glide.with(this), adapter, preloadSizeProvider, 10);
binding.itemsRecyclerView.addOnScrollListener(preloader);
binding.itemsRecyclerView.addRecyclerListener(viewHolder -> {
MainItemListAdapter.ItemViewHolder vh = (MainItemListAdapter.ItemViewHolder) viewHolder;
KoinJavaComponent.<GlideRequests>get(GlideRequests.class).clear(vh.getItemImage());
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
binding.itemsRecyclerView.setLayoutManager(layoutManager);
DividerItemDecoration decoration = new DividerItemDecoration(this, layoutManager.getOrientation());
binding.itemsRecyclerView.addItemDecoration(decoration);
binding.itemsRecyclerView.setAdapter(adapter);
Drawable readLater = ContextCompat.getDrawable(this, R.drawable.ic_read_later).mutate();
DrawableCompat.setTint(readLater, ContextCompat.getColor(this, android.R.color.white));
new ItemTouchHelper(new ReadropsItemTouchCallback(this,
new ReadropsItemTouchCallback.Config.Builder()
.swipeDirs(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)
.swipeCallback(this)
.leftDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read_later, readLater)
.rightDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read, null)
.build()))
.attachToRecyclerView(binding.itemsRecyclerView);
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
if (scrollToTop) {
binding.itemsRecyclerView.scrollToPosition(0);
scrollToTop = false;
}
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
if (scrollToTop) {
binding.itemsRecyclerView.scrollToPosition(0);
scrollToTop = false;
} else
super.onItemRangeMoved(fromPosition, toPosition, itemCount);
}
});
binding.itemsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy > 0) {
binding.addFeedFab.hide();
} else {
binding.addFeedFab.show();
}
int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
if (firstVisibleItemPosition - 2 >= 0) {
Item item = adapter.getItemWithFeed(firstVisibleItemPosition - 2).getItem();
// Might be better to have a global variable updated when going back from settings
if (!item.isRead() && SharedPreferencesManager.readBoolean(SharedPreferencesManager
.SharedPrefKey.MARK_ITEMS_READ_ON_SCROLL)) {
item.setRead(!item.isRead());
viewModel.setItemReadState(item)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
.subscribe();
}
}
}
});
}
@Override
public void onSwipe(@NotNull RecyclerView.ViewHolder viewHolder, int direction) {
Item item = adapter.getItemWithFeed(viewHolder.getBindingAdapterPosition()).getItem();
if (direction == ItemTouchHelper.LEFT) { // set item read state
item.setRead(!item.isRead());
viewModel.setItemReadState(item)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
.subscribe();
} else { // set item read it later state
item.setReadItLater(!item.isReadItLater());
viewModel.setItemReadItLater(item.isReadItLater(), item.getId())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
.subscribe();
}
adapter.notifyItemChanged(viewHolder.getBindingAdapterPosition());
}
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
binding.swipeRefreshLayout.setEnabled(false);
actionMode.getMenuInflater().inflate(R.menu.item_list_contextual_menu, menu);
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.primary_dark));
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
menu.findItem(R.id.item_mark_read).setVisible(!selectedItemWithFeed.getItem().isRead());
menu.findItem(R.id.item_mark_unread).setVisible(selectedItemWithFeed.getItem().isRead());
return true;
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
int itemId = menuItem.getItemId();
if (itemId == R.id.item_mark_read) {
setReadState(true);
} else if (itemId == R.id.item_mark_unread) {
setReadState(false);
} else if (itemId == R.id.item_select_all) {
if (allItemsSelected) {
adapter.unselectAll();
allItemsSelected = false;
actionMode.finish();
} else {
adapter.selectAll();
allItemsSelected = true;
}
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
mode.finish();
actionMode = null;
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
binding.swipeRefreshLayout.setEnabled(true);
adapter.clearSelection();
}
private void setReadState(boolean read) {
if (allItemsSelected) {
viewModel.setAllItemsReadState(read)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
.subscribe();
allItemsSelected = false;
} else {
viewModel.setItemsReadState(adapter.getSelectedItems(), read)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
.subscribe();
}
adapter.updateSelection(read);
updateDrawerFeeds();
actionMode.finish();
}
@Override
public void onRefresh() {
Log.d(TAG, "syncing started");
drawerManager.disableAccountSelection();
updating = true;
if (viewModel.isAccountLocal()) {
viewModel.getFeedCount()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableSingleObserver<Integer>() {
@Override
public void onSuccess(@NonNull Integer integer) {
feedNb = integer;
sync(null);
}
@Override
public void onError(@NonNull Throwable e) {
Utils.showSnackbar(binding.mainRoot, e.getMessage());
}
});
} else {
sync(null);
}
}
public void openAddFeedActivity(View view) {
Intent intent = new Intent(this, AddFeedActivity.class);
intent.putExtra(ACCOUNT_ID, viewModel.getCurrentAccount().getId());
startActivityForResult(intent, ADD_FEED_REQUEST);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == ADD_FEED_REQUEST && resultCode == RESULT_OK && data != null) {
List<Feed> feeds = data.getParcelableArrayListExtra(FEEDS);
if (feeds != null && !feeds.isEmpty() && viewModel.isAccountLocal()) {
binding.swipeRefreshLayout.setRefreshing(true);
feedNb = feeds.size();
sync(feeds);
}
} else if (requestCode == MANAGE_ACCOUNT_REQUEST || requestCode == SETTINGS_REQUEST) {
updateDrawerFeeds();
} else if (requestCode == ADD_ACCOUNT_REQUEST && resultCode == RESULT_OK && data != null) {
Account newAccount = data.getParcelableExtra(ACCOUNT);
if (newAccount != null) {
// get credentials before creating the repository
if (!newAccount.isLocal()) {
getAccountCredentials(Collections.singletonList(newAccount));
}
viewModel.addAccount(newAccount);
adapter.clearData();
// start syncing only if the account is not local
if (!viewModel.isAccountLocal()) {
binding.swipeRefreshLayout.setRefreshing(true);
onRefresh();
}
drawerManager.resetItems();
drawerManager.addAccount(newAccount, true);
}
}
super.onActivityResult(requestCode, resultCode, data);
}
private void sync(@Nullable List<Feed> feeds) {
viewModel.sync(feeds, feed -> {
if (viewModel.isAccountLocal() && feedNb > 0) {
binding.syncProgressTextView.setText(getString(R.string.updating_feed, feed.getName()));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
binding.syncProgressBar.setProgress((feedCount * 100) / feedNb, true);
} else
binding.syncProgressBar.setProgress((feedCount * 100) / feedNb);
}
feedCount++;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new CompletableObserver() {
@Override
public void onSubscribe(@NonNull Disposable d) {
syncDisposable = d;
if (viewModel.isAccountLocal() && feedNb > 0) {
binding.syncProgressLayout.setVisibility(View.VISIBLE);
binding.syncProgressBar.setProgress(0);
}
}
@Override
public void onComplete() {
viewModel.invalidate();
if (viewModel.isAccountLocal() && feedNb > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
binding.syncProgressBar.setProgress(100, true);
else
binding.syncProgressBar.setProgress(100);
binding.syncProgressLayout.setVisibility(View.GONE);
}
binding.swipeRefreshLayout.setRefreshing(false);
scrollToTop = true;
adapter.submitList(allItems);
drawerManager.enableAccountSelection();
updateDrawerFeeds(); // update drawer after syncing feeds
updating = false;
}
@Override
public void onError(@NonNull Throwable e) {
binding.swipeRefreshLayout.setRefreshing(false);
binding.syncProgressLayout.setVisibility(View.GONE);
Utils.showSnackbar(binding.mainRoot, e.getMessage());
drawerManager.enableAccountSelection();
updating = false;
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.item_list_menu, menu);
MenuItem articlesItem = menu.findItem(R.id.item_filter_read_items);
articlesItem.setChecked(viewModel.showReadItems());
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.item_filter_read_items) {
if (item.isChecked()) {
item.setChecked(false);
viewModel.setShowReadItems(false);
SharedPreferencesManager.writeValue(
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, false);
} else {
item.setChecked(true);
viewModel.setShowReadItems(true);
SharedPreferencesManager.writeValue(
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, true);
}
viewModel.invalidate();
return true;
} else if (itemId == R.id.item_sort) {
displayFilterDialog();
return true;
} else if (itemId == R.id.start_sync) {
if (!viewModel.isAccountLocal()) {
binding.swipeRefreshLayout.setRefreshing(true);
}
onRefresh();
}
return super.onOptionsItemSelected(item);
}
private void displayFilterDialog() {
int index = viewModel.getSortType() == ListSortType.OLDEST_TO_NEWEST ? 1 : 0;
new MaterialDialog.Builder(this)
.title(R.string.filter)
.items(R.array.filter_items)
.itemsCallbackSingleChoice(index, (dialog, itemView, which, text) -> {
String[] items = getResources().getStringArray(R.array.filter_items);
if (text.toString().equals(items[0]))
viewModel.setSortType(ListSortType.NEWEST_TO_OLDEST);
else
viewModel.setSortType(ListSortType.OLDEST_TO_NEWEST);
scrollToTop = true;
viewModel.invalidate();
return true;
})
.show();
}
private void getAccountCredentials(List<Account> accounts) {
for (Account account : accounts) {
if (account.getLogin() == null)
account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
if (account.getPassword() == null)
account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
}
}
private void startAboutActivity() {
Libs.ActivityStyle activityStyle;
int uiMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (uiMode == Configuration.UI_MODE_NIGHT_YES) {
activityStyle = Libs.ActivityStyle.DARK;
} else {
activityStyle = Libs.ActivityStyle.LIGHT_DARK_TOOLBAR;
}
new LibsBuilder()
.withAboutIconShown(true)
.withAboutVersionShown(true)
.withAboutAppName(getString(R.string.app_name))
.withAboutDescription(getString(R.string.app_description))
.withLicenseShown(true)
.withLicenseDialog(false)
.withActivityTitle(getString(R.string.about))
.withActivityStyle(activityStyle)
.withFields(R.string.class.getFields())
.withAboutSpecial1(getString(R.string.source_code))
.withAboutSpecial2(getString(R.string.changelog))
.withListener(new LibsConfiguration.LibsListener() {
@Override
public void onIconClicked(View v) {
}
@Override
public boolean onLibraryAuthorClicked(View v, Library library) {
return false;
}
@Override
public boolean onLibraryContentClicked(View v, Library library) {
return false;
}
@Override
public boolean onLibraryBottomClicked(View v, Library library) {
return false;
}
@Override
public boolean onExtraClicked(View v, Libs.SpecialButton specialButton) {
if (v.getId() == R.id.aboutSpecial1) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.app_url))));
} else if (v.getId() == R.id.aboutSpecial2) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.changelog_url))));
}
return false;
}
@Override
public boolean onIconLongClicked(View v) {
return false;
}
@Override
public boolean onLibraryAuthorLongClicked(View v, Library library) {
return false;
}
@Override
public boolean onLibraryContentLongClicked(View v, Library library) {
return false;
}
@Override
public boolean onLibraryBottomLongClicked(View v, Library library) {
return false;
}
})
.start(this);
}
@Override
protected void onDestroy() {
if (syncDisposable != null && !syncDisposable.isDisposed())
syncDisposable.dispose();
super.onDestroy();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
if (binding.swipeRefreshLayout.isRefreshing())
outState.putBoolean(SYNCING, true);
super.onSaveInstanceState(outState);
}
}

View File

@ -1,372 +0,0 @@
package com.readrops.app.itemslist;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.transition.DrawableCrossFadeFactory;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.readrops.api.utils.DateUtils;
import com.readrops.app.R;
import com.readrops.app.databinding.ListItemBinding;
import com.readrops.app.utils.GlideRequests;
import com.readrops.app.utils.Utils;
import com.readrops.db.entities.Item;
import com.readrops.db.pojo.ItemWithFeed;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItemListAdapter.ItemViewHolder> implements ListPreloader.PreloadModelProvider<String> {
private GlideRequests glideRequests;
private OnItemClickListener listener;
private ViewPreloadSizeProvider preloadSizeProvider;
private LinkedHashSet<Integer> selection;
public MainItemListAdapter(GlideRequests glideRequests, ViewPreloadSizeProvider preloadSizeProvider) {
super(DIFF_CALLBACK);
this.glideRequests = glideRequests;
this.preloadSizeProvider = preloadSizeProvider;
selection = new LinkedHashSet<>();
}
private static final DiffUtil.ItemCallback<ItemWithFeed> DIFF_CALLBACK = new DiffUtil.ItemCallback<ItemWithFeed>() {
@Override
public boolean areItemsTheSame(@NonNull ItemWithFeed item, @NonNull ItemWithFeed t1) {
return item.getItem().getId() == t1.getItem().getId();
}
@Override
public boolean areContentsTheSame(@NonNull ItemWithFeed itemWithFeed, @NonNull ItemWithFeed t1) {
Item oldItem = itemWithFeed.getItem();
Item newItem = t1.getItem();
boolean folder = false;
if (itemWithFeed.getFolder() != null && t1.getFolder() != null)
folder = itemWithFeed.getFolder().getName().equals(t1.getFolder().getName());
return oldItem.getTitle().equals(newItem.getTitle()) &&
itemWithFeed.getFeedName().equals(t1.getFeedName()) &&
folder &&
oldItem.isRead() == newItem.isRead() &&
oldItem.isReadItLater() == newItem.isReadItLater() &&
itemWithFeed.getColor() == t1.getColor() &&
itemWithFeed.getBgColor() == t1.getBgColor();
}
@Override
public Object getChangePayload(@NonNull ItemWithFeed oldItem, @NonNull ItemWithFeed newItem) {
return newItem;
}
};
private static final DrawableCrossFadeFactory FADE_FACTORY = new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build();
private static final RequestOptions REQUEST_OPTIONS = new RequestOptions().transform(new CenterCrop(), new RoundedCorners(16));
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
ListItemBinding binding = ListItemBinding.inflate(LayoutInflater.from(viewGroup.getContext()));
ItemViewHolder viewHolder = new ItemViewHolder(binding);
preloadSizeProvider.setView(binding.itemImage);
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position, @NonNull List<Object> payloads) {
if (!payloads.isEmpty()) {
ItemWithFeed itemWithFeed = (ItemWithFeed) payloads.get(0);
holder.bind(itemWithFeed);
holder.applyColors(itemWithFeed);
if (itemWithFeed.getFolder() != null)
holder.binding.itemFolderName.setText(itemWithFeed.getFolder().getName());
else
holder.binding.itemFolderName.setText(R.string.no_folder);
holder.setReadState(itemWithFeed.getItem().isRead());
holder.setSelected(selection.contains(position));
} else
onBindViewHolder(holder, position);
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder viewHolder, int i) {
ItemWithFeed itemWithFeed = getItem(i);
if (itemWithFeed == null)
return;
viewHolder.bind(itemWithFeed);
viewHolder.setImages(itemWithFeed);
viewHolder.applyColors(itemWithFeed);
int minutes = (int) Math.round(itemWithFeed.getItem().getReadTime());
if (minutes < 1)
viewHolder.binding.itemReadtime.setText(R.string.read_time_lower_than_1);
else if (minutes > 1)
viewHolder.binding.itemReadtime.setText(viewHolder.itemView.getContext().
getString(R.string.read_time, String.valueOf(minutes)));
else
viewHolder.binding.itemReadtime.setText(R.string.read_time_one_minute);
if (itemWithFeed.getFolder() != null)
viewHolder.binding.itemFolderName.setText(itemWithFeed.getFolder().getName());
else
viewHolder.binding.itemFolderName.setText(R.string.no_folder);
viewHolder.setReadState(itemWithFeed.getItem().isRead());
viewHolder.setSelected(selection.contains(viewHolder.getAdapterPosition()));
}
@Override
public long getItemId(int position) {
return getItem(position).getItem().getId();
}
public void toggleSelection(int position) {
if (selection.contains(position))
selection.remove(position);
else
selection.add(position);
notifyItemChanged(position, getItem(position));
}
public void clearSelection() {
LinkedHashSet<Integer> localSelection = new LinkedHashSet<>(selection);
selection.clear();
for (int position : localSelection) {
notifyItemChanged(position, getItem(position));
}
}
public Set<Integer> getSelection() {
return selection;
}
public void updateSelection(boolean read) {
for (int position : selection) {
ItemWithFeed itemWithFeed = getItem(position);
itemWithFeed.getItem().setRead(read);
notifyItemChanged(position, itemWithFeed);
}
}
public void selectAll() {
selection.clear();
for (int i = 0; i < getItemCount(); i++) {
selection.add(i);
}
notifyDataSetChanged();
}
public void unselectAll() {
selection.clear();
notifyDataSetChanged();
}
public List<ItemWithFeed> getSelectedItems() {
List<ItemWithFeed> items = new ArrayList<>();
for (int i : selection) {
items.add(getItem(i));
}
return items;
}
public void clearData() {
submitList(null);
}
public ItemWithFeed getItemWithFeed(int i) {
return getItem(i);
}
@NonNull
@Override
public List<String> getPreloadItems(int position) {
if (getItem(position).getItem().getHasImage()) {
String url = getItem(position).getItem().getImageLink();
return Collections.singletonList(url);
} else {
return Collections.emptyList();
}
}
@Nullable
@Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull String url) {
return glideRequests
.load(url)
.centerCrop()
.apply(REQUEST_OPTIONS)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.transition(DrawableTransitionOptions.withCrossFade(FADE_FACTORY));
}
public interface OnItemClickListener {
void onItemClick(ItemWithFeed itemWithFeed, int position);
void onItemLongClick(ItemWithFeed itemWithFeed, int position);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
public class ItemViewHolder extends RecyclerView.ViewHolder {
private ListItemBinding binding;
private View[] alphaViews;
ItemViewHolder(ListItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
itemView.setOnClickListener((view -> {
int position = getAdapterPosition();
if (listener != null && position != RecyclerView.NO_POSITION)
listener.onItemClick(getItem(position), position);
}));
itemView.setOnLongClickListener(v -> {
int position = getAdapterPosition();
if (listener != null && position != RecyclerView.NO_POSITION)
listener.onItemLongClick(getItem(position), position);
return true;
});
alphaViews = new View[]{
binding.itemDate,
binding.itemFolderName,
binding.itemFeedIcon,
binding.itemFeedName,
binding.itemDescription,
binding.itemTitle,
binding.itemImage,
binding.itemReadtimeLayout
};
}
private void bind(ItemWithFeed itemWithFeed) {
Item item = itemWithFeed.getItem();
binding.itemTitle.setText(item.getTitle());
binding.itemDate.setText(DateUtils.formattedDateByLocal(item.getPubDate()));
binding.itemFeedName.setText(itemWithFeed.getFeedName());
if (item.getCleanDescription() != null) {
binding.itemDescription.setVisibility(View.VISIBLE);
binding.itemDescription.setText(item.getCleanDescription());
} else {
binding.itemDescription.setVisibility(View.GONE);
if (itemWithFeed.getItem().getHasImage())
binding.itemTitle.setMaxLines(4);
}
}
private void setImages(ItemWithFeed itemWithFeed) {
if (itemWithFeed.getItem().getHasImage()) {
binding.itemImage.setVisibility(View.VISIBLE);
glideRequests
.load(itemWithFeed.getItem().getImageLink())
.centerCrop()
.apply(REQUEST_OPTIONS)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.transition(DrawableTransitionOptions.withCrossFade(FADE_FACTORY))
.into(binding.itemImage);
} else
binding.itemImage.setVisibility(View.GONE);
if (itemWithFeed.getFeedIconUrl() != null) {
glideRequests.
load(itemWithFeed.getFeedIconUrl())
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_rss_feed_grey)
.into(binding.itemFeedIcon);
} else
binding.itemFeedIcon.setImageResource(R.drawable.ic_rss_feed_grey);
}
private void applyColors(ItemWithFeed itemWithFeed) {
Resources resources = itemView.getResources();
if (itemWithFeed.getBgColor() != 0) {
binding.itemFeedName.setTextColor(itemWithFeed.getBgColor());
Utils.setDrawableColor(binding.itemDate.getBackground(), itemWithFeed.getBgColor());
} else if (itemWithFeed.getColor() != 0) {
binding.itemFeedName.setTextColor(itemWithFeed.getColor());
Utils.setDrawableColor(binding.itemDate.getBackground(), itemWithFeed.getColor());
} else if (itemWithFeed.getBgColor() == 0 && itemWithFeed.getColor() == 0) {
binding.itemFeedName.setTextColor(resources.getColor(android.R.color.tab_indicator_text));
Utils.setDrawableColor(binding.itemDate.getBackground(),
ContextCompat.getColor(itemView.getContext(), R.color.colorPrimary));
}
}
private void setReadState(boolean isRead) {
float alpha = isRead ? 0.5f : 1.0f;
for (View view : alphaViews) {
view.setAlpha(alpha);
}
}
private void setSelected(boolean selected) {
Context context = itemView.getContext();
TypedValue outValue = new TypedValue();
if (selected) {
context.getTheme().resolveAttribute(
android.R.attr.colorControlHighlight, outValue, true);
} else {
context.getTheme().resolveAttribute(
android.R.attr.selectableItemBackground, outValue, true);
}
itemView.setBackgroundResource(outValue.resourceId);
}
public ImageView getItemImage() {
return binding.itemImage;
}
}
}

View File

@ -1,266 +0,0 @@
package com.readrops.app.itemslist;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.ViewModel;
import androidx.paging.DataSource;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import com.readrops.app.repositories.ARepository;
import com.readrops.app.repositories.FeedUpdate;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.db.Database;
import com.readrops.db.RoomFactoryWrapper;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import com.readrops.db.filters.MainFilter;
import com.readrops.db.filters.ListSortType;
import com.readrops.db.pojo.ItemWithFeed;
import com.readrops.db.queries.ItemsQueryBuilder;
import com.readrops.db.queries.QueryFilters;
import org.koin.core.parameter.ParametersHolderKt;
import org.koin.java.KoinJavaComponent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import io.reactivex.Completable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public class MainViewModel extends ViewModel {
private final MediatorLiveData<PagedList<ItemWithFeed>> itemsWithFeed;
private LiveData<PagedList<ItemWithFeed>> lastFetch;
private ARepository repository;
private final Database database;
private final QueryFilters queryFilters;
private Account currentAccount;
private List<Account> accounts;
public MainViewModel(@NonNull Database database) {
this.database = database;
itemsWithFeed = new MediatorLiveData<>();
queryFilters = new QueryFilters();
/* queryFilters.setShowReadItems(SharedPreferencesManager.readBoolean(
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES));*/
}
//region main query
private void setRepository() {
repository = KoinJavaComponent.get(ARepository.class, null,
() -> ParametersHolderKt.parametersOf(currentAccount));
}
private void buildPagedList() {
if (lastFetch != null) {
itemsWithFeed.removeSource(lastFetch);
}
DataSource.Factory<Integer, ItemWithFeed> items;
items = database.itemDao().selectAll(ItemsQueryBuilder.INSTANCE.buildItemsQuery(queryFilters, currentAccount.getConfig().getUseSeparateState()));
lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(items),
new PagedList.Config.Builder()
.setPageSize(100)
.setPrefetchDistance(150)
.setEnablePlaceholders(false)
.build())
.build();
itemsWithFeed.addSource(lastFetch, itemsWithFeed::setValue);
}
public void invalidate() {
buildPagedList();
}
public void setShowReadItems(boolean showReadItems) {
//queryFilters.setShowReadItems(showReadItems);
}
public boolean showReadItems() {
return queryFilters.getShowReadItems();
}
public void setFilterType(MainFilter filterType) {
//queryFilters.setMainFilter(filterType);
}
public MainFilter getFilterType() {
return queryFilters.getMainFilter();
}
public void setSortType(ListSortType sortType) {
//queryFilters.setSortType(sortType);
}
public ListSortType getSortType() {
return queryFilters.getSortType();
}
public void setFilterFeedId(int filterFeedId) {
//queryFilters.setFilterFeedId(filterFeedId);
}
public void setFilerFolderId(int folderId) {
//queryFilters.setFilterFolderId(folderId);
}
public MediatorLiveData<PagedList<ItemWithFeed>> getItemsWithFeed() {
return itemsWithFeed;
}
public Completable sync(List<Feed> feeds, FeedUpdate update) {
itemsWithFeed.removeSource(lastFetch);
// get current viewed feed
if (feeds == null && queryFilters.getMainFilter() == MainFilter.ALL) {
return Single.<Feed>create(emitter -> emitter.onSuccess(database.feedDao()
.getFeedById(queryFilters.getFilterFeedId())))
.flatMapCompletable(feed -> repository.sync(Collections.singletonList(feed), update));
}
return repository.sync(feeds, update);
}
public Single<Integer> getFeedCount() {
return repository.getFeedCount(currentAccount.getId());
}
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
return repository.getFoldersWithFeeds();
}
//endregion
//region Account
public LiveData<List<Account>> getAllAccounts() {
return database.accountDao().selectAllAsync();
}
private Completable deselectOldCurrentAccount(int accountId) {
return Completable.create(emitter -> {
database.accountDao().deselectOldCurrentAccount(accountId);
emitter.onComplete();
});
}
private Account getAccount(int id) {
for (Account account : accounts) {
if (account.getId() == id)
return account;
}
return null;
}
public void addAccount(Account account) {
accounts.add(account);
setCurrentAccount(account);
}
public Account getCurrentAccount() {
return currentAccount;
}
public void setCurrentAccount(Account currentAccount) {
this.currentAccount = currentAccount;
setRepository();
//queryFilters.setAccountId(currentAccount.getId());
buildPagedList();
// set the new account as the current one
Completable setCurrentAccount = Completable.create(emitter -> {
database.accountDao().setCurrentAccount(currentAccount.getId());
emitter.onComplete();
});
Completable.concat(Arrays.asList(setCurrentAccount, deselectOldCurrentAccount(currentAccount.getId())))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
}
public void setCurrentAccount(int id) {
setCurrentAccount(getAccount(id));
}
public void setAccounts(List<Account> accounts) {
this.accounts = accounts;
boolean currentAccountExists = false;
for (Account account1 : accounts) {
if (account1.isCurrentAccount()) {
currentAccount = account1;
currentAccountExists = true;
setRepository();
//queryFilters.setAccountId(currentAccount.getId());
buildPagedList();
break;
}
}
if (!currentAccountExists && !accounts.isEmpty()) {
setCurrentAccount(accounts.get(0));
accounts.get(0).setCurrentAccount(true);
}
}
public boolean isAccountLocal() {
return currentAccount.isLocal();
}
//endregion
//region Item read state
public Completable setItemReadState(ItemWithFeed itemWithFeed) {
return repository.setItemReadState(itemWithFeed.getItem());
}
public Completable setItemReadState(Item item) {
return repository.setItemReadState(item);
}
public Completable setItemsReadState(List<ItemWithFeed> items, boolean read) {
List<Completable> completableList = new ArrayList<>();
for (ItemWithFeed itemWithFeed : items) {
itemWithFeed.getItem().setRead(read);
completableList.add(setItemReadState(itemWithFeed));
}
return Completable.concat(completableList);
}
public Completable setAllItemsReadState(boolean read) {
if (queryFilters.getMainFilter() == MainFilter.ALL)
return repository.setAllFeedItemsReadState(queryFilters.getFilterFeedId(), read);
else
return repository.setAllItemsReadState(read);
}
public Completable setItemReadItLater(boolean readLater, int itemId) {
return database.itemDao().setReadItLater(readLater, itemId);
}
//endregion
}

View File

@ -1,150 +0,0 @@
package com.readrops.app.notifications
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import com.readrops.app.R
import com.readrops.app.settings.SettingsActivity
import com.readrops.app.databinding.ActivityNotificationPermissionBinding
import com.readrops.app.utils.ReadropsKeys
import com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID
import com.readrops.app.utils.SharedPreferencesManager
import com.readrops.app.utils.Utils
import com.readrops.db.entities.Feed
import com.readrops.db.entities.account.Account
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.koin.androidx.viewmodel.ext.android.getViewModel
class NotificationPermissionActivity : AppCompatActivity() {
private lateinit var binding: ActivityNotificationPermissionBinding
private lateinit var viewModel: NotificationPermissionViewModel
private var adapter: NotificationPermissionListAdapter? = null
private var isFirstCheck = true
private var feedStateChanged = false
private var feeds = listOf<Feed>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityNotificationPermissionBinding.inflate(layoutInflater)
setContentView(binding.root)
setTitle(R.string.notifications)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val accountId = intent.getIntExtra(ACCOUNT_ID, 0)
viewModel = getViewModel<NotificationPermissionViewModel>()
viewModel.getAccount(accountId).observe(this, Observer { account ->
viewModel.account = account
if (adapter == null) {
// execute the method only once
setupUI(account)
}
})
}
private fun setupUI(account: Account) {
binding.notifPermissionAccountSwitch.isChecked = account.isNotificationsEnabled
binding.notifPermissionAccountSwitch.setOnCheckedChangeListener { _, isChecked ->
account.isNotificationsEnabled = isChecked
binding.notifPermissionFeedsSwitch.isEnabled = isChecked
adapter?.enableAll = isChecked
adapter?.notifyDataSetChanged()
viewModel.setAccountNotificationsState(isChecked)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError { Utils.showSnackbar(binding.root, it.message) }
.subscribe()
if (isChecked) displayAutoSynchroPopup()
}
binding.notifPermissionFeedsSwitch.isEnabled = account.isNotificationsEnabled
binding.notifPermissionFeedsSwitch.setOnCheckedChangeListener { _, isChecked ->
if (canUpdateAllFeedsPermissions(isChecked)) {
viewModel.setAllFeedsNotificationState(isChecked)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError { Utils.showSnackbar(binding.root, it.message) }
.subscribe()
}
if (isFirstCheck) isFirstCheck = false
if (feedStateChanged) feedStateChanged = false
}
adapter = NotificationPermissionListAdapter(account.isNotificationsEnabled) { feed ->
feedStateChanged = true
viewModel.setFeedNotificationState(feed)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError { Utils.showSnackbar(binding.root, it.message) }
.subscribe()
}
binding.notifPermissionAccountList.layoutManager = LinearLayoutManager(this)
binding.notifPermissionAccountList.adapter = adapter
viewModel.getFeedsWithNotifPermission().observe(this, Observer { newFeeds ->
feeds = newFeeds
binding.notifPermissionFeedsSwitch.isChecked = newFeeds.all { it.isNotificationEnabled }
adapter?.submitList(newFeeds)
})
}
/**
* Inform if is possible to update all feeds notifications permissions in the same time.
* The method takes into account the following states :
* - first check : when opening the activity with all feeds permissions enabled,
* the enable all feeds permissions switch will be checked but the request mustn't be executed
* - feed state : if all feeds permissions are enabled and a feed permission is disabled,
* the enable all feeds permissions switch will be unchecked but the request mustn't be executed as only one feed permission is disabled
* - all feeds permissions switch checked : if the setOnCheckedChangeListener method is triggered because all feeds permissions were enabled,
* do not execute the request as it would be pointless
*/
private fun canUpdateAllFeedsPermissions(isChecked: Boolean): Boolean {
return (!isFirstCheck || !feeds.all { it.isNotificationEnabled }) &&
(!feedStateChanged || (isChecked && !feeds.all { it.isNotificationEnabled }))
}
private fun displayAutoSynchroPopup() {
val autoSynchroValue = SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.AUTO_SYNCHRO)
if (autoSynchroValue.toFloat() <= 0) {
MaterialDialog.Builder(this)
.title(R.string.auto_synchro_disabled)
.content(R.string.enable_auto_synchro_text)
.positiveText(R.string.open)
.neutralText(R.string.cancel)
.onPositive { _, _ ->
val intent = Intent(this, SettingsActivity::class.java).apply {
putExtra(ReadropsKeys.SETTINGS, SettingsActivity.SettingsKey.SETTINGS.ordinal)
}
startActivity(intent)
}
.show()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
}

View File

@ -1,67 +0,0 @@
package com.readrops.app.notifications
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.readrops.app.R
import com.readrops.app.databinding.NotificationPermissionLayoutBinding
import com.readrops.app.utils.GlideRequests
import com.readrops.db.entities.Feed
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class NotificationPermissionListAdapter(var enableAll: Boolean, val listener: (feed: Feed) -> Unit) :
ListAdapter<Feed, NotificationPermissionListAdapter.NotificationPermissionViewHolder>(DIFF_CALLBACK), KoinComponent {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationPermissionViewHolder {
val binding = NotificationPermissionLayoutBinding.inflate(LayoutInflater.from(parent.context))
return NotificationPermissionViewHolder(binding)
}
override fun onBindViewHolder(holder: NotificationPermissionViewHolder, position: Int) {
val feed = getItem(position)
holder.binding.notificationFeedName.text = feed.name
holder.binding.notificationSwitch.isChecked = feed.isNotificationEnabled
holder.binding.notificationSwitch.isEnabled = enableAll
holder.itemView.setOnClickListener { if (enableAll) listener(getItem(position)) }
get<GlideRequests>()
.load(feed.iconUrl)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_rss_feed_grey)
.into(holder.binding.notificationFeedIcon)
}
override fun onBindViewHolder(holder: NotificationPermissionViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
val feed = payloads.first() as Feed
holder.binding.notificationSwitch.isChecked = feed.isNotificationEnabled
} else onBindViewHolder(holder, position)
}
inner class NotificationPermissionViewHolder(val binding: NotificationPermissionLayoutBinding) :
RecyclerView.ViewHolder(binding.root)
companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Feed>() {
override fun areItemsTheSame(oldItem: Feed, newItem: Feed): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Feed, newItem: Feed): Boolean {
return oldItem.isNotificationEnabled == newItem.isNotificationEnabled
}
override fun getChangePayload(oldItem: Feed, newItem: Feed): Any? {
return newItem
}
}
}
}

View File

@ -1,27 +0,0 @@
package com.readrops.app.notifications
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.account.Account
import io.reactivex.Completable
class NotificationPermissionViewModel(val database: Database) : ViewModel() {
var account: Account? = null
fun getAccount(accountId: Int): LiveData<Account> = database.accountDao().selectAsync(accountId)
fun getFeedsWithNotifPermission(): LiveData<List<Feed>> = database.feedDao()
.getFeedsForNotifPermission(account?.id!!)
fun setAccountNotificationsState(enabled: Boolean): Completable = database.accountDao()
.updateNotificationState(account?.id!!, enabled)
fun setFeedNotificationState(feed: Feed): Completable = database.feedDao()
.updateFeedNotificationState(feed.id, !feed.isNotificationEnabled)
fun setAllFeedsNotificationState(enabled: Boolean): Completable = database.feedDao()
.updateAllFeedsNotificationState(account?.id!!, enabled)
}

View File

@ -1,122 +0,0 @@
package com.readrops.app.notifications.sync
import android.content.Context
import androidx.core.content.ContextCompat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.readrops.app.R
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Item
import com.readrops.db.entities.account.Account
import com.readrops.api.services.SyncResult
import com.readrops.app.utils.GlideRequests
import com.readrops.app.utils.Utils
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
/**
* Simple class to get synchro notification content (title, content and largeIcon) according to some rules
*/
class SyncResultAnalyser(val context: Context, private val syncResults: Map<Account, SyncResult>, val database: Database) : KoinComponent {
private val notifContent = SyncResultNotifContent()
fun getSyncNotifContent(): SyncResultNotifContent {
if (newItemsInMultipleAccounts()) { // new items from several accounts
var itemCount = 0
val feeds = database.feedDao().selectFromIdList(getFeedsIdsForNewItems(syncResults))
syncResults.values.forEach { syncResult ->
itemCount += syncResult.items.filter { isFeedNotificationEnabledForItem(feeds, it) }.size
}
notifContent.title = context.getString(R.string.new_items, itemCount.toString())
} else { // new items from only one account
val syncResultMap = syncResults.filterValues { it.items.isNotEmpty() }
if (syncResultMap.values.isNotEmpty()) {
val syncResult = syncResultMap.values.first()
val account = syncResultMap.keys.first()
val feedsIdsForNewItems = getFeedsIdsForNewItems(syncResult)
notifContent.accountId = account.id
if (account.isNotificationsEnabled) {
val feeds = database.feedDao().selectFromIdList(feedsIdsForNewItems)
val items = syncResult.items.filter { isFeedNotificationEnabledForItem(feeds, it) }
val itemCount = items.size
// new items from several feeds from one account
if (feedsIdsForNewItems.size > 1 && itemCount > 1) {
notifContent.title = account.accountName
notifContent.content = context.getString(R.string.new_items, itemCount.toString())
notifContent.largeIcon = Utils.getBitmapFromDrawable(ContextCompat.getDrawable(context, account.accountType!!.iconRes))
} else if (feedsIdsForNewItems.size == 1) // new items from only one feed from one account
oneFeedCase(feedsIdsForNewItems.first(), syncResult.items)
else if (itemCount == 1)
oneFeedCase(items.first().feedId.toLong(), items)
}
}
}
return notifContent
}
private fun oneFeedCase(feedId: Long, items: List<Item>) {
val feed = database.feedDao().getFeedById(feedId.toInt())
if (feed.isNotificationEnabled) {
notifContent.title = feed?.name
feed?.iconUrl?.let {
val target = get<GlideRequests>()
.asBitmap()
.load(it)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.submit()
notifContent.largeIcon = target.get()
}
if (items.size == 1) {
val item = database.itemDao().selectByRemoteId(items.first().remoteId!!,
items.first().feedId)
notifContent.content = item.title
notifContent.item = item
} else notifContent.content = context.getString(R.string.new_items, items.size.toString())
}
}
private fun newItemsInMultipleAccounts(): Boolean {
val itemsNotEmptyByAccount = mutableListOf<Boolean>()
for ((account, syncResult) in syncResults) {
if (account.isNotificationsEnabled) itemsNotEmptyByAccount += syncResult.items.isNotEmpty()
}
// return true it there is at least two true booleans in the list
return itemsNotEmptyByAccount.groupingBy { it }.eachCount()[true] ?: 0 > 1
}
private fun getFeedsIdsForNewItems(syncResult: SyncResult): List<Long> {
val feedsIds = mutableListOf<Long>()
syncResult.items.forEach {
if (it.feedId.toLong() !in feedsIds)
feedsIds += it.feedId.toLong()
}
return feedsIds
}
private fun getFeedsIdsForNewItems(syncResults: Map<Account, SyncResult>): List<Long> {
val feedsIds = mutableListOf<Long>()
syncResults.values.forEach { feedsIds += getFeedsIdsForNewItems(it) }
return feedsIds
}
private fun isFeedNotificationEnabledForItem(feeds: List<Feed>, item: Item): Boolean =
feeds.find { it.id == item.feedId }?.isNotificationEnabled!!
}

View File

@ -1,100 +0,0 @@
package com.readrops.app.notifications.sync
import com.readrops.api.services.SyncResult
import com.readrops.db.Database
import com.readrops.db.entities.Item
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import org.jetbrains.annotations.TestOnly
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class SyncResultDebugData {
companion object : KoinComponent {
@TestOnly
fun oneAccountOneFeedOneItem(): Map<Account, SyncResult> {
val database = get<Database>()
val account1 = database.accountDao().select(2)
val item = database.itemDao().select(5000)
// database.feedDao().updateNotificationState(item.feedId, false).subscribe()
return mutableMapOf<Account, SyncResult>().apply {
put(account1, SyncResult().apply { items = mutableListOf(item) })
}
}
@TestOnly
fun oneAccountOneFeedMultipleItems(): Map<Account, SyncResult> {
val account1 = Account().apply {
id = 1
accountType = AccountType.FRESHRSS
isNotificationsEnabled = true
}
val database = get<Database>()
val item = database.itemDao().select(5055)
database.feedDao().updateFeedNotificationState(item.feedId, false).subscribe()
val item2 = database.itemDao().select(5056)
return mutableMapOf<Account, SyncResult>().apply {
put(account1, SyncResult().apply { items = listOf(item, item2) })
}
}
@TestOnly
fun oneAccountMultipleFeeds(): Map<Account, SyncResult> {
val account1 = Account().apply {
accountName = "Test account"
id = 1
accountType = AccountType.FRESHRSS
isNotificationsEnabled = true
}
val item1 = Item().apply {
id = 1
title = "oneAccountMultipleFeeds"
feedId = 1
}
val item2 = Item().apply {
id = 2
title = "oneAccountMultipleFeeds"
feedId = 2
}
return mutableMapOf<Account, SyncResult>().apply {
put(account1, SyncResult().apply { items = mutableListOf(item1, item2) })
}
}
fun multipleAccounts(): Map<Account, SyncResult> {
val account1 = Account().apply {
id = 1
accountType = AccountType.FRESHRSS
isNotificationsEnabled = true
}
val account2 = Account().apply {
id = 2
accountType = AccountType.LOCAL
isNotificationsEnabled = true
}
val item = Item().apply {
id = 1
title = "multipleAccountsCase"
feedId = 90
}
return mutableMapOf<Account, SyncResult>().apply {
put(account1, SyncResult().apply { items = mutableListOf(item) })
put(account2, SyncResult().apply { items = mutableListOf(item) })
}
}
}
}

View File

@ -1,12 +0,0 @@
package com.readrops.app.notifications.sync
import android.graphics.Bitmap
import com.readrops.db.entities.Item
class SyncResultNotifContent {
var title: String? = null
var content: String? = null
var largeIcon: Bitmap? = null
var item: Item? = null
var accountId: Int? = null
}

View File

@ -1,211 +0,0 @@
package com.readrops.app.notifications.sync
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.readrops.api.services.SyncResult
import com.readrops.app.R
import com.readrops.app.ReadropsApp
import com.readrops.app.itemslist.MainActivity
import com.readrops.app.repositories.ARepository
import com.readrops.app.utils.ReadropsKeys
import com.readrops.app.utils.SharedPreferencesManager
import com.readrops.db.Database
import com.readrops.db.entities.Item
import com.readrops.db.entities.account.Account
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.parameter.parametersOf
class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters), KoinComponent {
private var disposable: Disposable? = null
private val notificationManager = NotificationManagerCompat.from(applicationContext)
private val database = get<Database>()
override fun doWork(): Result {
var result = Result.success()
val syncResults = mutableMapOf<Account, SyncResult>()
try {
val accounts = database.accountDao().selectAll()
val notificationBuilder = NotificationCompat.Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
.setContentTitle(applicationContext.getString(R.string.auto_synchro))
.setProgress(0, 0, true)
.setSmallIcon(R.drawable.ic_notif)
.setOnlyAlertOnce(true)
accounts.forEach {
notificationBuilder.setContentText(it.accountName)
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
it.login = SharedPreferencesManager.readString(it.loginKey)
it.password = SharedPreferencesManager.readString(it.passwordKey)
val repository = get<ARepository>(parameters = { parametersOf(it) })
disposable = repository.sync(null, null)
.doOnError { throwable ->
result = Result.failure()
Log.e(TAG, throwable.message!!, throwable)
}
.subscribe()
if (repository.syncResult != null) syncResults[it] = repository.syncResult
}
} catch (e: Exception) {
Log.e(TAG, e.message!!)
result = Result.failure()
} finally {
notificationManager.cancel(SYNC_NOTIFICATION_ID)
displaySyncResultNotif(syncResults)
return result
}
}
override fun onStopped() {
super.onStopped()
disposable?.dispose()
notificationManager.cancel(SYNC_NOTIFICATION_ID)
}
private fun displaySyncResultNotif(syncResults: Map<Account, SyncResult>) {
val notifContent = SyncResultAnalyser(applicationContext, syncResults, database)
.getSyncNotifContent()
if (notifContent.title != null) {
val intent = Intent(applicationContext, MainActivity::class.java).apply {
if (notifContent.item != null) {
putExtra(ReadropsKeys.ITEM_ID, notifContent.item?.id)
putExtra(ReadropsKeys.IMAGE_URL, notifContent.item?.imageLink)
if (notifContent.accountId != null) putExtra(ReadropsKeys.ACCOUNT_ID, notifContent.accountId!!)
}
}
val intentFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_IMMUTABLE
}
val notificationBuilder = NotificationCompat.Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
.setContentTitle(notifContent.title)
.setContentText(notifContent.content)
.setStyle(NotificationCompat.BigTextStyle().bigText(notifContent.content))
.setSmallIcon(R.drawable.ic_notif)
.setContentIntent(PendingIntent.getActivity(applicationContext, 0,
intent, intentFlag))
.setAutoCancel(true)
notifContent.item?.let {
val feed = database.feedDao().getFeedById(it.feedId)
notificationBuilder.addAction(buildReadlaterAction(it))
.addAction(buildMarkAsRead(it))
.setColor(if (feed.backgroundColor != 0) feed.backgroundColor else feed.textColor)
}
notifContent.largeIcon?.let {
notificationBuilder.setLargeIcon(it)
}
notificationManager.notify(SYNC_RESULT_NOTIFICATION_ID, notificationBuilder.build())
}
}
private fun buildReadlaterAction(item: Item): NotificationCompat.Action {
val broadcastIntent = Intent(applicationContext, ReadLaterReceiver::class.java).apply {
putExtra(ReadropsKeys.ITEM_ID, item.id)
}
val intentFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_IMMUTABLE
}
return NotificationCompat.Action.Builder(R.drawable.ic_read_later, applicationContext.getString(R.string.read_later),
PendingIntent.getBroadcast(applicationContext, 0, broadcastIntent, intentFlag))
.setAllowGeneratedReplies(false)
.build()
}
private fun buildMarkAsRead(item: Item): NotificationCompat.Action {
val broadcastIntent = Intent(applicationContext, MarkReadReceiver::class.java).apply {
putExtra(ReadropsKeys.ITEM_ID, item.id)
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
NotificationCompat.Action.Builder(R.drawable.ic_read, applicationContext.getString(R.string.read),
PendingIntent.getBroadcast(applicationContext, 0, broadcastIntent, PendingIntent.FLAG_IMMUTABLE))
.setAllowGeneratedReplies(false)
.build()
} else {
NotificationCompat.Action.Builder(R.drawable.ic_read, applicationContext.getString(R.string.read),
PendingIntent.getBroadcast(applicationContext, 0, broadcastIntent, PendingIntent.FLAG_IMMUTABLE))
.setAllowGeneratedReplies(false)
.build()
}
}
class MarkReadReceiver : BroadcastReceiver(), KoinComponent {
override fun onReceive(context: Context?, intent: Intent?) {
val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!!
with(get<Database>()) {
itemDao().setReadState(itemId, true)
.subscribeOn(Schedulers.io())
.subscribe()
}
with(NotificationManagerCompat.from(context!!)) {
cancel(SYNC_RESULT_NOTIFICATION_ID)
}
}
}
class ReadLaterReceiver : BroadcastReceiver(), KoinComponent {
override fun onReceive(context: Context?, intent: Intent?) {
val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!!
with(get<Database>()) {
val item = itemDao().select(itemId)
item.isReadItLater = !item.isReadItLater
itemDao().setReadItLater(item.isReadItLater, itemId)
.subscribeOn(Schedulers.io())
.subscribe()
}
with(NotificationManagerCompat.from(context!!)) {
cancel(SYNC_RESULT_NOTIFICATION_ID)
}
}
}
companion object {
val TAG = SyncWorker::class.java.simpleName
private const val SYNC_NOTIFICATION_ID = 2
const val SYNC_RESULT_NOTIFICATION_ID = 3
}
}

View File

@ -1,207 +0,0 @@
package com.readrops.app.repositories;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.api.services.Credentials;
import com.readrops.api.services.SyncResult;
import com.readrops.api.utils.AuthInterceptor;
import com.readrops.app.addfeed.FeedInsertionResult;
import com.readrops.app.addfeed.ParsingResult;
import com.readrops.app.utils.feedscolors.FeedColorsKt;
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
import com.readrops.db.Database;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.ItemState;
import com.readrops.db.entities.account.Account;
import org.koin.java.KoinJavaComponent;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import io.reactivex.Completable;
import io.reactivex.Single;
public abstract class ARepository {
protected Context context;
protected Database database;
protected Account account;
protected SyncResult syncResult;
protected ARepository(Database database, @NonNull Context context, @Nullable Account account) {
this.context = context;
this.database = database;
this.account = account;
setCredentials(account);
}
protected void setCredentials(@Nullable Account account) {
KoinJavaComponent.<AuthInterceptor>get(AuthInterceptor.class)
.setCredentials(account != null && !account.isLocal() ? Credentials.toCredentials(account) : null);
}
public abstract Completable login(Account account, boolean insert);
public abstract Completable sync(@Nullable List<Feed> feeds, @Nullable FeedUpdate update);
public abstract Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results);
public Completable insertOPMLFoldersAndFeeds(Map<Folder, List<Feed>> foldersAndFeeds) {
List<Completable> completableList = new ArrayList<>();
for (Map.Entry<Folder, List<Feed>> entry : foldersAndFeeds.entrySet()) {
Folder folder = entry.getKey();
folder.setAccountId(account.getId());
Completable completable = Single.<Integer>create(emitter -> {
Folder dbFolder = database.folderDao().getFolderByName(folder.getName(), account.getId());
if (dbFolder != null)
emitter.onSuccess(dbFolder.getId());
else
emitter.onSuccess((int) database.folderDao().compatInsert(folder));
}).flatMap(folderId -> {
List<Feed> feeds = entry.getValue();
for (Feed feed : feeds) {
feed.setFolderId(folderId);
}
List<ParsingResult> parsingResults = ParsingResult.toParsingResults(feeds);
return addFeeds(parsingResults);
}).flatMapCompletable(feedInsertionResults -> Completable.complete());
completableList.add(completable);
}
return Completable.concat(completableList);
}
public Completable updateFeed(Feed feed) {
return Completable.create(emitter -> {
database.feedDao().updateFeedFields(feed.getId(), feed.getName(), feed.getUrl(), feed.getFolderId());
emitter.onComplete();
});
}
public Completable deleteFeed(Feed feed) {
return database.feedDao().delete(feed);
}
public Single<Long> addFolder(Folder folder) {
return database.folderDao().insert(folder);
}
public Completable updateFolder(Folder folder) {
return database.folderDao().update(folder);
}
public Completable deleteFolder(Folder folder) {
return database.folderDao().delete(folder);
}
public Completable setItemReadState(Item item) {
if (account.getConfig().getUseSeparateState()) {
return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId(), true)
.andThen(database.itemStateDao().upsertItemReadState(new ItemState(0, item.isRead(),
item.isStarred(), item.getRemoteId(), account.getId())));
} else if (account.isLocal()) {
return database.itemDao().setReadState(item.getId(), item.isRead());
} else { // nextcloud case
return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId(), false)
.andThen(database.itemDao().setReadState(item.getId(), item.isRead()));
}
}
public Completable setAllItemsReadState(boolean read) {
if (account.isLocal()) { // TODO see if it's possible to implement for others accounts
return database.itemDao().setAllItemsReadState(read ? 1 : 0, account.getId());
} else {
return Completable.complete();
}
}
public Completable setAllFeedItemsReadState(int feedId, boolean read) {
if (account.isLocal()) {
return database.itemDao().setAllFeedItemsReadState(feedId, read ? 1 : 0);
} else {
return Completable.complete();
}
}
public Completable setItemStarState(Item item) {
if (account.getConfig().getUseSeparateState()) {
return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId(), true)
.andThen(database.itemStateDao().upsertItemStarState(new ItemState(0, item.isRead(),
item.isStarred(), item.getRemoteId(), account.getId())));
} else if (account.isLocal()) {
return database.itemDao().setStarState(item.getId(), item.isRead());
} else { // nextcloud case
return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId(), false)
.andThen(database.itemDao().setStarState(item.getId(), item.isStarred()));
}
}
public Single<Integer> getFeedCount(int accountId) {
return database.feedDao().getFeedCount(accountId);
}
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
return Single.create(emitter -> {
List<Folder> folders = database.folderDao().getFolders(account.getId());
Map<Folder, List<Feed>> foldersWithFeeds = new TreeMap<>(Comparator.nullsLast(Folder::compareTo));
for (Folder folder : folders) {
List<Feed> feeds = database.feedDao().getFeedsByFolder(folder.getId());
for (Feed feed : feeds) {
int unreadCount = database.itemDao().getUnreadCount(feed.getId());
feed.setUnreadCount(unreadCount);
}
foldersWithFeeds.put(folder, feeds);
}
// feeds without folder
List<Feed> feedsWithoutFolder = database.feedDao().getFeedsWithoutFolder(account.getId());
for (Feed feed : feedsWithoutFolder) {
feed.setUnreadCount(database.itemDao().getUnreadCount(feed.getId()));
}
foldersWithFeeds.put(null, feedsWithoutFolder);
emitter.onSuccess(foldersWithFeeds);
});
}
protected void setFeedColors(Feed feed) {
FeedColorsKt.setFeedColors(feed);
database.feedDao().updateColors(feed.getId(),
feed.getTextColor(), feed.getBackgroundColor());
}
protected void setFeedsColors(List<Feed> feeds) {
Intent intent = new Intent(context, FeedsColorsIntentService.class);
intent.putParcelableArrayListExtra(FEEDS, new ArrayList<>(feeds));
context.startService(intent);
}
public SyncResult getSyncResult() {
return syncResult;
}
}

View File

@ -1,9 +0,0 @@
package com.readrops.app.repositories
import com.readrops.db.entities.Feed
interface FeedUpdate {
fun onNext(feed: Feed)
}

View File

@ -1,301 +0,0 @@
package com.readrops.app.repositories;
import android.content.Context;
import android.util.Log;
import android.util.TimingLogger;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.api.services.SyncType;
import com.readrops.api.services.freshrss.FreshRSSDataSource;
import com.readrops.api.services.freshrss.FreshRSSSyncData;
import com.readrops.app.addfeed.FeedInsertionResult;
import com.readrops.app.addfeed.ParsingResult;
import com.readrops.app.utils.Utils;
import com.readrops.db.Database;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.ItemState;
import com.readrops.db.entities.account.Account;
import com.readrops.db.pojo.ItemReadStarState;
import org.joda.time.DateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import io.reactivex.Completable;
import io.reactivex.Single;
public class FreshRSSRepository extends ARepository {
private static final String TAG = FreshRSSRepository.class.getSimpleName();
private final FreshRSSDataSource dataSource;
public FreshRSSRepository(FreshRSSDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) {
super(database, context, account);
this.dataSource = dataSource;
}
@Override
public Completable login(Account account, boolean insert) {
setCredentials(account);
return dataSource.login(account.getLogin(), account.getPassword())
.flatMap(token -> {
account.setToken(token);
setCredentials(account);
return dataSource.getWriteToken();
})
.flatMap(writeToken -> {
account.setWriteToken(writeToken);
return dataSource.getUserInfo();
})
.flatMapCompletable(userInfo -> {
account.setDisplayedName(userInfo.getUserName());
if (insert) {
return database.accountDao().insert(account)
.flatMapCompletable(id -> {
account.setId(id.intValue());
return Completable.complete();
});
}
return Completable.complete();
});
}
@Override
public Completable sync(@Nullable List<Feed> feeds, @Nullable FeedUpdate update) {
FreshRSSSyncData syncData = new FreshRSSSyncData();
SyncType syncType;
if (account.getLastModified() != 0) {
syncType = SyncType.CLASSIC_SYNC;
syncData.setLastModified(account.getLastModified());
} else
syncType = SyncType.INITIAL_SYNC;
long newLastModified = DateTime.now().getMillis() / 1000L;
TimingLogger logger = new TimingLogger(TAG, "FreshRSS sync timer");
return Single.<FreshRSSSyncData>create(emitter -> {
List<ItemReadStarState> itemStateChanges = database
.itemStateChangesDao()
.getItemStateChanges(account.getId());
syncData.setReadIds(itemStateChanges.stream()
.filter(it -> it.getReadChange() && it.getRead())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));
syncData.setUnreadIds(itemStateChanges.stream()
.filter(it -> it.getReadChange() && !it.getRead())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));
syncData.setStarredIds(itemStateChanges.stream()
.filter(it -> it.getStarChange() && it.getStarred())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));
syncData.setUnstarredIds(itemStateChanges.stream()
.filter(it -> it.getStarChange() && !it.getStarred())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));
emitter.onSuccess(syncData);
}).flatMap(syncData1 -> dataSource.sync(syncType, syncData1, account.getWriteToken()))
.flatMapCompletable(syncResult -> {
logger.addSplit("server queries");
insertFolders(syncResult.getFolders());
logger.addSplit("folders insertion");
insertFeeds(syncResult.getFeeds());
logger.addSplit("feeds insertion");
insertItems(syncResult.getItems(), false);
logger.addSplit("items insertion");
insertItems(syncResult.getStarredItems(), true);
logger.addSplit("starred items insertion");
insertItemsIds(syncResult.getUnreadIds(), syncResult.getReadIds(), syncResult.getStarredIds());
logger.addSplit("insert and update items ids");
account.setLastModified(newLastModified);
database.accountDao().updateLastModified(account.getId(), newLastModified);
database.itemStateChangesDao().resetStateChanges(account.getId());
logger.dumpToLog();
this.syncResult = syncResult;
return Completable.complete();
});
}
@Override
public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results) {
List<Completable> completableList = new ArrayList<>();
List<FeedInsertionResult> insertionResults = new ArrayList<>();
for (ParsingResult result : results) {
completableList.add(dataSource.createFeed(account.getWriteToken(), result.getUrl())
.doOnComplete(() -> {
FeedInsertionResult feedInsertionResult = new FeedInsertionResult();
feedInsertionResult.setParsingResult(result);
insertionResults.add(feedInsertionResult);
}).onErrorResumeNext(throwable -> {
Log.d(TAG, throwable.getMessage());
FeedInsertionResult feedInsertionResult = new FeedInsertionResult();
feedInsertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.ERROR);
feedInsertionResult.setParsingResult(result);
insertionResults.add(feedInsertionResult);
return Completable.complete();
}));
}
return Completable.concat(completableList)
.andThen(Single.just(insertionResults));
}
@Override
public Completable updateFeed(Feed feed) {
return Single.<Folder>create(emitter -> {
Folder folder = feed.getFolderId() == null ? null : database.folderDao().select(feed.getFolderId());
emitter.onSuccess(folder);
}).flatMapCompletable(folder -> dataSource.updateFeed(account.getWriteToken(),
feed.getUrl(), feed.getName(), folder == null ? null : folder.getRemoteId())
.andThen(super.updateFeed(feed)));
}
@Override
public Completable deleteFeed(Feed feed) {
return dataSource.deleteFeed(account.getWriteToken(), feed.getUrl())
.andThen(super.deleteFeed(feed));
}
@Override
public Single<Long> addFolder(Folder folder) {
return dataSource.createFolder(account.getWriteToken(), folder.getName())
.andThen(super.addFolder(folder));
}
@Override
public Completable updateFolder(Folder folder) {
return dataSource.updateFolder(account.getWriteToken(), folder.getRemoteId(), folder.getName())
.andThen(Completable.create(emitter -> {
folder.setRemoteId("user/-/label/" + folder.getName());
emitter.onComplete();
}))
.andThen(super.updateFolder(folder));
}
@Override
public Completable deleteFolder(Folder folder) {
return dataSource.deleteFolder(account.getWriteToken(), folder.getRemoteId())
.andThen(super.deleteFolder(folder));
}
private void insertFeeds(List<Feed> freshRSSFeeds) {
freshRSSFeeds.stream().forEach(feed -> feed.setAccountId(account.getId()));
List<Long> insertedFeedsIds = database.feedDao().feedsUpsert(freshRSSFeeds, account);
if (!insertedFeedsIds.isEmpty()) {
setFeedsColors(database.feedDao().selectFromIdList(insertedFeedsIds));
}
}
private void insertFolders(List<Folder> freshRSSFolders) {
freshRSSFolders.stream().forEach(folder -> folder.setAccountId(account.getId()));
database.folderDao().foldersUpsert(freshRSSFolders, account);
}
private void insertItems(List<Item> items, boolean starredItems) {
List<Item> itemsToInsert = new ArrayList<>();
Map<String, Integer> itemsFeedsIds = new HashMap<>();
for (Item item : items) {
Integer feedId;
if (itemsFeedsIds.containsKey(item.getFeedRemoteId())) {
feedId = itemsFeedsIds.get(item.getFeedRemoteId());
} else {
feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId());
itemsFeedsIds.put(item.getFeedRemoteId(), feedId);
}
item.setFeedId(feedId);
if (item.getText() != null) {
item.setReadTime(Utils.readTimeFromString(item.getText()));
}
// workaround to avoid inserting starred items coming from the main item call
// as the API exclusion filter doesn't seem to work
if (!starredItems) {
if (!item.isStarred()) {
itemsToInsert.add(item);
}
} else {
itemsToInsert.add(item);
}
}
if (!itemsToInsert.isEmpty()) {
Collections.sort(itemsToInsert, Item::compareTo);
database.itemDao().insert(itemsToInsert);
}
}
private void insertItemsIds(List<String> unreadIds, List<String> readIds, List<String> starredIds) {
database.itemStateDao().deleteItemsStates(account.getId());
database.itemStateDao().insertItemStates(unreadIds.stream().map(id -> {
boolean starred = starredIds.stream().filter(starredId -> starredId.equals(id)).count() == 1;
if (starred) {
starredIds.remove(id);
}
return new ItemState(0, false, starred, id, account.getId());
}
).collect(Collectors.toList()));
database.itemStateDao().insertItemStates(readIds.stream().map(id -> {
boolean starred = starredIds.stream().filter(starredId -> starredId.equals(id)).count() == 1;
if (starred) {
starredIds.remove(id);
}
return new ItemState(0, true, starred, id, account.getId());
}
).collect(Collectors.toList()));
// insert starred items ids which are read
if (!starredIds.isEmpty()) {
database.itemStateDao().insertItemStates(starredIds.stream().map(id ->
new ItemState(0, true, true, id, account.getId()))
.collect(Collectors.toList()));
}
}
}

View File

@ -1,188 +0,0 @@
package com.readrops.app.repositories;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.api.localfeed.LocalRSSDataSource;
import com.readrops.api.services.SyncResult;
import com.readrops.api.utils.ApiUtils;
import com.readrops.api.utils.exceptions.ParseException;
import com.readrops.api.utils.exceptions.UnknownFormatException;
import com.readrops.app.addfeed.FeedInsertionResult;
import com.readrops.app.addfeed.ParsingResult;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.db.Database;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import org.jsoup.Jsoup;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Single;
import kotlin.Pair;
import okhttp3.Headers;
public class LocalFeedRepository extends ARepository {
private static final String TAG = LocalFeedRepository.class.getSimpleName();
private LocalRSSDataSource dataSource;
public LocalFeedRepository(LocalRSSDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) {
super(database, context, account);
syncResult = new SyncResult();
this.dataSource = dataSource;
}
@Override
public Completable login(Account account, boolean insert) {
return null;
}
@Override
public Completable sync(@Nullable List<Feed> feeds, FeedUpdate update) {
return Completable.create(emitter -> {
List<Feed> feedList;
if (feeds == null || feeds.isEmpty()) {
feedList = database.feedDao().getFeeds(account.getId());
} else {
feedList = feeds;
}
for (Feed feed : feedList) {
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(() -> update.onNext(feed));
try {
Headers.Builder headers = new Headers.Builder();
if (feed.getEtag() != null) {
headers.add(ApiUtils.IF_NONE_MATCH_HEADER, feed.getEtag());
}
if (feed.getLastModified() != null) {
headers.add(ApiUtils.IF_MODIFIED_HEADER, feed.getLastModified());
}
Pair<Feed, List<Item>> pair = dataSource.queryRSSResource(feed.getUrl(), headers.build());
if (pair != null) {
insertNewItems(feed, pair.getSecond());
}
} catch (Exception e) {
Log.d(TAG, "sync: " + e.getMessage());
}
}
emitter.onComplete();
});
}
@Override
public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results) {
return Single.create(emitter -> {
List<FeedInsertionResult> insertionResults = new ArrayList<>();
for (ParsingResult parsingResult : results) {
FeedInsertionResult insertionResult = new FeedInsertionResult();
try {
Pair<Feed, List<Item>> pair = dataSource.queryRSSResource(parsingResult.getUrl(),
null);
Feed feed = insertFeed(pair.getFirst(), parsingResult);
if (feed != null) {
insertionResult.setFeed(feed);
}
} catch (ParseException e) {
Log.d(TAG, "addFeeds: " + e.getMessage());
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR);
} catch (UnknownFormatException e) {
Log.d(TAG, "addFeeds: " + e.getMessage());
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR);
} catch (IOException e) {
Log.d(TAG, "addFeeds: " + e.getMessage());
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
} catch (Exception e) {
Log.d(TAG, "addFeeds: " + e.getMessage());
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR);
} finally {
insertionResult.setParsingResult(parsingResult);
insertionResults.add(insertionResult);
}
}
emitter.onSuccess(insertionResults);
});
}
@SuppressWarnings("SimplifyStreamApiCallChains")
private void insertNewItems(Feed feed, List<Item> items) {
database.feedDao().updateHeaders(feed.getEtag(), feed.getLastModified(), feed.getId());
Collections.sort(items, Item::compareTo);
int maxItems = Integer.parseInt(SharedPreferencesManager.readString(
SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB));
if (maxItems > 0 && items.size() > maxItems) {
items = items.subList(items.size() - maxItems, items.size());
}
items.stream().forEach(item -> item.setFeedId(feed.getId()));
insertItems(items, feed);
}
private Feed insertFeed(Feed feed, ParsingResult parsingResult) {
feed.setFolderId(parsingResult.getFolderId());
if (database.feedDao().feedExists(feed.getUrl(), account.getId())) {
return null; // feed already inserted
}
setFeedColors(feed);
feed.setAccountId(account.getId());
// we need empty headers to query the feed just after, without any 304 result
feed.setEtag(null);
feed.setLastModified(null);
feed.setId((int) (database.feedDao().compatInsert(feed)));
return feed;
}
private void insertItems(Collection<Item> items, Feed feed) {
List<Item> itemsToInsert = new ArrayList<>();
for (Item dbItem : items) {
if (!database.itemDao().itemExists(dbItem.getGuid(), feed.getAccountId())) {
if (dbItem.getDescription() != null) {
dbItem.setCleanDescription(Jsoup.parse(dbItem.getDescription()).text());
}
if (dbItem.getContent() != null) {
dbItem.setReadTime(Utils.readTimeFromString(dbItem.getContent()));
} else if (dbItem.getDescription() != null) {
dbItem.setReadTime(Utils.readTimeFromString(dbItem.getCleanDescription()));
}
itemsToInsert.add(dbItem);
}
}
syncResult.getItems().addAll(itemsToInsert);
database.itemDao().insert(itemsToInsert);
}
}

View File

@ -1,352 +0,0 @@
package com.readrops.app.repositories;
import android.content.Context;
import android.database.sqlite.SQLiteConstraintException;
import android.util.Log;
import android.util.TimingLogger;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.api.services.SyncResult;
import com.readrops.api.services.SyncType;
import com.readrops.api.services.nextcloudnews.NextNewsDataSource;
import com.readrops.api.services.nextcloudnews.NextcloudNewsSyncData;
import com.readrops.api.utils.exceptions.UnknownFormatException;
import com.readrops.app.addfeed.FeedInsertionResult;
import com.readrops.app.addfeed.ParsingResult;
import com.readrops.app.utils.Utils;
import com.readrops.db.Database;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import com.readrops.db.pojo.ItemReadStarState;
import org.joda.time.LocalDateTime;
import org.koin.java.KoinJavaComponent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import io.reactivex.Completable;
import io.reactivex.Single;
import okhttp3.OkHttpClient;
public class NextNewsRepository extends ARepository {
private static final String TAG = NextNewsRepository.class.getSimpleName();
private final NextNewsDataSource dataSource;
public NextNewsRepository(NextNewsDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) {
super(database, context, account);
this.dataSource = dataSource;
}
@Override
public Completable login(Account account, boolean insert) {
setCredentials(account);
return Single.<String>create(emitter -> {
OkHttpClient httpClient = KoinJavaComponent.get(OkHttpClient.class);
String displayName = dataSource.login(httpClient, account);
emitter.onSuccess(displayName);
}).flatMapCompletable(displayName -> {
account.setDisplayedName(displayName);
account.setCurrentAccount(true);
if (insert) {
return database.accountDao().insert(account)
.flatMapCompletable(id -> {
account.setId(id.intValue());
return Completable.complete();
});
}
return Completable.complete();
});
}
@Override
public Completable sync(@Nullable List<Feed> feeds, @Nullable FeedUpdate update) {
setCredentials(account);
return Completable.create(emitter -> {
try {
long lastModified = LocalDateTime.now().toDateTime().getMillis();
SyncType syncType;
if (account.getLastModified() != 0) {
syncType = SyncType.CLASSIC_SYNC;
} else {
syncType = SyncType.INITIAL_SYNC;
}
NextcloudNewsSyncData syncData = new NextcloudNewsSyncData();
/*if (syncType == SyncType.CLASSIC_SYNC) {
syncData.setLastModified(account.getLastModified() / 1000L);
List<ItemReadStarState> itemStateChanges = database
.itemStateChangesDao()
.getNextcloudNewsStateChanges(account.getId());
syncData.setReadIds(itemStateChanges.stream()
.filter(it -> it.getReadChange() && it.getRead())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));
syncData.setUnreadIds(itemStateChanges.stream()
.filter(it -> it.getReadChange() && !it.getRead())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));
List<String> starredItemsIds = itemStateChanges.stream()
.filter(it -> it.getStarChange() && it.getStarred())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList());
if (!starredItemsIds.isEmpty()) {
syncData.setStarredIds(database.itemDao().getStarChanges(starredItemsIds, account.getId()));
}
List<String> unstarredItemsIds = itemStateChanges.stream()
.filter(it -> it.getStarChange() && !it.getStarred())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList());
if (!unstarredItemsIds.isEmpty()) {
syncData.setUnstarredIds(database.itemDao().getStarChanges(unstarredItemsIds, account.getId()));
}
}*/
TimingLogger timings = new TimingLogger(TAG, "nextcloud news " + syncType.name().toLowerCase());
SyncResult result = dataSource.sync(syncType, syncData);
timings.addSplit("server queries");
if (!result.isError()) {
syncResult = new SyncResult();
insertFolders(result.getFolders());
timings.addSplit("insert folders");
insertFeeds(result.getFeeds(), false);
timings.addSplit("insert feeds");
boolean initialSync = syncType == SyncType.INITIAL_SYNC;
insertItems(result.getItems(), initialSync);
timings.addSplit("insert items");
insertItems(result.getStarredItems(), initialSync);
timings.dumpToLog();
account.setLastModified(lastModified);
database.accountDao().updateLastModified(account.getId(), lastModified);
database.itemStateChangesDao().resetStateChanges(account.getId());
emitter.onComplete();
} else {
emitter.onError(new Throwable());
}
} catch (Exception e) {
Log.d(TAG, "sync: " + e.getMessage());
emitter.onError(e);
}
});
}
@Override
public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results) {
setCredentials(account);
return Single.create(emitter -> {
List<FeedInsertionResult> feedInsertionResults = new ArrayList<>();
for (ParsingResult result : results) {
FeedInsertionResult insertionResult = new FeedInsertionResult();
try {
List<Feed> nextNewsFeeds = dataSource.createFeed(result.getUrl(), 0);
if (nextNewsFeeds != null) {
List<Feed> newFeeds = insertFeeds(nextNewsFeeds, true);
// there is always only one object in the list, see nextcloud news dataSource doc
insertionResult.setFeed(newFeeds.get(0));
} else
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR);
insertionResult.setParsingResult(result);
} catch (Exception e) {
if (e instanceof IOException)
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
else if (e instanceof UnknownFormatException)
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR);
else if (e instanceof SQLiteConstraintException)
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.DB_ERROR);
else
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR);
}
feedInsertionResults.add(insertionResult);
}
emitter.onSuccess(feedInsertionResults);
});
}
@Override
public Completable updateFeed(Feed feed) {
setCredentials(account);
return Completable.create(emitter -> {
Folder folder = feed.getFolderId() == null ? null : database.folderDao().select(feed.getFolderId());
if (folder != null)
feed.setRemoteFolderId(folder.getRemoteId());
else
feed.setRemoteFolderId(String.valueOf(0)); // 0 for no folder
try {
if (dataSource.renameFeed(feed) && dataSource.changeFeedFolder(feed)) {
emitter.onComplete();
} else
emitter.onError(new Exception("Unknown error when updating feed"));
} catch (Exception e) {
emitter.onError(e);
}
}).andThen(super.updateFeed(feed));
}
@Override
public Completable deleteFeed(Feed feed) {
setCredentials(account);
return Completable.create(emitter -> {
try {
if (dataSource.deleteFeed(Integer.parseInt(feed.getRemoteId()))) {
emitter.onComplete();
} else
emitter.onError(new Exception("Unknown error"));
} catch (Exception e) {
emitter.onError(e);
}
emitter.onComplete();
}).andThen(super.deleteFeed(feed));
}
@Override
public Single<Long> addFolder(Folder folder) {
setCredentials(account);
return Single.<Folder>create(emitter -> {
try {
List<Folder> folders = dataSource.createFolder(folder);
if (folders != null) {
Folder nextNewsFolder = folders.get(0); // always only one item returned by the server, see doc
folder.setRemoteId(nextNewsFolder.getRemoteId());
emitter.onSuccess(folder);
} else
emitter.onError(new Exception("Unknown error"));
} catch (Exception e) {
emitter.onError(e);
}
}).flatMap(folder1 -> database.folderDao().insert(folder));
}
@Override
public Completable updateFolder(Folder folder) {
setCredentials(account);
return Completable.create(emitter -> {
try {
if (dataSource.renameFolder(folder)) {
emitter.onComplete();
} else
emitter.onError(new Exception("Unknown error"));
} catch (Exception e) {
emitter.onError(e);
}
emitter.onComplete();
}).andThen(super.updateFolder(folder));
}
@Override
public Completable deleteFolder(Folder folder) {
setCredentials(account);
return Completable.create(emitter -> {
try {
if (dataSource.deleteFolder(folder)) {
emitter.onComplete();
} else
emitter.onError(new Exception("Unknown error"));
} catch (Exception e) {
emitter.onError(e);
}
emitter.onComplete();
}).andThen(super.deleteFolder(folder));
}
private List<Feed> insertFeeds(List<Feed> nextNewsFeeds, boolean newFeeds) {
for (Feed nextNewsFeed : nextNewsFeeds) {
nextNewsFeed.setAccountId(account.getId());
}
List<Long> insertedFeedsIds;
if (newFeeds) {
insertedFeedsIds = database.feedDao().insert(nextNewsFeeds);
} else {
insertedFeedsIds = database.feedDao().feedsUpsert(nextNewsFeeds, account);
}
List<Feed> insertedFeeds = new ArrayList<>();
if (!insertedFeedsIds.isEmpty()) {
insertedFeeds.addAll(database.feedDao().selectFromIdList(insertedFeedsIds));
setFeedsColors(insertedFeeds);
}
return insertedFeeds;
}
private void insertFolders(List<Folder> nextNewsFolders) {
for (Folder folder : nextNewsFolders) {
folder.setAccountId(account.getId());
}
database.folderDao().foldersUpsert(nextNewsFolders, account);
}
private void insertItems(List<Item> items, boolean initialSync) {
List<Item> itemsToInsert = new ArrayList<>();
for (Item item : items) {
int feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId());
//if the item already exists, update only its read state
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(String.valueOf(item.getRemoteId()), feedId)) {
database.itemDao().setReadAndStarState(item.getRemoteId(), item.isRead(), item.isStarred());
continue;
}
item.setFeedId(feedId);
item.setReadTime(Utils.readTimeFromString(item.getContent()));
itemsToInsert.add(item);
}
if (!itemsToInsert.isEmpty()) {
syncResult.setItems(itemsToInsert);
Collections.sort(itemsToInsert, Item::compareTo);
database.itemDao().insert(itemsToInsert);
}
}
}

View File

@ -1,325 +0,0 @@
package com.readrops.app.settings;
import static android.app.Activity.RESULT_OK;
import static com.readrops.app.utils.OPMLHelper.OPEN_OPML_FILE_REQUEST;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID;
import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
import android.Manifest;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.api.opml.OPMLParser;
import com.readrops.app.R;
import com.readrops.app.ReadropsApp;
import com.readrops.app.account.AccountViewModel;
import com.readrops.app.account.AddAccountActivity;
import com.readrops.app.feedsfolders.ManageFeedsFoldersActivity;
import com.readrops.app.notifications.NotificationPermissionActivity;
import com.readrops.app.utils.FileUtils;
import com.readrops.app.utils.OPMLHelper;
import com.readrops.app.utils.PermissionManager;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import com.readrops.db.entities.account.AccountType;
import org.koin.android.compat.ViewModelCompat;
import java.io.FileNotFoundException;
import java.util.List;
import java.util.Map;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.schedulers.Schedulers;
import kotlin.Unit;
/**
* A simple {@link Fragment} subclass.
*/
public class AccountSettingsFragment extends PreferenceFragmentCompat {
private static final String TAG = AccountSettingsFragment.class.getSimpleName();
private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1;
private Account account;
private AccountViewModel viewModel;
public AccountSettingsFragment() {
}
public static AccountSettingsFragment newInstance(Account account) {
AccountSettingsFragment fragment = new AccountSettingsFragment();
Bundle args = new Bundle();
args.putParcelable(ACCOUNT, account);
fragment.setArguments(args);
return fragment;
}
@SuppressWarnings("ConstantConditions")
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.acount_preferences);
account = getArguments().getParcelable(ACCOUNT);
Preference feedsFoldersPref = findPreference("feeds_folders_key");
Preference credentialsPref = findPreference("credentials_key");
Preference deleteAccountPref = findPreference("delete_account_key");
Preference opmlPref = findPreference("opml_import_export");
Preference notificationPref = findPreference("notifications");
if (account.is(AccountType.LOCAL))
credentialsPref.setVisible(false);
if (!account.is(AccountType.LOCAL))
opmlPref.setVisible(false);
feedsFoldersPref.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(getContext(), ManageFeedsFoldersActivity.class);
intent.putExtra(ACCOUNT, account);
startActivity(intent);
return true;
});
credentialsPref.setOnPreferenceClickListener(preference -> {
if (!account.isLocal()) {
Intent intent = new Intent(getContext(), AddAccountActivity.class);
intent.putExtra(EDIT_ACCOUNT, account);
startActivity(intent);
}
return true;
});
deleteAccountPref.setOnPreferenceClickListener(preference -> {
deleteAccount();
return true;
});
opmlPref.setOnPreferenceClickListener(preference -> {
new MaterialDialog.Builder(getActivity())
.items(R.array.opml_import_export)
.itemsCallback(((dialog, itemView, position, text) -> openOPMLMode(position)))
.show();
return true;
});
notificationPref.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(getContext(), NotificationPermissionActivity.class);
intent.putExtra(ACCOUNT_ID, account.getId());
startActivity(intent);
return true;
});
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
viewModel.setAccount(account);
}
private void deleteAccount() {
new MaterialDialog.Builder(getContext())
.title(R.string.delete_account_question)
.positiveText(R.string.validate)
.negativeText(R.string.cancel)
.onPositive(((dialog, which) -> {
SharedPreferencesManager.remove(account.getLoginKey());
SharedPreferencesManager.remove(account.getPasswordKey());
viewModel.delete(account)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
getActivity().finish();
}
@Override
public void onError(Throwable e) {
Utils.showSnackbar(getView(), e.getMessage());
}
});
}))
.show();
}
private void openOPMLMode(int position) {
if (position == 0) {
OPMLHelper.openFileIntent(this);
} else {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
if (PermissionManager.isPermissionGranted(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
exportAsOPMLFile();
} else {
requestExternalStoragePermission();
}
} else {
exportAsOPMLFile();
}
}
}
// region opml import
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.title(R.string.opml_processing)
.content(R.string.operation_takes_time)
.progress(true, 100)
.cancelable(false)
.show();
try {
parseOPMLFile(uri, dialog);
} catch (FileNotFoundException e) {
Log.d(TAG, e.getMessage());
displayErrorMessage();
}
}
super.onActivityResult(requestCode, resultCode, data);
}
private void parseOPMLFile(Uri uri, MaterialDialog dialog) throws FileNotFoundException {
viewModel.parseOPMLFile(uri, getContext())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
dialog.dismiss();
}
@Override
public void onError(Throwable e) {
dialog.dismiss();
displayErrorMessage();
}
});
}
private void displayErrorMessage() {
new MaterialDialog.Builder(getActivity())
.title(R.string.processing_file_failed)
.neutralText(R.string.cancel)
.iconRes(R.drawable.ic_error)
.show();
}
//endregion
//region opml export
private void exportAsOPMLFile() {
String fileName = "subscriptions.opml";
try {
String path = FileUtils.writeDownloadFile(getContext(), fileName, "text/x-opml", outputStream -> {
Map<Folder, List<Feed>> folderListMap = viewModel.getFoldersWithFeeds()
.subscribeOn(Schedulers.io())
.blockingGet();
/*OPMLParser.write(folderListMap, outputStream)
.blockingAwait();*/
return Unit.INSTANCE;
});
displayNotification(fileName, path);
} catch (Exception e) {
displayErrorMessage();
}
}
private void displayNotification(String name, String absolutePath) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(absolutePath), "text/plain");
int intentFlag;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlag = PendingIntent.FLAG_IMMUTABLE;
} else {
intentFlag = PendingIntent.FLAG_IMMUTABLE;
}
Notification notification = new NotificationCompat.Builder(getContext(), ReadropsApp.OPML_EXPORT_CHANNEL_ID)
.setContentTitle(getString(R.string.opml_export))
.setContentText(name)
.setSmallIcon(R.drawable.ic_notif)
.setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, intentFlag))
.setAutoCancel(true)
.build();
NotificationManagerCompat manager = NotificationManagerCompat.from(getContext());
manager.notify(2, notification);
}
private void requestExternalStoragePermission() {
PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(permissions[0])) {
Utils.showSnackBarWithAction(getView(), getString(R.string.external_storage_opml_export),
getString(R.string.try_again), v -> requestExternalStoragePermission());
} else {
Utils.showSnackBarWithAction(getView(), getString(R.string.external_storage_opml_export),
getString(R.string.permissions), v -> {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", getContext().getPackageName(), null));
getContext().startActivity(intent);
});
}
} else {
exportAsOPMLFile();
}
}
}
//endregion
}

View File

@ -1,61 +0,0 @@
package com.readrops.app.settings;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.readrops.app.R;
import com.readrops.db.entities.account.Account;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.SETTINGS;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Account account = getIntent().getParcelableExtra(ACCOUNT);
SettingsKey settingsKey = SettingsKey.values()[getIntent().getIntExtra(SETTINGS, -1)];
Fragment fragment = null;
switch (settingsKey) {
case ACCOUNT_SETTINGS:
fragment = AccountSettingsFragment.newInstance(account);
setTitle(account.getAccountName());
break;
case SETTINGS:
fragment = new SettingsFragment();
setTitle(R.string.settings);
break;
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings_activity_fragment, fragment)
.commit();
}
public enum SettingsKey {
ACCOUNT_SETTINGS,
SETTINGS
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -1,135 +0,0 @@
package com.readrops.app.settings;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
import android.content.Intent;
import android.os.Bundle;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.readrops.app.R;
import com.readrops.app.notifications.sync.SyncWorker;
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
import com.readrops.db.Database;
import org.koin.java.KoinJavaComponent;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
Preference feedsColorsPreference = findPreference("reload_feeds_colors");
Preference themePreference = findPreference("dark_theme");
Preference synchroPreference = findPreference("auto_synchro");
AtomicBoolean serviceStarted = new AtomicBoolean(false);
feedsColorsPreference.setOnPreferenceClickListener(preference -> {
Database database = KoinJavaComponent.get(Database.class);
database.feedDao().getAllFeeds().observe(getActivity(), feeds -> {
if (!serviceStarted.get()) {
Intent intent = new Intent(getContext(), FeedsColorsIntentService.class);
intent.putParcelableArrayListExtra(FEEDS, new ArrayList<>(feeds));
getContext().startService(intent);
serviceStarted.set(true);
}
});
return true;
});
themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue.equals(getString(R.string.theme_value_light))) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
} else if (newValue.equals(getString(R.string.theme_value_dark))) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
}
return true;
});
synchroPreference.setOnPreferenceChangeListener(((preference, newValue) -> {
WorkManager workManager = WorkManager.getInstance(getContext());
Pair<Integer, TimeUnit> interval = getWorkerInterval((String) newValue);
if (interval != null) {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(SyncWorker.class, interval.first, interval.second)
.addTag(SyncWorker.Companion.getTAG())
.setConstraints(constraints)
.setInitialDelay(interval.first, interval.second)
.build();
workManager.enqueueUniquePeriodicWork(SyncWorker.Companion.getTAG(), ExistingPeriodicWorkPolicy.REPLACE, request);
} else {
workManager.cancelAllWorkByTag(SyncWorker.Companion.getTAG());
}
return true;
}));
}
@Nullable
private Pair<Integer, TimeUnit> getWorkerInterval(String newValue) {
int interval;
TimeUnit timeUnit;
switch (newValue) {
case "0.30":
interval = 30;
timeUnit = TimeUnit.MINUTES;
break;
case "1":
interval = 1;
timeUnit = TimeUnit.HOURS;
break;
case "2":
interval = 2;
timeUnit = TimeUnit.HOURS;
break;
case "3":
interval = 3;
timeUnit = TimeUnit.HOURS;
break;
case "6":
interval = 6;
timeUnit = TimeUnit.HOURS;
break;
case "12":
interval = 12;
timeUnit = TimeUnit.HOURS;
break;
case "24":
interval = 1;
timeUnit = TimeUnit.DAYS;
break;
default:
return null;
}
return new Pair<>(interval, timeUnit);
}
}

View File

@ -1,69 +0,0 @@
package com.readrops.app.utils
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
object FileUtils {
@JvmStatic
fun writeDownloadFile(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
writeFileApi29(context, fileName, mimeType, listener)
else
writeFileApi28(fileName, listener)
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun writeFileApi29(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String {
val resolver = context.contentResolver
val downloadsUri = MediaStore.Downloads
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val fileDetails = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
put(MediaStore.Downloads.IS_PENDING, 1)
put(MediaStore.Downloads.MIME_TYPE, mimeType)
}
val contentUri = resolver.insert(downloadsUri, fileDetails)
resolver.openOutputStream(contentUri!!)!!.use { stream ->
try {
listener(stream)
} catch (e: Exception) {
throw e
} finally {
stream.flush()
stream.close()
}
fileDetails.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(contentUri, fileDetails, null, null)
}
fileDetails.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(contentUri, fileDetails, null, null)
return contentUri.path!!
}
private fun writeFileApi28(fileName: String, listener: (OutputStream) -> Unit): String {
val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
val file = File(filePath, fileName)
val outputStream = FileOutputStream(file)
listener(outputStream)
outputStream.flush()
outputStream.close()
return file.absolutePath
}
}

View File

@ -1,110 +0,0 @@
package com.readrops.app.utils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.api.localfeed.LocalRSSHelper;
import com.readrops.api.utils.ApiUtils;
import com.readrops.api.utils.AuthInterceptor;
import com.readrops.app.addfeed.ParsingResult;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.koin.java.KoinJavaComponent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public final class HtmlParser {
private static final String TAG = HtmlParser.class.getSimpleName();
/**
* Parse the html page to get all rss urls
*
* @param url url to request
* @return a list of rss urls with their title
*/
public static List<ParsingResult> getFeedLink(String url) {
List<ParsingResult> results = new ArrayList<>();
String head = getHTMLHeadFromUrl(url);
if (head != null) {
Document document = Jsoup.parse(head, url);
Elements elements = document.select("link");
for (Element element : elements) {
String type = element.attributes().get("type");
if (LocalRSSHelper.isRSSType(type)) {
String feedUrl = element.absUrl("href");
String label = element.attributes().get("title");
results.add(new ParsingResult(feedUrl, label));
}
}
return results;
} else {
return Collections.emptyList();
}
}
@Nullable
public static String getFaviconLink(@NonNull String url) {
String favUrl = null;
String head = getHTMLHeadFromUrl(url);
if (head == null)
return null;
Document document = Jsoup.parse(head, url);
Elements elements = document.select("link");
for (Element element : elements) {
if (element.attributes().get("rel").toLowerCase().contains("icon")) {
favUrl = element.absUrl("href");
break;
}
}
return favUrl;
}
@Nullable
private static String getHTMLHeadFromUrl(@NonNull String url) {
long start = System.currentTimeMillis();
try {
Response response = KoinJavaComponent.<OkHttpClient>get(OkHttpClient.class)
.newCall(new Request.Builder().url(url).build()).execute();
KoinJavaComponent.<AuthInterceptor>get(AuthInterceptor.class).setCredentials(null);
if (response.header("Content-Type").contains(ApiUtils.HTML_CONTENT_TYPE)) {
String body = response.body().string();
String head = body.substring(body.indexOf("<head"), body.indexOf("</head>"));
long end = System.currentTimeMillis();
Log.d(TAG, "parsing time : " + (end - start));
return head;
} else {
return null;
}
} catch (Exception e) {
Log.d(TAG, e.getMessage());
return null;
}
}
}

View File

@ -1,27 +0,0 @@
package com.readrops.app.utils
import android.app.Activity
import android.content.Intent
import androidx.fragment.app.Fragment
object OPMLHelper {
const val OPEN_OPML_FILE_REQUEST = 1
@JvmStatic
fun openFileIntent(activity: Activity) =
activity.startActivityForResult(createIntent(), OPEN_OPML_FILE_REQUEST)
@JvmStatic
fun openFileIntent(fragment: Fragment) =
fragment.startActivityForResult(createIntent(), OPEN_OPML_FILE_REQUEST)
private fun createIntent(): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/*", "text/*"))
}
}
}

View File

@ -1,24 +0,0 @@
package com.readrops.app.utils
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
object PermissionManager {
@JvmStatic
fun isPermissionGranted(context: Context, permission: String): Boolean =
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
@JvmStatic
fun requestPermissions(activity: Activity, requestCode: Int, vararg permissions: String) =
ActivityCompat.requestPermissions(activity, permissions, requestCode)
@JvmStatic
fun requestPermissions(fragment: Fragment, requestCode: Int, vararg permissions: String) =
fragment.requestPermissions(permissions, requestCode)
}

View File

@ -1,23 +0,0 @@
package com.readrops.app.utils
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule
import okhttp3.OkHttpClient
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import java.io.InputStream
@GlideModule
class ReadropsGlideModule : AppGlideModule(), KoinComponent {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val factory = OkHttpUrlLoader.Factory(get<OkHttpClient>())
glide.registry.replace(GlideUrl::class.java, InputStream::class.java, factory)
}
}

View File

@ -1,25 +0,0 @@
package com.readrops.app.utils
object ReadropsKeys {
const val ACCOUNT = "ACCOUNT_KEY"
const val ACCOUNT_ID = "ACCOUNT_ID"
const val ACCOUNT_TYPE = "ACCOUNT_TYPE_KEY"
const val EDIT_ACCOUNT = "EDIT_ACCOUNT"
const val FROM_MAIN_ACTIVITY = "FROM_MAIN_ACTIVITY_KEY"
const val ITEM_ID = "ITEM_ID_KEY"
const val IMAGE_URL = "IMAGE_URL_KEY"
const val SYNCING = "SYNCING_KEY"
const val SETTINGS = "SETTINGS_KEY"
const val WEB_URL = "WEB_URL_KEY"
const val ACTION_BAR_COLOR = "ACTION_BAR_COLOR_KEY"
const val FEEDS = "FEEDS"
const val STARRED_ITEM = "STARRED_ITEM"
}

View File

@ -1,86 +0,0 @@
package com.readrops.app.utils;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import org.koin.java.KoinJavaComponent;
public final class SharedPreferencesManager {
public static void writeValue(String key, Object value) {
SharedPreferences sharedPref = KoinJavaComponent.get(SharedPreferences.class);
SharedPreferences.Editor editor = sharedPref.edit();
if (value instanceof Boolean)
editor.putBoolean(key, (Boolean) value);
else if (value instanceof String)
editor.putString(key, (String) value);
editor.apply();
}
public static void writeValue(SharedPrefKey sharedPrefKey, Object value) {
writeValue(sharedPrefKey.key, value);
}
public static int readInt(SharedPrefKey sharedPrefKey) {
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
return sharedPreferences.getInt(sharedPrefKey.key, sharedPrefKey.getIntDefaultValue());
}
public static boolean readBoolean(SharedPrefKey sharedPrefKey) {
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
return sharedPreferences.getBoolean(sharedPrefKey.key, sharedPrefKey.getBooleanDefaultValue());
}
public static String readString(String key) {
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
return sharedPreferences.getString(key, null);
}
public static String readString(SharedPrefKey sharedPrefKey) {
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
return sharedPreferences.getString(sharedPrefKey.key, sharedPrefKey.getStringDefaultValue());
}
public static void remove(String key) {
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(key);
editor.apply();
}
public enum SharedPrefKey {
SHOW_READ_ARTICLES("show_read_articles", false),
ITEMS_TO_PARSE_MAX_NB("items_to_parse_max_nb", "20"),
OPEN_ITEMS_IN("open_items_in", "0"),
DARK_THEME("dark_theme", "false"),
AUTO_SYNCHRO("auto_synchro", "0"),
HIDE_FEEDS("hide_feeds", false),
MARK_ITEMS_READ_ON_SCROLL("mark_items_read", false);
@NonNull
private String key;
@NonNull
private Object defaultValue;
public boolean getBooleanDefaultValue() {
return Boolean.valueOf(defaultValue.toString());
}
public String getStringDefaultValue() {
return (String) defaultValue;
}
public int getIntDefaultValue() {
return Integer.parseInt(defaultValue.toString());
}
SharedPrefKey(@NonNull String key, @NonNull Object defaultValue) {
this.key = key;
this.defaultValue = defaultValue;
}
}
}

View File

@ -1,124 +0,0 @@
package com.readrops.app.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import com.google.android.material.snackbar.Snackbar;
import org.koin.java.KoinJavaComponent;
import java.io.InputStream;
import java.util.Locale;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public final class Utils {
public static final String HTTP_PREFIX = "http://";
public static final String HTTPS_PREFIX = "https://";
private static final int AVERAGE_WORDS_PER_MINUTE = 250;
public static Bitmap getImageFromUrl(String url) {
try {
Request request = new Request.Builder().url(url).build();
Response response = KoinJavaComponent.<OkHttpClient>get(OkHttpClient.class).newCall(request).execute();
if (response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
return BitmapFactory.decodeStream(inputStream);
} else
return null;
} catch (Exception e) {
return null; // no way to get the favicon
}
}
public static double readTimeFromString(String value) {
int nbWords = value.split("\\s+").length;
return (double) nbWords / AVERAGE_WORDS_PER_MINUTE;
}
public static String getCssColor(@ColorInt int color) {
return String.format(Locale.US, "rgba(%d,%d,%d,%.2f)",
Color.red(color),
Color.green(color),
Color.blue(color),
Color.alpha(color) / 255.0);
}
public static boolean isTypeImage(@NonNull String type) {
return type.equals("image") || type.equals("image/jpeg") || type.equals("image/jpg")
|| type.equals("image/png");
}
public static void setDrawableColor(Drawable drawable, @ColorInt int color) {
drawable.mutate().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
}
public static Drawable drawableWithColor(@ColorInt int color) {
ShapeDrawable drawable = new ShapeDrawable(new OvalShape());
drawable.setIntrinsicWidth(50);
drawable.setIntrinsicHeight(50);
drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
return drawable;
}
public static void showSnackBarWithAction(View root, String message, String action, View.OnClickListener listener) {
Snackbar snackbar = Snackbar.make(root, message, Snackbar.LENGTH_LONG);
snackbar.setAction(action, listener);
snackbar.show();
}
public static void showSnackbar(View root, String message) {
Snackbar snackbar = Snackbar.make(root, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
public static Bitmap getBitmapFromDrawable(Drawable drawable) {
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
public static boolean isColorTooBright(@ColorInt int color) {
return getColorLuma(color) > 210;
}
public static boolean isColorTooDark(@ColorInt int color) {
return getColorLuma(color) < 40;
}
private static double getColorLuma(@ColorInt int color) {
int r = (color >> 16) & 0xff;
int g = (color >> 8) & 0xff;
int b = (color >> 0) & 0xff;
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
}

View File

@ -1,174 +0,0 @@
package com.readrops.app.utils.customviews;
import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.LayoutRes;
import androidx.annotation.StringRes;
import com.mikepenz.fastadapter.IClickable;
import com.mikepenz.fastadapter.IItem;
import com.mikepenz.fastadapter.listeners.OnClickListener;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.materialdrawer.Drawer;
import com.mikepenz.materialdrawer.holder.BadgeStyle;
import com.mikepenz.materialdrawer.holder.ColorHolder;
import com.mikepenz.materialdrawer.holder.StringHolder;
import com.mikepenz.materialdrawer.icons.MaterialDrawerFont;
import com.mikepenz.materialdrawer.model.BaseDescribeableDrawerItem;
import com.mikepenz.materialdrawer.model.BaseViewHolder;
import com.mikepenz.materialdrawer.model.interfaces.ColorfulBadgeable;
import com.readrops.app.R;
import java.util.List;
/**
* This a simple modification of original ExpandableBadgeDrawerItem from MaterialDrawer lib to get two click events from an expandable drawer item
*/
public class CustomExpandableBadgeDrawerItem extends BaseDescribeableDrawerItem<CustomExpandableBadgeDrawerItem,
CustomExpandableBadgeDrawerItem.ViewHolder>
implements ColorfulBadgeable<CustomExpandableBadgeDrawerItem>, IClickable {
protected ColorHolder arrowColor;
protected int arrowRotationAngleStart = 0;
protected int arrowRotationAngleEnd = 180;
protected StringHolder mBadge;
protected BadgeStyle mBadgeStyle = new BadgeStyle();
@Override
public int getType() {
return R.id.material_drawer_item_expandable_badge;
}
@Override
@LayoutRes
public int getLayoutRes() {
return R.layout.custom_expandable_drawer_item;
}
@Override
public void bindView(CustomExpandableBadgeDrawerItem.ViewHolder viewHolder, List payloads) {
super.bindView(viewHolder, payloads);
Context ctx = viewHolder.itemView.getContext();
//bind the basic view parts
bindViewHelper(viewHolder);
//set the text for the badge or hide
boolean badgeVisible = StringHolder.applyToOrHide(mBadge, viewHolder.badge);
//style the badge if it is visible
if (true) {
mBadgeStyle.style(viewHolder.badge, getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx)));
viewHolder.badgeContainer.setVisibility(View.VISIBLE);
} else {
viewHolder.badgeContainer.setVisibility(View.GONE);
}
//define the typeface for our textViews
if (getTypeface() != null) {
viewHolder.badge.setTypeface(getTypeface());
}
//make sure all animations are stopped
if (viewHolder.arrow.getDrawable() instanceof IconicsDrawable) {
((IconicsDrawable) viewHolder.arrow.getDrawable()).color(this.arrowColor != null ? this.arrowColor.color(ctx) : getIconColor(ctx));
}
viewHolder.arrow.clearAnimation();
if (!isExpanded()) {
viewHolder.arrow.setRotation(this.arrowRotationAngleStart);
} else {
viewHolder.arrow.setRotation(this.arrowRotationAngleEnd);
}
//call the onPostBindView method to trigger post bind view actions (like the listener to modify the item if required)
onPostBindView(this, viewHolder.itemView);
}
@Override
public CustomExpandableBadgeDrawerItem withOnDrawerItemClickListener(Drawer.OnDrawerItemClickListener onDrawerItemClickListener) {
mOnDrawerItemClickListener = null;
return this;
}
@Override
public Drawer.OnDrawerItemClickListener getOnDrawerItemClickListener() {
return null;
}
@Override
public CustomExpandableBadgeDrawerItem withBadge(StringHolder badge) {
this.mBadge = badge;
return this;
}
@Override
public CustomExpandableBadgeDrawerItem withBadge(String badge) {
this.mBadge = new StringHolder(badge);
return this;
}
@Override
public CustomExpandableBadgeDrawerItem withBadge(@StringRes int badgeRes) {
this.mBadge = new StringHolder(badgeRes);
return this;
}
@Override
public CustomExpandableBadgeDrawerItem withBadgeStyle(BadgeStyle badgeStyle) {
this.mBadgeStyle = badgeStyle;
return this;
}
public StringHolder getBadge() {
return mBadge;
}
public BadgeStyle getBadgeStyle() {
return mBadgeStyle;
}
@Override
public ViewHolder getViewHolder(View v) {
return new ViewHolder(v);
}
@Override
public IItem withOnItemPreClickListener(OnClickListener onItemPreClickListener) {
return null;
}
@Override
public OnClickListener getOnPreItemClickListener() {
return null;
}
@Override
public IItem withOnItemClickListener(OnClickListener onItemClickListener) {
return null;
}
@Override
public OnClickListener getOnItemClickListener() {
return null;
}
public static class ViewHolder extends BaseViewHolder {
public ImageView arrow;
public View badgeContainer;
public TextView badge;
public ViewHolder(View view) {
super(view);
badgeContainer = view.findViewById(R.id.material_drawer_badge_container);
badge = view.findViewById(R.id.material_drawer_badge);
arrow = view.findViewById(R.id.material_drawer_arrow);
arrow.setImageDrawable(new IconicsDrawable(view.getContext(), MaterialDrawerFont.Icon.mdf_expand_more).sizeDp(16).paddingDp(2).color(Color.BLACK));
}
}
}

View File

@ -1,24 +0,0 @@
package com.readrops.app.utils.customviews
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import com.readrops.app.R
import com.readrops.app.databinding.EmptyListViewBinding
/**
* A simple custom view to display a empty list message
*/
class EmptyListView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
val binding: EmptyListViewBinding = EmptyListViewBinding.inflate(LayoutInflater.from(context), this, true)
init {
val attributes = context.obtainStyledAttributes(attrs, R.styleable.EmptyListView)
binding.emptyListImage.setImageDrawable(attributes.getDrawable(R.styleable.EmptyListView_image))
binding.emptyListText.text = attributes.getString(R.styleable.EmptyListView_text)
attributes.recycle()
}
}

View File

@ -1,141 +0,0 @@
package com.readrops.app.utils.customviews
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.Nullable
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
class ReadropsItemTouchCallback(private val context: Context, private val config: Config) :
ItemTouchHelper.SimpleCallback(config.dragDirs, config.swipeDirs) {
private val iconHorizontalMargin = 40
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
config.moveCallback?.onMove()
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
config.swipeCallback?.onSwipe(viewHolder, direction)
}
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
val background: ColorDrawable
var icon: Drawable? = null
val itemView: View = viewHolder.itemView
var draw = true // variable used to draw under some conditions
// do not draw anymore if the view has reached the screen's left/right side
if (abs(dX).toInt() == itemView.right) {
draw = false
} else if (abs(dX).toInt() == 0) {
draw = true
}
// left swipe
if (dX > 0 && config.leftDraw != null && draw) {
background = ColorDrawable(config.leftDraw.bgColor)
background.setBounds(itemView.left, itemView.top, dX.toInt(), itemView.bottom)
icon = config.leftDraw.drawable
?: ContextCompat.getDrawable(context, config.leftDraw.iconRes)!!
val iconMargin = (itemView.height - icon.intrinsicHeight) / 2
icon.setBounds(itemView.left + iconHorizontalMargin, itemView.top + iconMargin,
itemView.left + iconHorizontalMargin + icon.intrinsicWidth, itemView.bottom - iconMargin)
// right swipe
} else if (dX < 0 && config.rightDraw != null && draw) {
background = ColorDrawable(config.rightDraw.bgColor)
background.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
icon = config.rightDraw.drawable
?: ContextCompat.getDrawable(context, config.rightDraw.iconRes)!!
val iconMargin = (itemView.height - icon.intrinsicHeight) / 2
icon.setBounds(itemView.right - iconHorizontalMargin - icon.intrinsicWidth, itemView.top + iconMargin,
itemView.right - iconHorizontalMargin, itemView.bottom - iconMargin)
} else {
background = ColorDrawable()
}
background.draw(c)
if (dX > 0)
c.clipRect(itemView.left, itemView.top, dX.toInt(), itemView.bottom)
else if (dX < 0)
c.clipRect(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
icon?.draw(c)
}
override fun isItemViewSwipeEnabled(): Boolean {
return config.swipeCallback != null
}
override fun isLongPressDragEnabled(): Boolean {
return config.moveCallback != null
}
interface MoveCallback {
fun onMove()
}
interface SwipeCallback {
fun onSwipe(viewHolder: RecyclerView.ViewHolder, direction: Int)
}
class SwipeDraw(@ColorInt val bgColor: Int, @DrawableRes val iconRes: Int = 0, val drawable: Drawable?)
class Config(val dragDirs: Int = 0, val swipeDirs: Int = 0, val moveCallback: MoveCallback? = null,
val swipeCallback: SwipeCallback? = null, val leftDraw: SwipeDraw? = null, val rightDraw: SwipeDraw? = null) {
private constructor(builder: Builder) : this(builder.dragDirs, builder.swipeDirs,
builder.moveCallback, builder.swipeCallback, builder.leftDraw, builder.rightDraw)
class Builder {
var dragDirs: Int = 0
private set
var swipeDirs: Int = 0
private set
var moveCallback: MoveCallback? = null
private set
var swipeCallback: SwipeCallback? = null
private set
var leftDraw: SwipeDraw? = null
private set
var rightDraw: SwipeDraw? = null
private set
fun dragDirs(dragDirs: Int) = apply { this.dragDirs = dragDirs }
fun swipeDirs(swipeDirs: Int) = apply { this.swipeDirs = swipeDirs }
fun moveCallback(moveCallback: MoveCallback) = apply { this.moveCallback = moveCallback }
fun swipeCallback(swipeCallback: SwipeCallback) = apply { this.swipeCallback = swipeCallback }
fun leftDraw(@ColorInt bgColor: Int, @DrawableRes iconRes: Int, @Nullable icon: Drawable? = null) = apply { leftDraw = SwipeDraw(bgColor, iconRes, icon) }
fun rightDraw(@ColorInt bgColor: Int, @DrawableRes iconRes: Int, @Nullable icon: Drawable? = null) = apply { this.rightDraw = SwipeDraw(bgColor, iconRes, icon) }
fun build(): Config {
return Config(this)
}
}
}
}

View File

@ -1,118 +0,0 @@
package com.readrops.app.utils.customviews;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Base64;
import android.webkit.WebSettings;
import android.webkit.WebView;
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import com.readrops.app.R;
import com.readrops.app.utils.Utils;
import com.readrops.db.pojo.ItemWithFeed;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;
import org.jsoup.select.Elements;
public class ReadropsWebView extends WebView {
private ItemWithFeed itemWithFeed;
@ColorInt
private int textColor;
@ColorInt
private int backgroundColor;
public ReadropsWebView(Context context, AttributeSet attrs) {
super(context, attrs);
getColors(context, attrs);
init();
}
public void setItem(ItemWithFeed itemWithFeed) {
this.itemWithFeed = itemWithFeed;
String text = getText();
String base64Content = null;
if (text != null)
base64Content = Base64.encodeToString(text.getBytes(), Base64.NO_PADDING);
loadData(base64Content, "text/html; charset=utf-8", "base64");
}
public String getItemContent() {
String content = itemWithFeed.getItem().getContent();
return content;
}
private void getColors(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ReadropsWebView);
textColor = typedArray.getColor(R.styleable.ReadropsWebView_textColor, 0);
backgroundColor = typedArray.getColor(R.styleable.ReadropsWebView_backgroundColor, 0);
typedArray.recycle();
}
@SuppressLint("SetJavaScriptEnabled")
private void init() {
if (!isInEditMode()) {
WebSettings settings = getSettings();
settings.setJavaScriptEnabled(true);
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
}
setVerticalScrollBarEnabled(false);
setBackgroundColor(backgroundColor);
}
@Nullable
private String getText() {
if (itemWithFeed.getItem().getText() != null) {
Document document;
if (itemWithFeed.getWebsiteUrl() != null)
document = Jsoup.parse(Parser.unescapeEntities(itemWithFeed.getItem().getText(), false), itemWithFeed.getWebsiteUrl());
else
document = Jsoup.parse(Parser.unescapeEntities(itemWithFeed.getItem().getText(), false));
formatDocument(document);
int color = itemWithFeed.getColor() != 0 ? itemWithFeed.getColor() : getResources().getColor(R.color.colorPrimary);
return getContext().getString(R.string.webview_html_template,
Utils.getCssColor(itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() :
color),
Utils.getCssColor(this.textColor),
Utils.getCssColor(backgroundColor),
document.body().html());
} else
return null;
}
private void formatDocument(Document document) {
Elements elements = document.select("figure,figcaption");
for (Element element : elements) {
element.unwrap();
}
elements.clear();
elements = document.select("div,span");
for (Element element : elements) {
element.clearAttributes();
}
}
}

View File

@ -1,38 +0,0 @@
package com.readrops.app.utils.feedscolors
import androidx.palette.graphics.Palette
import com.readrops.app.utils.HtmlParser
import com.readrops.app.utils.Utils
import com.readrops.db.entities.Feed
fun setFeedColors(feed: Feed) {
getFaviconLink(feed)
if (feed.iconUrl != null) {
val bitmap = Utils.getImageFromUrl(feed.iconUrl) ?: return
val palette = Palette.from(bitmap).generate()
val dominantSwatch = palette.dominantSwatch
feed.textColor = if (dominantSwatch != null && !Utils.isColorTooBright(dominantSwatch.rgb)
&& !Utils.isColorTooDark(dominantSwatch.rgb)) {
dominantSwatch.rgb
} else 0
val mutedSwatch = palette.mutedSwatch
feed.backgroundColor = if (mutedSwatch != null && !Utils.isColorTooBright(mutedSwatch.rgb)
&& !Utils.isColorTooDark(mutedSwatch.rgb)) {
mutedSwatch.rgb
} else 0
}
}
fun getFaviconLink(feed: Feed) {
feed.iconUrl = if (feed.iconUrl != null)
feed.iconUrl
else
HtmlParser.getFaviconLink(feed.siteUrl!!)
}

View File

@ -1,48 +0,0 @@
package com.readrops.app.utils.feedscolors
import android.app.IntentService
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.readrops.app.R
import com.readrops.app.ReadropsApp
import com.readrops.app.utils.ReadropsKeys.FEEDS
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class FeedsColorsIntentService : IntentService("FeedsColorsIntentService"), KoinComponent {
override fun onHandleIntent(intent: Intent?) {
val feeds: List<Feed> = intent!!.getParcelableArrayListExtra(FEEDS)!!
val database = get<Database>()
val notificationBuilder = NotificationCompat.Builder(this, ReadropsApp.FEEDS_COLORS_CHANNEL_ID)
.setContentTitle(getString(R.string.get_feeds_colors))
.setProgress(feeds.size, 0, false)
.setSmallIcon(R.drawable.ic_notif)
.setOnlyAlertOnce(true)
startForeground(NOTIFICATION_ID, notificationBuilder.build())
val notificationManager = NotificationManagerCompat.from(this)
var feedsNb = 0
feeds.forEach {
notificationBuilder.setContentText(it.name)
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
setFeedColors(it)
database.feedDao().updateColors(it.id, it.textColor, it.backgroundColor)
notificationBuilder.setProgress(feeds.size, ++feedsNb, false)
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
}
stopForeground(true)
}
companion object {
private const val NOTIFICATION_ID = 1
}
}

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/darker_gray" android:state_enabled="false" />
<item android:color="@android:color/white" />
</selector>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/colorPrimaryDark" />
<item android:gravity="center" android:width="120dp" android:height="120dp">
<bitmap
android:gravity="fill_horizontal|fill_vertical"
android:src="@drawable/logo" />
</item>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM6,10L6,7L4,7v3L1,10v2h3v3h2v-3h3v-2L6,10zM15,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#11FF00"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#727272"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM19,14h-3v3h-2v-3h-3v-2h3L14,9h2v3h3v2z"/>
</vector>

View File

@ -1,20 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:fillColor="#000000"
android:fillType="nonZero"
android:pathData="M119.105,183.425C107.127,195.403 86.992,194.681 74.134,181.823C61.275,168.964 60.557,148.832 72.535,136.854C84.513,124.877 104.645,125.595 117.5,138.451C130.364,151.315 131.083,171.447 119.105,183.425" />
<path
android:fillColor="#727272"
android:fillType="nonZero"
android:pathData="M113.728,159.622C110.739,162.611 104.392,161.119 99.558,156.29C94.718,151.454 93.23,145.105 96.223,142.113C99.215,139.121 105.559,140.61 110.396,145.448C115.227,150.281 116.718,156.628 113.728,159.622" />
<path
android:fillColor="#00000000"
android:pathData="M16.301,127.979a111.96,111.829 89.999,1 0,223.658 0a111.96,111.829 89.999,1 0,-223.658 0z"
android:strokeWidth="29.11"
android:strokeColor="#000000" />
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#727272"
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z" />
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M15,1L9,1v2h6L15,1zM11,14h2L13,8h-2v6zM19.03,7.39l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9 9,-4.03 9,-9c0,-2.12 -0.74,-4.07 -1.97,-5.61zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View File

@ -1,24 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="133dp"
android:height="154dp"
android:viewportWidth="133"
android:viewportHeight="154">
<path
android:fillColor="#00000000"
android:pathData="M66.5,87.5m-59,0a59,59 0,1 1,118 0a59,59 0,1 1,-118 0"
android:strokeWidth="15"
android:strokeColor="#727272" />
<path
android:fillColor="#727272"
android:pathData="M112.968,33.469l10.536,-10.536l9.037,9.037l-10.536,10.536z" />
<path
android:fillColor="#00000000"
android:pathData="M66.5,44L66.5,88"
android:strokeWidth="15"
android:strokeColor="#727272" />
<path
android:fillColor="#00000000"
android:pathData="M45.5,7.5L87.5,7.5"
android:strokeWidth="15"
android:strokeColor="#727272" />
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View File

@ -1,6 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" 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="#FF000000" android:pathData="M4,4.44v2.83c7.03,0 12.73,5.7 12.73,12.73h2.83c0,-8.59 -6.97,-15.56 -15.56,-15.56zM4,10.1v2.83c3.9,0 7.07,3.17 7.07,7.07h2.83c0,-5.47 -4.43,-9.9 -9.9,-9.9z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#727272"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z" />
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#727272"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</vector>

View File

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

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM12,17H6v-2h6V17zM15,13H9v-2h6V13zM18,9h-6V7h6V9z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FF6200"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More