Merge branch 'develop'

This commit is contained in:
Shinokuni 2019-12-21 19:49:53 +01:00
commit 624d57f1d6
88 changed files with 1951 additions and 406 deletions

View File

@ -12,5 +12,5 @@ script:
- ./gradlew clean assembleDebug assembleRelease testDebug
before_install:
- yes | sdkmanager "platforms;android-28"
- yes | sdkmanager "build-tools;28.0.3"
- yes | sdkmanager "platforms;android-29"
- yes | sdkmanager "build-tools;29.0.2"

View File

@ -1,5 +1,31 @@
#v1.1.0
- OPML import/export for local account
- Dark theme
- Share or download item image
- Open item in webview
- Minor bug fixes and improvements
#v1.0.2.2
Disable Proguard as it makes fail some functionalities.
#v1.0.2.1
Fix a crash related to Proguard Rules.
#v1.0.2
- Add swipe background to main list items
- Add preference to parse a fixed number of items when adding a local feed
- Change feed/folders way to interact
- Minor bug fixes and improvements
# 1.0 Initial release
# v1.0.1
# v1.0 Initial release
- Local RSS parsing
- RSS 2.0, ATOM and JSON formats support

View File

@ -4,19 +4,31 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId "com.readrops.app"
minSdkVersion 21
targetSdkVersion 28
versionCode 5
versionName "1.0.2.2"
targetSdkVersion 29
versionCode 6
versionName "1.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = [
"room.incremental": "true"
]
}
}
}
testOptions {
unitTests.returnDefaultValues = true
}
buildTypes {
release {
minifyEnabled false // proguard makes some functionalities fail so It's disabled until I find the problem source
shrinkResources false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
@ -46,10 +58,12 @@ dependencies {
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.palette:palette:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.preference:preference:1.1.0'
implementation "androidx.core:core-ktx:1.1.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
@ -66,14 +80,14 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
kapt 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
implementation 'androidx.room:room-runtime:2.1.0'
kapt 'androidx.room:room-compiler:2.1.0'
implementation 'android.arch.persistence.room:rxjava2:1.1.1'
implementation 'androidx.room:room-runtime:2.2.2'
kapt 'androidx.room:room-compiler:2.2.2'
implementation 'androidx.room:room-rxjava2:2.2.2'
implementation 'androidx.paging:paging-runtime:2.1.0'
implementation 'androidx.paging:paging-common:2.1.0'
implementation 'joda-time:joda-time:2.10.3'
implementation 'joda-time:joda-time:2.10.5'
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
@ -83,11 +97,7 @@ dependencies {
implementation 'com.mikepenz:fastadapter:3.3.1'
implementation 'com.mikepenz:fastadapter-commons:3.3.0'
implementation 'com.mikepenz:materialdrawer:6.1.2'
implementation "com.mikepenz:aboutlibraries:6.2.3"
implementation 'com.facebook.stetho:stetho:1.5.1'
implementation "androidx.core:core-ktx:1.1.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "com.mikepenz:aboutlibraries:6.2.3"
}

View File

@ -23,3 +23,10 @@
-dontwarn org.xmlpull.v1.XmlPullParser
-dontwarn org.xmlpull.v1.XmlSerializer
-keep class org.xmlpull.v1.* {*;}
-keep class org.simpleframework.xml.** { *; }
-keep class com.readrops.readropslibrary.services.freshrss.json.** { *; }
-keep class com.readrops.readropslibrary.services.nextcloudnews.json.** { *; }
-keep class com.readrops.readropslibrary.localfeed.** { *; }

View File

@ -5,9 +5,11 @@
<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" />
<application
android:name=".utils.ReadropsApp"
android:name=".ReadropsApp"
android:allowBackup="true"
android:icon="@drawable/ic_readrops"
android:label="@string/app_name"
@ -15,10 +17,26 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning">
android:requestLegacyExternalStorage="true"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<provider
android:authorities="${applicationId}"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity
android:name=".activities.WebViewActivity"
android:theme="@style/AppTheme.NoActionBar" />
<service android:name=".utils.feedscolors.FeedsColorsIntentService" />
<activity android:name=".activities.SettingsActivity" />
<activity
android:name=".activities.SplashActivity"
android:theme="@style/SplashTheme">
@ -28,32 +46,27 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".activities.AccountTypeListActivity" />
<activity
android:name=".activities.AddAccountActivity"
android:label="@string/add_account" />
<activity
android:name=".activities.ManageFeedsFoldersActivity"
android:label="@string/manage_feeds_folders"
android:parentActivityName=".activities.MainActivity"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".activities.MainActivity"
android:label="@string/articles"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".activities.ItemActivity"
android:parentActivityName=".activities.MainActivity"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".activities.AddFeedActivity"
android:label="@string/add_feed_title"
android:parentActivityName=".activities.MainActivity" />
</application>
</manifest>

View File

@ -0,0 +1,57 @@
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.facebook.stetho.Stetho;
import com.readrops.app.utils.SharedPreferencesManager;
import io.reactivex.plugins.RxJavaPlugins;
public class ReadropsApp extends Application {
public static final String FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel";
public static final String OPML_EXPORT_CHANNEL_ID = "opmlExportChannel";
@Override
public void onCreate() {
super.onCreate();
RxJavaPlugins.setErrorHandler(e -> {
});
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this);
}
createNotificationChannels();
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
if (Boolean.valueOf(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME)))
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
else
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
private void createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel feedsColorsChannel = new NotificationChannel(FEEDS_COLORS_CHANNEL_ID,
getString(R.string.feeds_colors), NotificationManager.IMPORTANCE_DEFAULT);
feedsColorsChannel.setDescription(getString(R.string.get_feeds_colors));
NotificationChannel opmlExportChannel = new NotificationChannel(OPML_EXPORT_CHANNEL_ID,
getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT);
opmlExportChannel.setDescription(getString(R.string.opml_export_description));
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(feedsColorsChannel);
manager.createNotificationChannel(opmlExportChannel);
}
}
}

View File

@ -1,34 +1,47 @@
package com.readrops.app.activities;
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.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.app.R;
import com.readrops.app.adapters.AccountTypeListAdapter;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.entities.account.AccountType;
import com.readrops.app.databinding.ActivityAccountTypeListBinding;
import com.readrops.app.utils.Utils;
import com.readrops.app.viewmodels.AccountViewModel;
import com.readrops.app.adapters.AccountTypeListAdapter;
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.fragments.settings.AccountSettingsFragment.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;
public class AccountTypeListActivity extends AppCompatActivity {
private static final String TAG = AccountTypeListActivity.class.getSimpleName();
private ActivityAccountTypeListBinding binding;
private AccountTypeListAdapter adapter;
private AccountViewModel viewModel;
@ -47,24 +60,43 @@ public class AccountTypeListActivity extends AppCompatActivity {
binding.accountTypeRecyclerview.setLayoutManager(new LinearLayoutManager(this));
binding.accountTypeRecyclerview.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
fromMainActivity = getIntent().getBooleanExtra("fromMainActivity", false);
fromMainActivity = getIntent().getBooleanExtra(FROM_MAIN_ACTIVITY, false);
if (fromMainActivity)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
adapter = new AccountTypeListAdapter(accountType -> {
if (!(accountType == AccountType.LOCAL)) {
if (accountType != AccountType.LOCAL) {
Intent intent = new Intent(getApplicationContext(), AddAccountActivity.class);
if (fromMainActivity)
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
intent.putExtra("accountType", (Parcelable) accountType);
intent.putExtra(ACCOUNT_TYPE, (Parcelable) accountType);
startActivity(intent);
finish();
} else
createNewLocalAccount(accountType);
} else {
Account account = new Account(null, getString(AccountType.LOCAL.getName()), 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());
}
});
}
});
@ -82,39 +114,6 @@ public class AccountTypeListActivity extends AppCompatActivity {
return accountTypes;
}
private void createNewLocalAccount(AccountType accountType) {
Account account = new Account(null, getString(accountType.getName()), accountType);
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());
if (fromMainActivity) {
Intent intent = new Intent();
intent.putExtra(MainActivity.ACCOUNT_KEY, account);
setResult(RESULT_OK, intent);
} else {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.putExtra(MainActivity.ACCOUNT_KEY, account);
startActivity(intent);
}
finish();
}
@Override
public void onError(Throwable e) {
Utils.showSnackbar(binding.accountTypeListRoot, e.getMessage());
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@ -125,4 +124,75 @@ public class AccountTypeListActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
public void openOPMLFile(View view) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/*");
startActivityForResult(intent, OPEN_OPML_FILE_REQUEST);
}
@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.getName()), AccountType.LOCAL);
account.setCurrentAccount(true);
viewModel.insert(account)
.flatMapCompletable(id -> {
account.setId(id.intValue());
viewModel.setAccount(account);
return viewModel.parseOPMLFile(uri);
})
.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

@ -26,9 +26,11 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class AddAccountActivity extends AppCompatActivity {
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;
public static final String EDIT_ACCOUNT = "EDIT_ACCOUNT";
public class AddAccountActivity extends AppCompatActivity {
private ActivityAddAccountBinding binding;
private AccountViewModel viewModel;
@ -45,7 +47,7 @@ public class AddAccountActivity extends AppCompatActivity {
binding = DataBindingUtil.setContentView(this, R.layout.activity_add_account);
viewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
accountType = getIntent().getParcelableExtra("accountType");
accountType = getIntent().getParcelableExtra(ACCOUNT_TYPE);
int flag = getIntent().getFlags();
forwardResult = flag == Intent.FLAG_ACTIVITY_FORWARD_RESULT;
@ -114,13 +116,13 @@ public class AddAccountActivity extends AppCompatActivity {
if (forwardResult) {
Intent intent = new Intent();
intent.putExtra(MainActivity.ACCOUNT_KEY, account);
intent.putExtra(ACCOUNT, account);
setResult(RESULT_OK, intent);
finish();
} else {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.putExtra(MainActivity.ACCOUNT_KEY, account);
intent.putExtra(ACCOUNT, account);
startActivity(intent);
}
@ -250,7 +252,7 @@ public class AddAccountActivity extends AppCompatActivity {
createAccount(null);
return true;
}
return super.onKeyUp(keyCode, event);
}
}

View File

@ -42,6 +42,8 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
public class AddFeedActivity extends AppCompatActivity implements View.OnClickListener {
private AccountArrayAdapter arrayAdapter;
@ -311,7 +313,7 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi
public void finish() {
if (!feedsToUpdate.isEmpty()) {
Intent intent = new Intent();
intent.putParcelableArrayListExtra("feedIds", feedsToUpdate);
intent.putParcelableArrayListExtra(FEEDS, feedsToUpdate);
setResult(RESULT_OK, intent);
}

View File

@ -1,22 +1,35 @@
package com.readrops.app.activities;
import android.app.DownloadManager;
import android.content.Intent;
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.util.Log;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebView;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
import androidx.core.app.ShareCompat;
import androidx.lifecycle.ViewModelProviders;
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.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@ -26,11 +39,19 @@ import com.readrops.app.database.pojo.ItemWithFeed;
import com.readrops.app.utils.DateUtils;
import com.readrops.app.utils.GlideApp;
import com.readrops.app.utils.ReadropsWebView;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.app.viewmodels.ItemViewModel;
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 ItemViewModel viewModel;
private TextView date;
private TextView title;
@ -45,9 +66,6 @@ public class ItemActivity extends AppCompatActivity {
private FloatingActionButton actionButton;
private ReadropsWebView webView;
public static final String ITEM_ID = "itemId";
public static final String IMAGE_URL = "imageUrl";
private ItemWithFeed itemWithFeed;
private boolean appBarCollapsed;
@ -81,6 +99,8 @@ public class ItemActivity extends AppCompatActivity {
readTimeLayout = findViewById(R.id.activity_item_readtime_layout);
dateLayout = findViewById(R.id.activity_item_date_layout);
registerForContextMenu(webView);
if (imageUrl == null) {
getSupportActionBar().setDisplayShowTitleEnabled(false);
toolbarLayout.setTitleEnabled(false);
@ -111,9 +131,9 @@ public class ItemActivity extends AppCompatActivity {
}
}));
viewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication()).create(ItemViewModel.class);
viewModel = ViewModelProviders.of(this).get(ItemViewModel.class);
viewModel.getItemById(itemId).observe(this, this::bindUI);
actionButton.setOnClickListener(v -> openLink());
actionButton.setOnClickListener(v -> openInNavigator());
}
private void bindUI(ItemWithFeed itemWithFeed) {
@ -188,11 +208,7 @@ public class ItemActivity extends AppCompatActivity {
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem item = menu.findItem(R.id.item_open);
if (appBarCollapsed)
item.setVisible(true);
else
item.setVisible(false);
item.setVisible(appBarCollapsed);
return super.onPrepareOptionsMenu(menu);
}
@ -207,10 +223,16 @@ public class ItemActivity extends AppCompatActivity {
shareArticle();
return true;
case R.id.item_open:
openLink();
int value = Integer.valueOf(SharedPreferencesManager.readString(this,
SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN));
if (value == 0)
openInNavigator();
else
openInWebView();
return true;
default:
return super.onOptionsItemSelected(item);
}
return super.onOptionsItemSelected(item);
}
@Override
@ -219,15 +241,86 @@ public class ItemActivity extends AppCompatActivity {
super.onBackPressed();
}
private void openLink() {
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.getColor() != 0 ? itemWithFeed.getColor() : itemWithFeed.getBgColor());
startActivity(intent);
}
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)));
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_article)));
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
WebView.HitTestResult hitTestResult = webView.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) -> {
if (position == 0)
shareImage(hitTestResult.getExtra());
else
downloadImage(hitTestResult.getExtra());
})
.show();
}
}
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) {
GlideApp.with(this)
.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);
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

@ -43,7 +43,6 @@ import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.pojo.ItemWithFeed;
import com.readrops.app.fragments.settings.AccountSettingsFragment;
import com.readrops.app.utils.DrawerManager;
import com.readrops.app.utils.GlideApp;
import com.readrops.app.utils.ReadropsItemTouchCallback;
@ -64,6 +63,14 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
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;
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener,
ReadropsItemTouchCallback.SwipeCallback, ActionMode.Callback {
@ -74,10 +81,6 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
public static final int ITEM_REQUEST = 3;
public static final int ADD_ACCOUNT_REQUEST = 4;
public static final String ACCOUNT_KEY = "account";
private static final String SYNCING_KEY = "SYNCING";
private RecyclerView recyclerView;
private MainItemListAdapter adapter;
private SwipeRefreshLayout refreshLayout;
@ -158,14 +161,14 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
switch (id) {
case DrawerManager.ADD_ACCOUNT_ID:
Intent intent = new Intent(this, AccountTypeListActivity.class);
intent.putExtra("fromMainActivity", true);
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(SettingsActivity.SETTINGS_KEY,
intent1.putExtra(SETTINGS,
SettingsActivity.SettingsKey.ACCOUNT_SETTINGS.ordinal());
intent1.putExtra(AccountSettingsFragment.ACCOUNT, viewModel.getCurrentAccount());
intent1.putExtra(ACCOUNT, viewModel.getCurrentAccount());
startActivity(intent1);
break;
default:
@ -177,16 +180,16 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
}
} else {
Intent intent = new Intent(this, SettingsActivity.class);
intent.putExtra(SettingsActivity.SETTINGS_KEY,
intent.putExtra(SETTINGS,
SettingsActivity.SettingsKey.ACCOUNT_SETTINGS.ordinal());
intent.putExtra(AccountSettingsFragment.ACCOUNT, viewModel.getCurrentAccount());
intent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
startActivityForResult(intent, MANAGE_ACCOUNT_REQUEST);
}
return true;
});
Account currentAccount = getIntent().getParcelableExtra(MainActivity.ACCOUNT_KEY);
Account currentAccount = getIntent().getParcelableExtra(ACCOUNT);
WeakReference<Account> accountWeakReference = new WeakReference<>(currentAccount);
viewModel.getAllAccounts().observe(this, accounts -> {
@ -210,7 +213,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
refreshLayout.setRefreshing(true);
onRefresh();
accountWeakReference.clear();
} else if (currentAccount == null && savedInstanceState != null && savedInstanceState.getBoolean(SYNCING_KEY)) {
} else if (currentAccount == null && savedInstanceState != null && savedInstanceState.getBoolean(SYNCING)) {
refreshLayout.setRefreshing(true);
onRefresh();
savedInstanceState.clear();
@ -239,7 +242,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
break;
case DrawerManager.SETTINGS_ID:
Intent intent = new Intent(getApplication(), SettingsActivity.class);
intent.putExtra(SettingsActivity.SETTINGS_KEY,
intent.putExtra(SETTINGS,
SettingsActivity.SettingsKey.SETTINGS.ordinal());
startActivity(intent);
break;
@ -289,8 +292,8 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
if (actionMode == null) {
Intent intent = new Intent(getApplicationContext(), ItemActivity.class);
intent.putExtra(ItemActivity.ITEM_ID, itemWithFeed.getItem().getId());
intent.putExtra(ItemActivity.IMAGE_URL, itemWithFeed.getItem().getImageLink());
intent.putExtra(ITEM_ID, itemWithFeed.getItem().getId());
intent.putExtra(IMAGE_URL, itemWithFeed.getItem().getImageLink());
startActivityForResult(intent, ITEM_REQUEST);
viewModel.setItemReadState(itemWithFeed, true)
@ -312,7 +315,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
@Override
public void onItemLongClick(ItemWithFeed itemWithFeed, int position) {
if (actionMode != null)
if (actionMode != null || refreshLayout.isRefreshing())
return;
selectedItemWithFeed = itemWithFeed;
@ -501,7 +504,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == ADD_FEED_REQUEST && resultCode == RESULT_OK) {
if (data != null) {
ArrayList<Feed> feeds = data.getParcelableArrayListExtra("feedIds");
ArrayList<Feed> feeds = data.getParcelableArrayListExtra(FEEDS);
if (feeds != null && feeds.size() > 0 && viewModel.isAccountLocal()) {
refreshLayout.setRefreshing(true);
@ -515,7 +518,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
} else if (requestCode == ADD_ACCOUNT_REQUEST && resultCode == RESULT_OK) {
if (data != null) {
Account newAccount = data.getParcelableExtra(ACCOUNT_KEY);
Account newAccount = data.getParcelableExtra(ACCOUNT);
if (newAccount != null) {
viewModel.addAccount(newAccount);
@ -667,6 +670,12 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
}
private void startAboutActivity() {
Libs.ActivityStyle activityStyle;
if (Boolean.valueOf(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME)))
activityStyle = Libs.ActivityStyle.DARK;
else
activityStyle = Libs.ActivityStyle.LIGHT_DARK_TOOLBAR;
new LibsBuilder()
.withAboutIconShown(true)
.withAboutVersionShown(true)
@ -675,7 +684,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
.withLicenseShown(true)
.withLicenseDialog(false)
.withActivityTitle(getString(R.string.about))
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
.withActivityStyle(activityStyle)
.withFields(R.string.class.getFields())
.start(this);
}
@ -691,7 +700,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
@Override
protected void onSaveInstanceState(Bundle outState) {
if (refreshLayout.isRefreshing())
outState.putBoolean(SYNCING_KEY, true);
outState.putBoolean(SYNCING, true);
super.onSaveInstanceState(outState);
}

View File

@ -11,7 +11,6 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.lifecycle.ViewModelProviders;
import androidx.viewpager.widget.ViewPager;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.app.R;
@ -28,12 +27,12 @@ import com.readrops.readropslibrary.utils.UnknownFormatException;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
public class ManageFeedsFoldersActivity extends AppCompatActivity {
public static final String ACCOUNT = "ACCOUNT";
private ActivityManageFeedsFoldersBinding binding;
private FeedsFoldersPageAdapter pageAdapter;
private ManageFeedsFoldersViewModel viewModel;
private Account account;
@ -48,30 +47,13 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity {
account = getIntent().getParcelableExtra(ACCOUNT);
pageAdapter = new FeedsFoldersPageAdapter(getSupportFragmentManager());
FeedsFoldersPageAdapter pageAdapter = new FeedsFoldersPageAdapter(getSupportFragmentManager());
binding.manageFeedsFoldersViewpager.setAdapter(pageAdapter);
binding.manageFeedsFoldersTablayout.setupWithViewPager(binding.manageFeedsFoldersViewpager);
viewModel = ViewModelProviders.of(this).get(ManageFeedsFoldersViewModel.class);
viewModel.setAccount(account);
binding.manageFeedsFoldersViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
binding.manageFeedsFoldersTablayout.getTabAt(position).select();
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
@ -92,8 +74,9 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity {
case R.id.add_folder:
addFolder();
return true;
default:
return super.onOptionsItemSelected(item);
}
return super.onOptionsItemSelected(item);
}
@Override
@ -132,7 +115,7 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity {
public class FeedsFoldersPageAdapter extends FragmentPagerAdapter {
private FeedsFoldersPageAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
@Override

View File

@ -11,9 +11,10 @@ import com.readrops.app.database.entities.account.Account;
import com.readrops.app.fragments.settings.AccountSettingsFragment;
import com.readrops.app.fragments.settings.SettingsFragment;
public class SettingsActivity extends AppCompatActivity {
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.SETTINGS;
public static final String SETTINGS_KEY = SettingsKey.class.getSimpleName().toUpperCase();
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -22,9 +23,9 @@ public class SettingsActivity extends AppCompatActivity {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Account account = getIntent().getParcelableExtra(AccountSettingsFragment.ACCOUNT);
Account account = getIntent().getParcelableExtra(ACCOUNT);
SettingsKey settingsKey = SettingsKey.values()[getIntent().getIntExtra(SETTINGS_KEY, -1)];
SettingsKey settingsKey = SettingsKey.values()[getIntent().getIntExtra(SETTINGS, -1)];
Fragment fragment = null;
switch (settingsKey) {

View File

@ -0,0 +1,123 @@
package com.readrops.app.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.PorterDuff
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 androidx.databinding.DataBindingUtil
import com.readrops.app.R
import com.readrops.app.databinding.ActivityWebViewBinding
import com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR
import com.readrops.app.utils.ReadropsKeys.WEB_URL
class WebViewActivity : AppCompatActivity() {
private lateinit var binding: ActivityWebViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_web_view)
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()
binding.activityWebViewSwipe.setOnRefreshListener { binding.webView.reload() }
binding.activityWebViewProgress.indeterminateDrawable.setColorFilter(actionBarColor, PorterDuff.Mode.SRC_IN)
binding.activityWebViewProgress.max = 100
val url: String = intent.getStringExtra(WEB_URL)
binding.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?) {
binding.activityWebViewSwipe.isRefreshing = false
binding.activityWebViewProgress.progress = 0
binding.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) {
binding.activityWebViewProgress.progress = newProgress
if (newProgress == 100)
binding.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)
intent.type = "text/plain"
intent.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

@ -2,7 +2,6 @@ package com.readrops.app.adapters;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.LayoutInflater;
@ -38,6 +37,7 @@ 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> {
@ -63,18 +63,18 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
@Override
public boolean areContentsTheSame(@NonNull ItemWithFeed itemWithFeed, @NonNull ItemWithFeed t1) {
Item item = itemWithFeed.getItem();
Item item1 = t1.getItem();
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 item.getTitle().equals(item1.getTitle()) &&
return oldItem.getTitle().equals(newItem.getTitle()) &&
itemWithFeed.getFeedName().equals(t1.getFeedName()) &&
folder &&
item.isRead() == item1.isRead() &&
item.isReadItLater() == item1.isReadItLater() &&
oldItem.isRead() == newItem.isRead() &&
oldItem.isReadItLater() == newItem.isReadItLater() &&
itemWithFeed.getColor() == t1.getColor() &&
itemWithFeed.getBgColor() == t1.getBgColor();
}
@ -87,7 +87,7 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
private static final DrawableCrossFadeFactory FADE_FACTORY = new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build();
private static final RequestOptions REQUEST_OPTIONS = new RequestOptions().transforms(new CenterCrop(), new RoundedCorners(16));
private static final RequestOptions REQUEST_OPTIONS = new RequestOptions().transform(new CenterCrop(), new RoundedCorners(16));
@NonNull
@Override
@ -102,7 +102,7 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.size() > 0) {
if (!payloads.isEmpty()) {
ItemWithFeed itemWithFeed = (ItemWithFeed) payloads.get(0);
holder.bind(itemWithFeed);
@ -171,7 +171,7 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
}
}
public LinkedHashSet<Integer> getSelection() {
public Set<Integer> getSelection() {
return selection;
}
@ -250,7 +250,7 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
public class ItemViewHolder extends RecyclerView.ViewHolder {
private ListItemBinding binding;
View[] alphaViews;
private View[] alphaViews;
ItemViewHolder(ListItemBinding binding) {
super(binding.getRoot());
@ -352,16 +352,17 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
private void setSelected(boolean selected) {
Context context = itemView.getContext();
TypedValue outValue = new TypedValue();
if (selected) {
itemView.setBackground(new ColorDrawable(ContextCompat.getColor(context, R.color.selected_background)));
context.getTheme().resolveAttribute(
android.R.attr.colorControlHighlight, outValue, true);
} else {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(
android.R.attr.selectableItemBackground, outValue, true);
itemView.setBackgroundResource(outValue.resourceId);
}
itemView.setBackgroundResource(outValue.resourceId);
}
public ImageView getItemImage() {

View File

@ -0,0 +1,76 @@
package com.readrops.app.database;
import androidx.annotation.NonNull;
import androidx.paging.DataSource;
import androidx.paging.PositionalDataSource;
/**
* Workaround class to avoid item recycler view scrolling down when updating data
* This class is to keep until a new version of androidx paging is released with
* bug https://issuetracker.google.com/issues/123834703 merged.
* @param <T>
*/
public class RoomFactoryWrapper<T> extends DataSource.Factory<Integer, T> {
final DataSource.Factory<Integer, T> m_wrappedFactory;
public RoomFactoryWrapper(@NonNull DataSource.Factory<Integer, T> wrappedFactory) {
m_wrappedFactory = wrappedFactory;
}
@NonNull
@Override
public DataSource<Integer, T> create() {
return new DataSourceWrapper<>((PositionalDataSource<T>) m_wrappedFactory.create());
}
public static class DataSourceWrapper<T> extends PositionalDataSource<T> {
final PositionalDataSource<T> m_wrappedSource;
DataSourceWrapper(PositionalDataSource<T> wrappedSource) {
m_wrappedSource = wrappedSource;
}
@Override
public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
m_wrappedSource.addInvalidatedCallback(onInvalidatedCallback);
}
@Override
public void removeInvalidatedCallback(
@NonNull InvalidatedCallback onInvalidatedCallback) {
m_wrappedSource.removeInvalidatedCallback(onInvalidatedCallback);
}
@Override
public void invalidate() {
m_wrappedSource.invalidate();
}
@Override
public boolean isInvalid() {
return m_wrappedSource.isInvalid();
}
@Override
public void loadInitial(@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback<T> callback) {
// Workaround for paging bug: https://issuetracker.google.com/issues/123834703
// edit initial load position to start 1/2 load ahead of requested position
int newStartPos = params.placeholdersEnabled
? params.requestedStartPosition
: Math.max(0, params.requestedStartPosition - (params.requestedLoadSize / 2));
m_wrappedSource.loadInitial(new LoadInitialParams(
newStartPos,
params.requestedLoadSize,
params.pageSize,
params.placeholdersEnabled
), callback);
}
@Override
public void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback<T> callback) {
m_wrappedSource.loadRange(params, callback);
}
}
}

View File

@ -7,11 +7,17 @@ import androidx.room.Update;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Single;
public interface BaseDao<T> {
@Insert
long insert(T entity); // can't turn return type to Single<Long> while some repositories can't use rxjava properly
Single<Long> insert(T entity);
// only here for compatibility with LocalFeedRepository
// which hasn't been written with rxjava usage in mind
@Insert
long compatInsert(T entity);
@Insert
List<Long> insert(List<T> entities);

View File

@ -4,10 +4,11 @@ package com.readrops.app.database.dao;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Query;
import androidx.room.RoomWarnings;
import androidx.room.Transaction;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.pojo.FeedWithFolder;
import java.util.ArrayList;
@ -19,7 +20,10 @@ import io.reactivex.Single;
public abstract class FeedDao implements BaseDao<Feed> {
@Query("Select * from Feed Where account_id = :accountId order by name ASC")
public abstract List<Feed> getAllFeeds(int accountId);
public abstract List<Feed> getFeeds(int accountId);
@Query("Select * from Feed Order By name ASC")
public abstract LiveData<List<Feed>> getAllFeeds();
@Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end")
public abstract boolean feedExists(String feedUrl, int accountId);
@ -54,6 +58,7 @@ public abstract class FeedDao implements BaseDao<Feed> {
@Query("Update Feed set text_color = :textColor, background_color = :bgColor Where id = :feedId")
public abstract void updateColors(int feedId, int textColor, int bgColor);
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("Select Feed.name as feed_name, Feed.id as feed_id, Folder.name as folder_name, Folder.id as folder_id, Folder.remoteId as folder_remoteId," +
"Feed.description as feed_description, Feed.icon_url as feed_icon_url, Feed.url as feed_url, Feed.folder_id as feed_folder_id" +
", Feed.siteUrl as feed_siteUrl, Feed.remoteId as feed_remoteId from Feed Left Join Folder on Feed.folder_id = Folder.id Where Feed.account_id = :accountId Order by Feed.name")

View File

@ -39,6 +39,9 @@ public abstract class FolderDao implements BaseDao<Folder> {
@Query("Delete From Folder Where remoteId in (:ids)")
abstract void deleteByIds(List<String> ids);
@Query("Select * From Folder Where name = :name And account_id = :accountId")
public abstract Folder getFolderByName(String name, int accountId);
/**
* Insert, update and delete folders
*

View File

@ -6,6 +6,7 @@ import androidx.paging.DataSource;
import androidx.room.Dao;
import androidx.room.Query;
import androidx.room.RawQuery;
import androidx.room.RoomWarnings;
import androidx.sqlite.db.SupportSQLiteQuery;
import com.readrops.app.database.entities.Feed;
@ -51,6 +52,7 @@ public abstract class ItemDao implements BaseDao<Item> {
@Query("Select count(*) From Item Where feed_id = :feedId And read = 0")
public abstract int getUnreadCount(int feedId);
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("Select title, Item.description, content, link, pub_date, image_link, author, read, text_color, " +
"background_color, read_time, Feed.name, Feed.id as feedId, siteUrl, Folder.id as folder_id, " +
"Folder.name as folder_name from Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id Where Item.id = :id")
@ -64,4 +66,7 @@ public abstract class ItemDao implements BaseDao<Item> {
@Query("Update Item set read_changed = 0 Where feed_id in (Select id From Feed Where account_id = :accountId)")
public abstract void resetReadChanges(int accountId);
@Query("Update Item set read = :read Where remoteId = :remoteId")
public abstract void setReadState(String remoteId, boolean read);
}

View File

@ -15,7 +15,6 @@ import androidx.lifecycle.ViewModelProviders;
import com.google.android.material.textfield.TextInputEditText;
import com.readrops.app.R;
import com.readrops.app.activities.ManageFeedsFoldersActivity;
import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.account.Account;
@ -29,7 +28,7 @@ import java.util.TreeMap;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.activities.ManageFeedsFoldersActivity.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
public class EditFeedDialogFragment extends DialogFragment implements AdapterView.OnItemSelectedListener {
@ -63,7 +62,7 @@ public class EditFeedDialogFragment extends DialogFragment implements AdapterVie
viewModel = ViewModelProviders.of(getActivity()).get(ManageFeedsFoldersViewModel.class);
feedWithFolder = getArguments().getParcelable("feedWithFolder");
account = getArguments().getParcelable(ManageFeedsFoldersActivity.ACCOUNT);
account = getArguments().getParcelable(ACCOUNT);
viewModel.setAccount(account);

View File

@ -9,10 +9,10 @@ import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.readrops.app.R
import com.readrops.app.activities.ManageFeedsFoldersActivity.ACCOUNT
import com.readrops.app.database.entities.account.Account
import com.readrops.app.database.pojo.FeedWithFolder
import com.readrops.app.databinding.FeedOptionsLayoutBinding
import com.readrops.app.utils.ReadropsKeys.ACCOUNT
class FeedOptionsDialogFragment : BottomSheetDialogFragment() {
@ -21,7 +21,7 @@ class FeedOptionsDialogFragment : BottomSheetDialogFragment() {
private lateinit var binding: FeedOptionsLayoutBinding
companion object {
val FEED_KEY = "FEED_KEY"
const val FEED_KEY = "FEED_KEY"
fun newInstance(feedWithFolder: FeedWithFolder, account: Account): FeedOptionsDialogFragment {
val bundle = Bundle()

View File

@ -20,7 +20,6 @@ import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.pojo.FeedWithFolder;
import com.readrops.app.databinding.FragmentFeedsBinding;
import com.readrops.app.fragments.settings.AccountSettingsFragment;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
@ -29,7 +28,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.activities.ManageFeedsFoldersActivity.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
public class FeedsFragment extends Fragment {
@ -48,7 +47,7 @@ public class FeedsFragment extends Fragment {
FeedsFragment fragment = new FeedsFragment();
Bundle args = new Bundle();
args.putParcelable(AccountSettingsFragment.ACCOUNT, account);
args.putParcelable(ACCOUNT, account);
fragment.setArguments(args);
return fragment;

View File

@ -16,7 +16,7 @@ class FolderOptionsDialogFragment : BottomSheetDialogFragment() {
private lateinit var foldersOptionsLayoutBinding: FolderOptionsLayoutBinding
companion object {
val FOLDER_KEY = "FOLDER_KEY"
const val FOLDER_KEY = "FOLDER_KEY"
fun newInstance(folder: Folder): FolderOptionsDialogFragment {
val args = Bundle()

View File

@ -16,7 +16,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.app.R;
import com.readrops.app.activities.ManageFeedsFoldersActivity;
import com.readrops.app.adapters.FoldersAdapter;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.account.Account;
@ -30,6 +29,8 @@ import com.readrops.readropslibrary.utils.UnknownFormatException;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
public class FoldersFragment extends Fragment {
private FoldersAdapter adapter;
@ -46,7 +47,7 @@ public class FoldersFragment extends Fragment {
FoldersFragment fragment = new FoldersFragment();
Bundle args = new Bundle();
args.putParcelable(ManageFeedsFoldersActivity.ACCOUNT, account);
args.putParcelable(ACCOUNT, account);
fragment.setArguments(args);
return fragment;
@ -56,7 +57,7 @@ public class FoldersFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
account = getArguments().getParcelable(ManageFeedsFoldersActivity.ACCOUNT);
account = getArguments().getParcelable(ACCOUNT);
if (account.getLogin() == null)
account.setLogin(SharedPreferencesManager.readString(getContext(), account.getLoginKey()));

View File

@ -1,11 +1,22 @@
package com.readrops.app.fragments.settings;
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.Bundle;
import android.widget.Toast;
import android.os.Environment;
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.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.preference.Preference;
@ -13,21 +24,39 @@ import androidx.preference.PreferenceFragmentCompat;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.app.R;
import com.readrops.app.ReadropsApp;
import com.readrops.app.activities.AddAccountActivity;
import com.readrops.app.activities.ManageFeedsFoldersActivity;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.entities.account.AccountType;
import com.readrops.app.utils.Utils;
import com.readrops.app.utils.matchers.OPMLMatcher;
import com.readrops.app.viewmodels.AccountViewModel;
import com.readrops.readropslibrary.opml.OPMLParser;
import com.readrops.readropslibrary.opml.model.OPML;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.schedulers.Schedulers;
import static android.app.Activity.RESULT_OK;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
/**
* A simple {@link Fragment} subclass.
*/
public class AccountSettingsFragment extends PreferenceFragmentCompat {
public static final String ACCOUNT = "ACCOUNT";
private static final String TAG = AccountSettingsFragment.class.getSimpleName();
public static final int OPEN_OPML_FILE_REQUEST = 1;
private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1;
private Account account;
private AccountViewModel viewModel;
@ -50,9 +79,18 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
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");
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);
@ -65,7 +103,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
credentialsPref.setOnPreferenceClickListener(preference -> {
if (!account.isLocal()) {
Intent intent = new Intent(getContext(), AddAccountActivity.class);
intent.putExtra(AddAccountActivity.EDIT_ACCOUNT, account);
intent.putExtra(EDIT_ACCOUNT, account);
startActivity(intent);
}
@ -76,6 +114,23 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
deleteAccount();
return true;
});
opmlPref.setOnPreferenceClickListener(preference -> {
new MaterialDialog.Builder(getActivity())
.items(R.array.opml_import_export)
.itemsCallback(((dialog, itemView, position, text) -> {
if (position == 0) {
openOPMLFile();
} else {
if (isExternalStoragePermissionGranted())
exportAsOPMLFile();
else
requestExternalStoragePermission();
}
}))
.show();
return true;
});
}
@Override
@ -83,7 +138,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
super.onCreate(savedInstanceState);
viewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
account = getArguments().getParcelable(ACCOUNT);
viewModel.setAccount(account);
}
private void deleteAccount() {
@ -102,9 +157,162 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
@Override
public void onError(Throwable e) {
Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_LONG).show();
Utils.showSnackbar(getView(), e.getMessage());
}
})))
.show();
}
// region opml import
private void openOPMLFile() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/*");
startActivityForResult(intent, OPEN_OPML_FILE_REQUEST);
}
@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();
parseOPMLFile(uri, dialog);
}
super.onActivityResult(requestCode, resultCode, data);
}
private void parseOPMLFile(Uri uri, MaterialDialog dialog) {
viewModel.parseOPMLFile(uri)
.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() {
try {
String filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
File file = new File(filePath, "subscriptions.opml");
final OutputStream outputStream = new FileOutputStream(file);
viewModel.getFoldersWithFeeds()
.flatMapCompletable(folderListMap -> {
OPML opml = OPMLMatcher.INSTANCE.foldersAndFeedsToOPML(folderListMap, getContext());
return OPMLParser.write(opml, outputStream);
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doAfterTerminate(() -> {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
Utils.showSnackbar(getView(), e.getMessage());
}
})
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
displayNotification(file);
}
@Override
public void onError(Throwable e) {
Utils.showSnackbar(getView(), e.getMessage());
}
});
} catch (Exception e) {
Log.e(TAG, e.getMessage());
Utils.showSnackbar(getView(), e.getMessage());
}
}
private void displayNotification(File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(file.getAbsolutePath()));
Notification notification = new NotificationCompat.Builder(getContext(), ReadropsApp.OPML_EXPORT_CHANNEL_ID)
.setContentTitle(getString(R.string.opml_export))
.setContentText(file.getName())
.setSmallIcon(R.drawable.ic_readrops)
.setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setDeleteIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
.build();
NotificationManagerCompat manager = NotificationManagerCompat.from(getContext());
manager.notify(2, notification);
}
private boolean isExternalStoragePermissionGranted() {
return ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
private void requestExternalStoragePermission() {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE_REQUEST);
}
@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();
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
//endregion
}

View File

@ -1,15 +1,58 @@
package com.readrops.app.fragments.settings;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.readrops.app.R;
import com.readrops.app.database.Database;
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
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");
AtomicBoolean serviceStarted = new AtomicBoolean(false);
feedsColorsPreference.setOnPreferenceClickListener(preference -> {
Database database = Database.getInstance(getContext());
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) -> {
boolean darkTheme = Boolean.parseBoolean(newValue.toString());
if (darkTheme) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
return true;
});
}
}

View File

@ -1,12 +1,10 @@
package com.readrops.app.repositories;
import android.app.Application;
import android.graphics.Bitmap;
import android.util.Patterns;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.palette.graphics.Palette;
import com.readrops.app.database.Database;
import com.readrops.app.database.entities.Feed;
@ -15,17 +13,20 @@ import com.readrops.app.database.entities.Item;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.entities.account.AccountType;
import com.readrops.app.utils.FeedInsertionResult;
import com.readrops.app.utils.HtmlParser;
import com.readrops.app.utils.ParsingResult;
import com.readrops.app.utils.Utils;
import com.readrops.app.utils.feedscolors.FeedColorsKt;
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
public abstract class ARepository<T> {
@ -39,14 +40,48 @@ public abstract class ARepository<T> {
this.application = application;
this.database = Database.getInstance(application);
this.account = account;
api = createAPI();
}
protected abstract T createAPI();
public abstract Single<Boolean> login(Account account, boolean insert);
public abstract Observable<Feed> sync(List<Feed> feeds);
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());
@ -58,11 +93,8 @@ public abstract class ARepository<T> {
return database.feedDao().delete(feed);
}
public Completable addFolder(Folder folder) {
return Completable.create(emitter -> {
database.folderDao().insert(folder);
emitter.onComplete();
});
public Single<Long> addFolder(Folder folder) {
return database.folderDao().insert(folder);
}
public Completable updateFolder(Folder folder) {
@ -75,10 +107,9 @@ public abstract class ARepository<T> {
public Completable setItemReadState(Item item, boolean read) {
return database.itemDao().setReadState(item.getId(), read ? 1 : 0, !item.isReadChanged() ? 1 : 0);
}
public Completable setAllItemsReadState(Boolean read) {
public Completable setAllItemsReadState(boolean read) {
return database.itemDao().setAllItemsReadState(read ? 1 : 0, account.getId());
}
@ -90,48 +121,46 @@ public abstract class ARepository<T> {
return database.feedDao().getFeedCount(accountId);
}
protected void setFaviconUtils(List<Feed> feeds) {
Observable.<Feed>create(emitter -> {
for (Feed feed : feeds) {
setFavIconUtils(feed);
emitter.onNext(feed);
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<>(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);
}
}).subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.doOnNext(feed1 -> database.feedDao().updateColors(feed1.getId(),
feed1.getTextColor(), feed1.getBackgroundColor()))
.subscribe();
Folder noFolder = new Folder("no folder");
List<Feed> feedsWithoutFolder = database.feedDao().getFeedsWithoutFolder(account.getId());
for (Feed feed : feedsWithoutFolder) {
feed.setUnreadCount(database.itemDao().getUnreadCount(feed.getId()));
}
foldersWithFeeds.put(noFolder, feedsWithoutFolder);
emitter.onSuccess(foldersWithFeeds);
});
}
protected void setFavIconUtils(Feed feed) throws IOException {
String favUrl;
if (feed.getIconUrl() != null)
favUrl = feed.getIconUrl();
else
favUrl = HtmlParser.getFaviconLink(feed.getSiteUrl());
if (favUrl != null && Patterns.WEB_URL.matcher(favUrl).matches()) {
feed.setIconUrl(favUrl);
setFeedColors(favUrl, feed);
}
protected void setFeedColors(Feed feed) {
FeedColorsKt.setFeedColors(feed);
database.feedDao().updateColors(feed.getId(),
feed.getTextColor(), feed.getBackgroundColor());
}
protected void setFeedColors(String favUrl, Feed feed) {
Bitmap favicon = Utils.getImageFromUrl(favUrl);
protected void setFeedsColors(List<Feed> feeds) {
Intent intent = new Intent(application, FeedsColorsIntentService.class);
intent.putParcelableArrayListExtra(FEEDS, new ArrayList<>(feeds));
if (favicon != null) {
Palette palette = Palette.from(favicon).generate();
if (palette.getDominantSwatch() != null) {
feed.setTextColor(palette.getDominantSwatch().getRgb());
}
if (palette.getMutedSwatch() != null) {
feed.setBackgroundColor(palette.getMutedSwatch().getRgb());
}
}
application.startService(intent);
}
public static ARepository repositoryFactory(Account account, AccountType accountType, Application application) throws Exception {

View File

@ -2,6 +2,7 @@ package com.readrops.app.repositories;
import android.app.Application;
import android.util.Log;
import android.util.TimingLogger;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -11,8 +12,8 @@ import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.Item;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.utils.FeedInsertionResult;
import com.readrops.app.utils.FeedMatcher;
import com.readrops.app.utils.ItemMatcher;
import com.readrops.app.utils.matchers.FeedMatcher;
import com.readrops.app.utils.matchers.ItemMatcher;
import com.readrops.app.utils.ParsingResult;
import com.readrops.app.utils.Utils;
import com.readrops.readropslibrary.services.SyncType;
@ -35,12 +36,17 @@ import io.reactivex.Single;
public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
private static final String TAG = FreshRSSRepository.class.getSimpleName();
public FreshRSSRepository(@NonNull Application application, @Nullable Account account) {
super(application, account);
}
@Override
protected FreshRSSAPI createAPI() {
if (account != null)
api = new FreshRSSAPI(account.toCredentials());
return new FreshRSSAPI(account.toCredentials());
return null;
}
@Override
@ -65,8 +71,14 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
.flatMap(userInfo -> {
account.setDisplayedName(userInfo.getUserName());
if (insert)
account.setId((int) database.accountDao().insert(account));
if (insert) {
return database.accountDao().insert(account)
.flatMap(id -> {
account.setId(id.intValue());
return Single.just(true);
});
}
return Single.just(true);
});
@ -83,6 +95,8 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
} else
syncType = SyncType.INITIAL_SYNC;
TimingLogger logger = new TimingLogger(TAG, "FreshRSS sync timer");
return Single.<FreshRSSSyncData>create(emitter -> {
syncData.setReadItemsIds(database.itemDao().getReadChanges(account.getId()));
syncData.setUnreadItemsIds(database.itemDao().getUnreadChanges(account.getId()));
@ -90,14 +104,21 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
emitter.onSuccess(syncData);
}).flatMap(syncData1 -> api.sync(syncType, syncData1, account.getWriteToken()))
.flatMapObservable(syncResult -> {
insertFolders(syncResult.getFolders(), account);
insertFeeds(syncResult.getFeeds(), account);
insertItems(syncResult.getItems(), account, syncType == SyncType.INITIAL_SYNC);
logger.addSplit("server queries");
insertFolders(syncResult.getFolders());
logger.addSplit("folders insertion");
insertFeeds(syncResult.getFeeds());
logger.addSplit("feeds insertion");
insertItems(syncResult.getItems(), syncType == SyncType.INITIAL_SYNC);
logger.addSplit("items insertion");
account.setLastModified(syncResult.getLastUpdated());
database.accountDao().updateLastModified(account.getId(), syncResult.getLastUpdated());
database.itemDao().resetReadChanges(account.getId());
logger.dumpToLog();
return Observable.empty();
});
@ -149,8 +170,9 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
}
@Override
public Completable addFolder(Folder folder) {
return api.createFolder(account.getWriteToken(), folder.getName());
public Single<Long> addFolder(Folder folder) {
return api.createFolder(account.getWriteToken(), folder.getName())
.andThen(super.addFolder(folder));
}
@Override
@ -169,7 +191,7 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
.andThen(super.deleteFolder(folder));
}
private List<Feed> insertFeeds(List<FreshRSSFeed> freshRSSFeeds, Account account) {
private void insertFeeds(List<FreshRSSFeed> freshRSSFeeds) {
List<Feed> feeds = new ArrayList<>();
for (FreshRSSFeed freshRSSFeed : freshRSSFeeds) {
@ -178,16 +200,13 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
List<Long> insertedFeedsIds = database.feedDao().feedsUpsert(feeds, account);
List<Feed> insertedFeeds = new ArrayList<>();
if (!insertedFeedsIds.isEmpty()) {
insertedFeeds.addAll(database.feedDao().selectFromIdList(insertedFeedsIds));
setFaviconUtils(insertedFeeds);
setFeedsColors(database.feedDao().selectFromIdList(insertedFeedsIds));
}
return insertedFeeds;
}
private void insertFolders(List<FreshRSSFolder> freshRSSFolders, Account account) {
private void insertFolders(List<FreshRSSFolder> freshRSSFolders) {
List<Folder> folders = new ArrayList<>();
for (FreshRSSFolder freshRSSFolder : freshRSSFolders) {
@ -205,15 +224,15 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
database.folderDao().foldersUpsert(folders, account);
}
private void insertItems(List<FreshRSSItem> items, Account account, boolean initialSync) {
private void insertItems(List<FreshRSSItem> items, boolean initialSync) {
List<Item> newItems = new ArrayList<>();
for (FreshRSSItem freshRSSItem : items) {
int feedId = database.feedDao().getFeedIdByRemoteId(String.valueOf(freshRSSItem.getOrigin().getStreamId()), account.getId());
if (!initialSync && feedId > 0) {
if (database.itemDao().remoteItemExists(freshRSSItem.getId(), feedId))
break;
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(freshRSSItem.getId(), feedId)) {
database.itemDao().setReadState(freshRSSItem.getId(), freshRSSItem.isRead());
break;
}
Item item = ItemMatcher.freshRSSItemtoItem(freshRSSItem, feedId);

View File

@ -10,9 +10,9 @@ import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.Item;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.utils.FeedInsertionResult;
import com.readrops.app.utils.FeedMatcher;
import com.readrops.app.utils.matchers.FeedMatcher;
import com.readrops.app.utils.HtmlParser;
import com.readrops.app.utils.ItemMatcher;
import com.readrops.app.utils.matchers.ItemMatcher;
import com.readrops.app.utils.ParsingResult;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
@ -46,6 +46,11 @@ public class LocalFeedRepository extends ARepository<Void> {
super(application, account);
}
@Override
protected Void createAPI() {
return null;
}
@Override
public Single<Boolean> login(Account account, boolean insert) {
return null;
@ -57,7 +62,7 @@ public class LocalFeedRepository extends ARepository<Void> {
List<Feed> feedList;
if (feeds == null || feeds.size() == 0)
feedList = database.feedDao().getAllFeeds(account.getId());
feedList = database.feedDao().getFeeds(account.getId());
else
feedList = new ArrayList<>(feeds);
@ -117,7 +122,7 @@ public class LocalFeedRepository extends ARepository<Void> {
RSSQueryResult queryResult = rssNet.queryUrl(parsingResult.getUrl(), new HashMap<>());
if (queryResult != null && queryResult.getException() == null) {
Feed feed = insertFeed(queryResult.getFeed(), queryResult.getRssType());
Feed feed = insertFeed(queryResult.getFeed(), queryResult.getRssType(), parsingResult);
if (feed != null) {
insertionResult.setFeed(feed);
insertionResult.setParsingResult(parsingResult);
@ -128,8 +133,6 @@ public class LocalFeedRepository extends ARepository<Void> {
insertionResult.setInsertionError(getErrorFromException(queryResult.getException()));
insertionResults.add(insertionResult);
} else {
// error 304
}
} catch (Exception e) {
if (e instanceof IOException)
@ -147,8 +150,8 @@ public class LocalFeedRepository extends ARepository<Void> {
}
private void insertNewItems(AFeed feed, RSSQuery.RSSType type) throws ParseException {
Feed dbFeed = null;
List<Item> items = null;
Feed dbFeed;
List<Item> items;
switch (type) {
case RSS_2:
@ -163,6 +166,8 @@ public class LocalFeedRepository extends ARepository<Void> {
dbFeed = database.feedDao().getFeedByUrl(((JSONFeed) feed).getFeedUrl(), account.getId());
items = ItemMatcher.itemsFromJSON(((JSONFeed) feed).getItems(), dbFeed);
break;
default:
throw new IllegalArgumentException("Unknown RSS type");
}
database.feedDao().updateHeaders(dbFeed.getEtag(), dbFeed.getLastModified(), dbFeed.getId());
@ -175,8 +180,8 @@ public class LocalFeedRepository extends ARepository<Void> {
insertItems(items, dbFeed);
}
private Feed insertFeed(AFeed feed, RSSQuery.RSSType type) throws IOException {
Feed dbFeed = null;
private Feed insertFeed(AFeed feed, RSSQuery.RSSType type, ParsingResult parsingResult) {
Feed dbFeed;
switch (type) {
case RSS_2:
dbFeed = FeedMatcher.feedFromRSS((RSSFeed) feed);
@ -187,19 +192,23 @@ public class LocalFeedRepository extends ARepository<Void> {
case RSS_JSON:
dbFeed = FeedMatcher.feedFromJSON((JSONFeed) feed);
break;
default:
throw new IllegalArgumentException("Unknown RSS type");
}
dbFeed.setFolderId(parsingResult.getFolderId());
if (database.feedDao().feedExists(dbFeed.getUrl(), account.getId()))
return null; // feed already inserted
setFavIconUtils(dbFeed);
setFeedColors(dbFeed);
dbFeed.setAccountId(account.getId());
// we need empty headers to query the feed just after, without any 304 result
dbFeed.setEtag(null);
dbFeed.setLastModified(null);
dbFeed.setId((int) (database.feedDao().insert(dbFeed)));
dbFeed.setId((int) (database.feedDao().compatInsert(dbFeed)));
return dbFeed;
}

View File

@ -12,8 +12,8 @@ import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.Item;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.utils.FeedInsertionResult;
import com.readrops.app.utils.FeedMatcher;
import com.readrops.app.utils.ItemMatcher;
import com.readrops.app.utils.matchers.FeedMatcher;
import com.readrops.app.utils.matchers.ItemMatcher;
import com.readrops.app.utils.ParsingResult;
import com.readrops.app.utils.Utils;
import com.readrops.readropslibrary.services.SyncType;
@ -46,14 +46,19 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
public NextNewsRepository(@NonNull Application application, @Nullable Account account) {
super(application, account);
}
@Override
protected NextNewsAPI createAPI() {
if (account != null)
api = new NextNewsAPI(account.toCredentials());
return new NextNewsAPI(account.toCredentials());
return null;
}
@Override
public Single<Boolean> login(Account account, boolean insert) {
return Single.create(emitter -> {
return Single.<NextNewsUser>create(emitter -> {
if (api == null)
api = new NextNewsAPI(account.toCredentials());
else
@ -61,15 +66,23 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
NextNewsUser user = api.login();
emitter.onSuccess(user);
}).flatMap(user -> {
if (user != null) {
account.setDisplayedName(user.getDisplayName());
account.setCurrentAccount(true);
if (insert)
account.setId((int) database.accountDao().insert(account));
emitter.onSuccess(true);
if (insert) {
return database.accountDao().insert(account)
.flatMap(id -> {
account.setId(id.intValue());
return Single.just(true);
});
}
return Single.just(true);
} else
emitter.onSuccess(false);
return Single.just(false);
});
}
@ -201,8 +214,8 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
}
@Override
public Completable addFolder(Folder folder) {
return Completable.create(emitter -> {
public Single<Long> addFolder(Folder folder) {
return Single.<Folder>create(emitter -> {
try {
int folderRemoteId = folder.getRemoteId() == null ? 0 : Integer.parseInt(folder.getRemoteId());
NextNewsFolders folders = api.createFolder(new NextNewsFolder(folderRemoteId, folder.getName()));
@ -212,15 +225,13 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
folder.setName(nextNewsFolder.getName());
folder.setRemoteId(String.valueOf(nextNewsFolder.getId()));
database.folderDao().insert(folder);
emitter.onSuccess(folder);
} else
emitter.onError(new Exception("Unknown error"));
} catch (Exception e) {
emitter.onError(e);
}
emitter.onComplete();
});
}).flatMap(folder1 -> database.folderDao().insert(folder));
}
@Override
@ -269,7 +280,7 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
List<Feed> insertedFeeds = new ArrayList<>();
if (!insertedFeedsIds.isEmpty()) {
insertedFeeds.addAll(database.feedDao().selectFromIdList(insertedFeedsIds));
setFaviconUtils(insertedFeeds);
setFeedsColors(insertedFeeds);
}
return insertedFeeds;
@ -295,9 +306,9 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
for (NextNewsItem nextNewsItem : items) {
int feedId = database.feedDao().getFeedIdByRemoteId(String.valueOf(nextNewsItem.getFeedId()), account.getId());
if (!initialSync && feedId > 0) {
if (database.itemDao().remoteItemExists(String.valueOf(nextNewsItem.getId()), feedId))
break;
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(String.valueOf(nextNewsItem.getId()), feedId)) {
database.itemDao().setReadState(String.valueOf(nextNewsItem.getId()), !nextNewsItem.isUnread());
break;
}
Item item = ItemMatcher.nextNewsItemToItem(nextNewsItem, feedId);
@ -306,7 +317,9 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
newItems.add(item);
}
Collections.sort(newItems, Item::compareTo);
database.itemDao().insert(newItems);
if (!newItems.isEmpty()) {
Collections.sort(newItems, Item::compareTo);
database.itemDao().insert(newItems);
}
}
}

View File

@ -9,9 +9,11 @@ import androidx.annotation.NonNull;
import com.mikepenz.fastadapter.FastAdapter;
import com.mikepenz.fastadapter.items.AbstractItem;
import com.readrops.app.R;
import com.readrops.app.database.entities.Feed;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.ParsingResultViewHolder> {
@ -22,6 +24,8 @@ public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.Par
private boolean checked;
private Integer folderId;
public ParsingResult(String url, String label) {
this.url = url;
this.label = label;
@ -39,6 +43,18 @@ public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.Par
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;
}
@ -51,6 +67,14 @@ public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.Par
return checked;
}
public Integer getFolderId() {
return folderId;
}
public void setFolderId(Integer folderId) {
this.folderId = folderId;
}
@Override
public boolean isSelectable() {
return true;
@ -116,7 +140,18 @@ public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.Par
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,18 +0,0 @@
package com.readrops.app.utils;
import android.app.Application;
import com.facebook.stetho.Stetho;
import io.reactivex.plugins.RxJavaPlugins;
public class ReadropsApp extends Application {
@Override
public void onCreate() {
super.onCreate();
RxJavaPlugins.setErrorHandler(e -> { });
Stetho.initializeWithDefaults(this);
}
}

View File

@ -0,0 +1,22 @@
package com.readrops.app.utils
object ReadropsKeys {
const val ACCOUNT = "ACCOUNT_KEY"
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"
}

View File

@ -2,31 +2,58 @@ package com.readrops.app.utils;
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.database.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;
loadData(getText(), "text/html; charset=utf-8", "UTF-8");
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");
}
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")
@ -40,12 +67,13 @@ public class ReadropsWebView extends WebView {
}
setVerticalScrollBarEnabled(false);
setBackgroundColor(getResources().getColor(R.color.colorBackground));
setBackgroundColor(backgroundColor);
}
@Nullable
private String getText() {
if (itemWithFeed.getItem().getText() != null) {
Document document = Jsoup.parse(itemWithFeed.getItem().getText(), itemWithFeed.getWebsiteUrl());
Document document = Jsoup.parse(Parser.unescapeEntities(itemWithFeed.getItem().getText(), false), itemWithFeed.getWebsiteUrl());
formatDocument(document);
@ -53,6 +81,8 @@ public class ReadropsWebView extends WebView {
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

View File

@ -50,15 +50,17 @@ public final class SharedPreferencesManager {
public enum SharedPrefKey {
SHOW_READ_ARTICLES("show_read_articles", false),
ITEMS_TO_PARSE_MAX_NB("items_to_parse_max_nb", "20");
ITEMS_TO_PARSE_MAX_NB("items_to_parse_max_nb", "20"),
OPEN_ITEMS_IN("open_items_in", "0"),
DARK_THEME("dark_theme", "false");
@NonNull
private String key;
@NonNull
private Object defaultValue;
public Boolean getBooleanDefaultValue() {
return (Boolean) defaultValue;
public boolean getBooleanDefaultValue() {
return Boolean.valueOf(defaultValue.toString());
}
public String getStringDefaultValue() {
@ -66,7 +68,7 @@ public final class SharedPreferencesManager {
}
public int getIntDefaultValue() {
return (int) defaultValue;
return Integer.parseInt(defaultValue.toString());
}
SharedPrefKey(@NonNull String key, @NonNull Object defaultValue) {

View File

@ -15,6 +15,8 @@ import androidx.annotation.NonNull;
import com.google.android.material.snackbar.Snackbar;
import org.jsoup.Jsoup;
import java.io.InputStream;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
@ -95,4 +97,13 @@ public final class Utils {
Snackbar snackbar = Snackbar.make(root, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
/**
* Remove html tags and trim the text
* @param text string to clean
* @return cleaned text
*/
public static String cleanText(String text) {
return Jsoup.parse(text).text().trim();
}
}

View File

@ -0,0 +1,33 @@
package com.readrops.app.utils.feedscolors
import androidx.palette.graphics.Palette
import com.readrops.app.database.entities.Feed
import com.readrops.app.utils.HtmlParser
import com.readrops.app.utils.Utils
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
if (dominantSwatch != null)
feed.textColor = dominantSwatch.rgb
val mutedSwatch = palette.mutedSwatch
if (mutedSwatch != null)
feed.backgroundColor = mutedSwatch.rgb
}
}
fun getFaviconLink(feed: Feed) {
feed.iconUrl = if (feed.iconUrl != null)
feed.iconUrl
else
HtmlParser.getFaviconLink(feed.siteUrl)
}

View File

@ -0,0 +1,46 @@
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.database.Database
import com.readrops.app.database.entities.Feed
import com.readrops.app.utils.ReadropsKeys.FEEDS
class FeedsColorsIntentService : IntentService("FeedsColorsIntentService") {
override fun onHandleIntent(intent: Intent?) {
val feeds: List<Feed> = intent!!.getParcelableArrayListExtra(FEEDS)!!
val database = Database.getInstance(this)
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,7 +1,7 @@
package com.readrops.app.utils;
package com.readrops.app.utils.matchers;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.account.Account;
import com.readrops.readropslibrary.localfeed.atom.ATOMFeed;
import com.readrops.readropslibrary.localfeed.json.JSONFeed;
import com.readrops.readropslibrary.localfeed.rss.RSSChannel;

View File

@ -1,7 +1,9 @@
package com.readrops.app.utils;
package com.readrops.app.utils.matchers;
import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.Item;
import com.readrops.app.utils.DateUtils;
import com.readrops.app.utils.Utils;
import com.readrops.readropslibrary.localfeed.atom.ATOMEntry;
import com.readrops.readropslibrary.localfeed.json.JSONItem;
import com.readrops.readropslibrary.localfeed.rss.RSSEnclosure;
@ -13,7 +15,6 @@ import com.readrops.readropslibrary.utils.ParseException;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import org.jsoup.Jsoup;
import java.util.ArrayList;
import java.util.List;
@ -58,6 +59,7 @@ public final class ItemMatcher {
newItem.setLink(item.getAlternate().get(0).getHref());
newItem.setFeedId(feedId);
newItem.setRemoteId(item.getId());
newItem.setRead(item.isRead());
return newItem;
}
@ -72,7 +74,7 @@ public final class ItemMatcher {
newItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
newItem.setDescription(item.getDescription());
newItem.setGuid(item.getGuid());
newItem.setTitle(Jsoup.parse(item.getTitle()).text().trim());
newItem.setTitle(Utils.cleanText(item.getTitle()));
try {
newItem.setPubDate(DateUtils.stringToLocalDateTime(item.getDate()));
@ -117,13 +119,14 @@ public final class ItemMatcher {
dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
dbItem.setDescription(item.getSummary());
dbItem.setGuid(item.getId());
dbItem.setTitle(Jsoup.parse(item.getTitle()).text().trim());
dbItem.setTitle(Utils.cleanText(item.getTitle()));
try {
dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getUpdated()));
} catch (Exception e) {
throw new ParseException();
}
dbItem.setLink(item.getUrl());
dbItem.setFeedId(feed.getId());
@ -146,7 +149,7 @@ public final class ItemMatcher {
dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
dbItem.setDescription(item.getSummary());
dbItem.setGuid(item.getId());
dbItem.setTitle(Jsoup.parse(item.getTitle()).text().trim());
dbItem.setTitle(Utils.cleanText(item.getTitle()));
try {
dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getPubDate()));

View File

@ -0,0 +1,59 @@
package com.readrops.app.utils.matchers
import android.content.Context
import com.readrops.app.R
import com.readrops.app.database.entities.Feed
import com.readrops.app.database.entities.Folder
import com.readrops.readropslibrary.opml.model.Body
import com.readrops.readropslibrary.opml.model.Head
import com.readrops.readropslibrary.opml.model.OPML
import com.readrops.readropslibrary.opml.model.Outline
object OPMLMatcher {
fun opmltoFoldersAndFeeds(opml: OPML): Map<Folder, List<Feed>> {
val foldersAndFeeds: MutableMap<Folder, List<Feed>> = HashMap()
val body = opml.body!!
body.outlines?.forEach { outline ->
val folder = Folder(outline.title)
val feeds = arrayListOf<Feed>()
outline.outlines?.forEach { feedOutline ->
val feed = Feed().apply {
name = feedOutline.title
url = feedOutline.xmlUrl
siteUrl = feedOutline.htmlUrl
}
feeds.add(feed)
}
foldersAndFeeds[folder] = feeds
}
return foldersAndFeeds
}
fun foldersAndFeedsToOPML(foldersAndFeeds: Map<Folder, List<Feed>>, context: Context): OPML {
val outlines = arrayListOf<Outline>()
for (folderAndFeeds in foldersAndFeeds) {
val outline = Outline(folderAndFeeds.key.name)
val feedOutlines = arrayListOf<Outline>()
folderAndFeeds.value.forEach { feed ->
val feedOutline = Outline(feed.name, feed.url, feed.siteUrl)
feedOutlines.add(feedOutline)
}
outline.outlines = feedOutlines
outlines.add(outline)
}
val head = Head(context.getString(R.string.subscriptions))
val body = Body(outlines)
return OPML("2.0", head, body)
}
}

View File

@ -1,20 +1,31 @@
package com.readrops.app.viewmodels;
import android.app.Application;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import com.readrops.app.database.Database;
import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.entities.account.AccountType;
import com.readrops.app.repositories.ARepository;
import com.readrops.app.utils.matchers.OPMLMatcher;
import com.readrops.readropslibrary.opml.OPMLParser;
import java.util.List;
import java.util.Map;
import io.reactivex.Completable;
import io.reactivex.Single;
public class AccountViewModel extends AndroidViewModel {
private static final String TAG = AccountViewModel.class.getSimpleName();
private ARepository repository;
private Database database;
@ -28,15 +39,20 @@ public class AccountViewModel extends AndroidViewModel {
repository = ARepository.repositoryFactory(null, accountType, getApplication());
}
public void setAccount(Account account) {
try {
repository = ARepository.repositoryFactory(account, getApplication());
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}
public Single<Boolean> login(Account account, boolean insert) {
return repository.login(account, insert);
}
public Single<Long> insert(Account account) {
return Single.create(emitter -> {
long id = database.accountDao().insert(account);
emitter.onSuccess(id);
});
return database.accountDao().insert(account);
}
public Completable update(Account account) {
@ -50,4 +66,17 @@ public class AccountViewModel extends AndroidViewModel {
public Single<Integer> getAccountCount() {
return database.accountDao().getAccountCount();
}
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
return repository.getFoldersWithFeeds();
}
public Completable parseOPMLFile(Uri uri) {
return OPMLParser.read(uri, getApplication())
.flatMapCompletable(opml -> {
Map<Folder, List<Feed>> foldersAndFeeds = OPMLMatcher.INSTANCE.opmltoFoldersAndFeeds(opml);
return repository.insertOPMLFoldersAndFeeds(foldersAndFeeds);
});
}
}

View File

@ -1,14 +1,23 @@
package com.readrops.app.viewmodels;
import android.app.Application;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.annotation.NonNull;
import com.readrops.app.database.Database;
import com.readrops.app.database.dao.ItemDao;
import com.readrops.app.database.pojo.ItemWithFeed;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class ItemViewModel extends AndroidViewModel {
private ItemDao itemDao;
@ -23,4 +32,19 @@ public class ItemViewModel extends AndroidViewModel {
}
public Uri saveImageInCache(Bitmap bitmap) throws IOException {
File imagesFolder = new File(getApplication().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(getApplication(), getApplication().getPackageName(), image);
}
}

View File

@ -12,9 +12,10 @@ import androidx.paging.PagedList;
import com.readrops.app.activities.MainActivity;
import com.readrops.app.database.Database;
import com.readrops.app.database.ItemsListQueryBuilder;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.RoomFactoryWrapper;
import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.pojo.ItemWithFeed;
import com.readrops.app.repositories.ARepository;
@ -22,7 +23,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import io.reactivex.Completable;
import io.reactivex.Observable;
@ -68,7 +68,7 @@ public class MainViewModel extends AndroidViewModel {
if (lastFetch != null)
itemsWithFeed.removeSource(lastFetch);
lastFetch = new LivePagedListBuilder<>(db.itemDao().selectAll(queryBuilder.getQuery()),
lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(db.itemDao().selectAll(queryBuilder.getQuery())),
new PagedList.Config.Builder()
.setPageSize(100)
.setPrefetchDistance(150)
@ -124,32 +124,7 @@ public class MainViewModel extends AndroidViewModel {
}
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
return Single.create(emitter -> {
List<Folder> folders = db.folderDao().getFolders(currentAccount.getId());
Map<Folder, List<Feed>> foldersWithFeeds = new TreeMap<>(Folder::compareTo);
for (Folder folder : folders) {
List<Feed> feeds = db.feedDao().getFeedsByFolder(folder.getId());
for (Feed feed : feeds) {
int unreadCount = db.itemDao().getUnreadCount(feed.getId());
feed.setUnreadCount(unreadCount);
}
foldersWithFeeds.put(folder, feeds);
}
Folder noFolder = new Folder("no folder");
List<Feed> feedsWithoutFolder = db.feedDao().getFeedsWithoutFolder(currentAccount.getId());
for (Feed feed : feedsWithoutFolder) {
feed.setUnreadCount(db.itemDao().getUnreadCount(feed.getId()));
}
foldersWithFeeds.put(noFolder, feedsWithoutFolder);
emitter.onSuccess(foldersWithFeeds);
});
return repository.getFoldersWithFeeds();
}
//endregion

View File

@ -17,6 +17,7 @@ import com.readrops.app.repositories.ARepository;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Single;
public class ManageFeedsFoldersViewModel extends AndroidViewModel {
@ -69,7 +70,7 @@ public class ManageFeedsFoldersViewModel extends AndroidViewModel {
return db.folderDao().getFoldersWithFeedCount(account.getId());
}
public Completable addFolder(Folder folder) {
public Single<Long> addFolder(Folder folder) {
return repository.addFolder(folder);
}

View File

@ -0,0 +1,6 @@
<?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

@ -0,0 +1,5 @@
<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

@ -0,0 +1,5 @@
<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

@ -0,0 +1,20 @@
<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

@ -0,0 +1,5 @@
<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

@ -49,6 +49,30 @@
app:layout_constraintTop_toBottomOf="@+id/account_type_list_choose"
tools:itemCount="4"
tools:listitem="@layout/account_type_item" />
<TextView
android:id="@+id/account_type_or"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/or"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/account_type_recyclerview" />
<Button
android:id="@+id/account_type_opml_import"
style="@style/GenericButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/opml_import"
android:onClick="openOPMLFile"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/account_type_or" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -127,6 +127,7 @@
android:layout_height="wrap_content"
android:enabled="true"
android:text="@string/validate"
android:textColor="@color/generic_button_color_selector"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.6"
app:layout_constraintStart_toStartOf="parent"

View File

@ -156,7 +156,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/activity_item_details_layout"
android:layout_marginTop="15dp" />
android:layout_marginTop="15dp"
app:backgroundColor="?android:attr/colorBackground"
app:textColor="?android:attr/textColorPrimary" />
</RelativeLayout>

View File

@ -18,7 +18,8 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
android:theme="@style/ToolbarTheme"
app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight.ActionBar" />
<RelativeLayout
android:id="@+id/sync_progress_layout"
@ -88,10 +89,10 @@
android:id="@+id/empty_list_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image="@drawable/ic_rss_feed_grey"
app:text="@string/no_item"
android:layout_gravity="center"
android:visibility="gone" />
android:visibility="gone"
app:image="@drawable/ic_rss_feed_grey"
app:text="@string/no_item" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_feed_fab"

View File

@ -16,6 +16,7 @@
<androidx.appcompat.widget.Toolbar
android:id="@+id/manage_feeds_folders_toolbar"
style="@style/ToolbarTheme"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways" />
@ -26,7 +27,7 @@
android:layout_height="match_parent"
app:tabIndicator="@drawable/tab_indicator"
app:tabIndicatorColor="@android:color/white"
app:tabIndicatorHeight="4dp"/>
app:tabIndicatorHeight="4dp" />
</com.google.android.material.appbar.AppBarLayout>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.readrops.app.activities.WebViewActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/activity_web_view_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/activity_web_view_swipe"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/activity_web_view_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="4dp"
android:indeterminate="false" />
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@ -2,31 +2,26 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:gravity="center_horizontal|center_vertical"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal|center_vertical"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/empty_list_image"
android:layout_width="200dp"
android:layout_height="200dp"
tools:src="@drawable/ic_rss_feed_grey" />
<ImageView
android:id="@+id/empty_list_image"
android:layout_width="200dp"
android:layout_height="200dp"
tools:src="@drawable/ic_rss_feed_grey" />
<TextView
android:id="@+id/empty_list_text_v"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/no_feed" />
</LinearLayout>
</FrameLayout>
<TextView
android:id="@+id/empty_list_text_v"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/no_feed" />
</LinearLayout>
</layout>

View File

@ -10,7 +10,7 @@
<item
android:id="@+id/item_share"
android:title="@string/share"
android:title="@string/share_article"
android:icon="@drawable/ic_share_white"
app:showAsAction="ifRoom" />

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/web_view_share"
android:icon="@drawable/ic_share_white"
android:title="@string/share_article"
app:showAsAction="ifRoom" />
<item
android:id="@+id/web_view_refresh"
android:icon="@drawable/ic_refresh"
android:title="@string/actualize"
app:showAsAction="never" />
</menu>

View File

@ -20,7 +20,7 @@
<string name="read_time">%1$s min</string>
<string name="read_time_lower_than_1">Moins d\'une minute</string>
<string name="read_time_one_minute">1 min</string>
<string name="share">Partager l\'article</string>
<string name="share_article">Partager l\'article</string>
<string name="open_url">Ouvrir le lien</string>
<string name="add_folder">Ajouter un dossier</string>
<string name="feed_folder">Dossier du flux</string>
@ -84,5 +84,31 @@
<string name="no_item">Aucun item</string>
<string name="no_feed_found">Aucun flux trouvé</string>
<string name="feed_insertion_error">Erreur pour le flux %1$s</string>
<string name="get_feeds_colors">Récupération des couleurs des flux</string>
<string name="feeds_colors">Couleurs des flux</string>
<string name="global">Général</string>
<string name="reload_feeds_colors">Recharger les couleurs des flux</string>
<string name="open_items_in">Ouvrir les articles avec</string>
<string name="webview">Vue web</string>
<string name="external_navigator">Navigateur externe</string>
<string name="actualize">Actualiser</string>
<string name="share_url">Partager le lien</string>
<string name="opml_import_export">Import/Export OPML</string>
<string name="opml_processing">Traitement du fichier OPML</string>
<string name="operation_takes_time">Cette opération peut prendre un certain temps car il faut interroger chaque flux.</string>
<string name="processing_file_failed">Une erreur s\'est produite lors du traitement du fichier</string>
<string name="opml_import">Import OPML</string>
<string name="opml_export">Export OPML</string>
<string name="external_storage_opml_export">L\'export des soubscriptions nécessite l\'accès au stockage</string>
<string name="try_again">Réessayer</string>
<string name="permissions">Permissions</string>
<string name="or">Ou</string>
<string name="image_options">Options de l\'image</string>
<string name="download_image">Télécharger l\'image</string>
<string name="share_image">Partager l\'image</string>
<string name="theme">Thème</string>
<string name="light">Clair</string>
<string name="dark">Sombre</string>
<string name="opml_export_description">Export des flux et dossiers</string>
</resources>

View File

@ -0,0 +1,19 @@
<resources>
<style name="AppTheme" parent="MaterialDrawerTheme.ActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="actionModeBackground">@color/colorPrimary</item>
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
<item name="bottomSheetDialogTheme">@style/Theme.Design.BottomSheetDialog</item>
<item name="buttonStyle">@style/GenericButton</item>
</style>
<style name="GenericButton" parent="Widget.AppCompat.Button.Colored">
<item name="android:textColor">@android:color/white</item>
</style>
</resources>

View File

@ -5,6 +5,16 @@
<item>@string/filter_oldest</item>
</string-array>
<string-array name="opml_import_export">
<item>@string/opml_import</item>
<item>@string/opml_export</item>
</string-array>
<string-array name="image_options">
<item>@string/share_image</item>
<item>@string/download_image</item>
</string-array>
<string-array name="items_per_feed_numbers_values">
<item>20</item>
<item>50</item>
@ -19,4 +29,23 @@
<item>@string/unlimited</item>
</string-array>
<string-array name="open_items_in">
<item>@string/external_navigator</item>
<item>@string/webview</item>
</string-array>
<string-array name="open_item_in_values">
<item>0</item>
<item>1</item>
</string-array>
<string-array name="themes">
<item>@string/light</item>
<item>@string/dark</item>
</string-array>
<string-array name="themes_values">
<item>false</item>
<item>true</item>
</string-array>
</resources>

View File

@ -6,4 +6,9 @@
<attr name="text" format="string" />
</declare-styleable>
<declare-styleable name="ReadropsWebView">
<attr name="textColor" format="color" />
<attr name="backgroundColor" format="color" />
</declare-styleable>
</resources>

View File

@ -6,6 +6,4 @@
<color name="colorControlNormal">#d7d7d7</color>
<color name="colorBackground">#fafafa</color>
<color name="textColorPrimary">#000000</color>
<color name="selected_background">#E0E0E0</color>
</resources>

View File

@ -25,6 +25,8 @@
body {
margin: 0px;
color: %2$s;
background-color: %3$s;
}
h1, p, div {
@ -44,7 +46,7 @@
</head>
<body>
%2$s
%4$s
</body>
</html>]]></string>
</resources>

View File

@ -22,7 +22,7 @@
<string name="read_time_lower_than_1">Less than a minute</string>
<string name="read_time_one_minute">1 min</string>
<string name="interpoint" translatable="false">·</string>
<string name="share">Share Article</string>
<string name="share_article">Share Article</string>
<string name="open_url">Open url</string>
<string name="add_folder">Add folder</string>
<string name="feed_folder">Feed folder</string>
@ -92,4 +92,31 @@
<string name="no_item">No item</string>
<string name="no_feed_found">No feed found</string>
<string name="feed_insertion_error">Error for feed %1$s</string>
<string name="get_feeds_colors">Get feeds colors</string>
<string name="feeds_colors">Feeds Colors</string>
<string name="global">Global</string>
<string name="reload_feeds_colors">Reload feeds colors</string>
<string name="open_items_in">Open items in</string>
<string name="webview">Webview</string>
<string name="external_navigator">External navigator</string>
<string name="actualize">Actualize</string>
<string name="share_url">Share url</string>
<string name="opml_import_export">OPML Import/Export</string>
<string name="opml_processing">Processing OPML file</string>
<string name="operation_takes_time">This operation can take a significant time as each feed needs to be queried.</string>
<string name="processing_file_failed">An error occurred during the file processing</string>
<string name="opml_import">OPML import</string>
<string name="opml_export">OPML export</string>
<string name="subscriptions" translatable="false">Subscriptions</string>
<string name="external_storage_opml_export">Subscriptions export needs external storage permission</string>
<string name="try_again">Try again</string>
<string name="permissions">Permissions</string>
<string name="or">Or</string>
<string name="image_options">Image Options</string>
<string name="download_image">Download image</string>
<string name="share_image">Share image</string>
<string name="theme">Theme</string>
<string name="light">Light</string>
<string name="dark">Dark</string>
<string name="opml_export_description">Export feeds and folders</string>
</resources>

View File

@ -1,8 +1,6 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<style name="AppTheme" parent="MaterialDrawerTheme.Light.ActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
@ -10,7 +8,10 @@
<item name="colorControlNormal">@color/colorControlNormal</item>
<item name="android:textColorPrimary">@color/textColorPrimary</item>
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
<item name="actionModeBackground">@color/colorPrimary</item>
<item name="actionBarTheme">@style/ToolbarTheme</item>
<item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
</style>
<style name="AppTheme.NoActionBar" parent="AppTheme">
@ -23,15 +24,18 @@
<item name="android:windowBackground">@drawable/splash_background</item>
</style>
<style name="TextAppearance.Design.CollapsingToolbar.Expanded.Custom" parent="TextAppearance.Design.CollapsingToolbar.Expanded">
<item name="android:textSize">12sp</item>
<item name="android:color">@color/colorBackground</item>
<item name="android:layout_margin">14dp</item>
<style name="ToolbarTheme" parent="ThemeOverlay.AppCompat.ActionBar">
<item name="android:textColorPrimary">@android:color/white</item>
<item name="colorControlNormal">@android:color/white</item>
</style>
<style name="GenericButton" parent="Base.Widget.AppCompat.Button.Colored">
<style name="GenericButton" parent="Widget.AppCompat.Button.Colored">
<item name="android:colorBackground">@color/colorPrimary</item>
<item name="android:textColorPrimary">@color/colorControlNormal</item>
</style>
<style name="DrawerArrowStyle" parent="MaterialDrawer.DrawerArrowStyle">
<item name="color">@android:color/white</item>
</style>
</resources>

View File

@ -11,6 +11,11 @@
android:key="credentials_key"
android:title="@string/credentials" />
<PreferenceScreen
android:icon="@drawable/ic_import_export"
android:key="opml_import_export"
android:title="@string/opml_import_export" />
<PreferenceScreen
android:icon="@drawable/ic_delete_grey"
android:key="delete_account_key"

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<cache-path
name="shared_images"
path="images/" />
</paths>
</resources>

View File

@ -10,4 +10,24 @@
android:title="@string/number_items_to_parse" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/global">
<Preference
android:key="reload_feeds_colors"
android:title="@string/reload_feeds_colors" />
<ListPreference
android:defaultValue="0"
android:entries="@array/open_items_in"
android:entryValues="@array/open_item_in_values"
android:key="open_items_in"
android:title="@string/open_items_in" />
<ListPreference
android:defaultValue="false"
android:entries="@array/themes"
android:entryValues="@array/themes_values"
android:key="dark_theme"
android:title="@string/theme" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -0,0 +1,36 @@
package com.readrops.app;
import com.readrops.app.utils.HtmlParser;
import com.readrops.app.utils.ParsingResult;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static junit.framework.TestCase.assertEquals;
public class HtmlParserTest {
@Test
public void getFeedLinkTest() throws Exception {
String url = "https://github.com/readrops/Readrops";
ParsingResult parsingResult = new ParsingResult("https://github.com/readrops/Readrops/commits/develop.atom", "Recent Commits to Readrops:develop");
List<ParsingResult> parsingResultList = new ArrayList<>();
parsingResultList.add(parsingResult);
List<ParsingResult> parsingResultList1 = HtmlParser.getFeedLink(url);
Assert.assertEquals(parsingResultList, parsingResultList1);
}
@Test
public void getFaviconLinkTest() throws IOException {
String url = "https://github.com/readrops/Readrops";
assertEquals("https://github.com/fluidicon.png", HtmlParser.getFaviconLink(url));
}
}

View File

@ -0,0 +1,17 @@
package com.readrops.app;
import com.readrops.app.utils.Utils;
import org.junit.Test;
import static junit.framework.TestCase.assertEquals;
public class UtilsTest {
@Test
public void cleanTextTest() {
String text = " <p>This is a text<br/>to</p> clean ";
assertEquals("This is a text to clean", Utils.cleanText(text));
}
}

View File

@ -1,14 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.3.61'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:3.5.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View File

@ -0,0 +1,5 @@
- OPML import/export for local account
- Dark theme
- Share or download item image
- Open item in webview
- Minor bug fixes and improvements

View File

@ -3,19 +3,22 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
targetSdkVersion 29
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
@ -29,7 +32,10 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.core:core-ktx:1.1.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
@ -45,6 +51,5 @@ dependencies {
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
implementation "androidx.core:core-ktx:1.1.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

View File

@ -0,0 +1,40 @@
package com.readrops.readropslibrary.opml
import android.content.Context
import android.net.Uri
import com.readrops.readropslibrary.opml.model.OPML
import com.readrops.readropslibrary.utils.LibUtils
import io.reactivex.Completable
import io.reactivex.Single
import org.simpleframework.xml.Serializer
import org.simpleframework.xml.core.Persister
import java.io.OutputStream
class OPMLParser {
companion object {
@JvmStatic
fun read(uri: Uri, context: Context): Single<OPML> {
return Single.create { emitter ->
val fileString = LibUtils.fileToString(uri, context)
val serializer: Serializer = Persister()
val opml: OPML = serializer.read(OPML::class.java, fileString)
emitter.onSuccess(opml)
}
}
@JvmStatic
fun write(opml: OPML, outputStream: OutputStream): Completable {
return Completable.create { emitter ->
val serializer: Serializer = Persister()
serializer.write(opml, outputStream)
emitter.onComplete()
}
}
}
}

View File

@ -0,0 +1,13 @@
package com.readrops.readropslibrary.opml.model
import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Root
@Root(name = "body", strict = false)
data class Body(@field:ElementList(inline = true, required = true) var outlines: List<Outline>?) {
/**
* empty constructor required by SimpleXMl
*/
constructor() : this(null)
}

View File

@ -0,0 +1,13 @@
package com.readrops.readropslibrary.opml.model
import org.simpleframework.xml.Element
import org.simpleframework.xml.Root
@Root(name = "head", strict = false)
data class Head(@field:Element(required = false) var title: String?) {
/**
* empty constructor required by SimpleXML
*/
constructor() : this(null)
}

View File

@ -0,0 +1,19 @@
package com.readrops.readropslibrary.opml.model
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.Element
import org.simpleframework.xml.Order
import org.simpleframework.xml.Root
@Order(elements = ["head", "body"])
@Root(name = "opml", strict = false)
data class OPML(@field:Attribute(required = true) var version: String?,
@field:Element(required = true) var head: Head?,
@field:Element(required = true) var body: Body?) {
/**
* empty constructor required by SimpleXML
*/
constructor() : this(null, null, null)
}

View File

@ -0,0 +1,29 @@
package com.readrops.readropslibrary.opml.model
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Root
@Root(name = "outline", strict = false)
data class Outline(@field:Attribute(required = false) var title: String?,
@field:Attribute(required = false) var text: String?,
@field:Attribute(required = false) var type: String?,
@field:Attribute(required = false) var xmlUrl: String?,
@field:Attribute(required = false) var htmlUrl: String?,
@field:ElementList(inline = true, required = false) var outlines: List<Outline>?) {
/**
* empty constructor required by SimpleXML
*/
constructor() : this(
null,
null,
null,
null,
null,
null)
constructor(title: String) : this(title, null, null, null, null, null)
constructor(title: String, xmlUrl: String, htmlUrl: String) : this(title, title, "rss", xmlUrl, htmlUrl, null)
}

View File

@ -15,6 +15,8 @@ import retrofit2.converter.gson.GsonConverterFactory;
*/
public abstract class API<T> {
protected static final int MAX_ITEMS = 5000;
protected T api;
public API(Credentials credentials, @NonNull Class<T> clazz, @NonNull String endPoint) {

View File

@ -22,7 +22,9 @@ import okhttp3.RequestBody;
public class FreshRSSAPI extends API<FreshRSSService> {
private static final String GOOGLE_READ = "user/-/state/com.google/read";
public static final String GOOGLE_READ = "user/-/state/com.google/read";
private static final String FEED_PREFIX = "feed/";
public FreshRSSAPI(Credentials credentials) {
super(credentials, FreshRSSService.class, FreshRSSService.END_POINT);
@ -93,9 +95,9 @@ public class FreshRSSAPI extends API<FreshRSSService> {
switch (syncType) {
case INITIAL_SYNC:
return getItems(GOOGLE_READ, 10000, null);
return getItems(GOOGLE_READ, MAX_ITEMS, null);
case CLASSIC_SYNC:
return getItems(GOOGLE_READ, 10000, syncData.getLastModified());
return getItems(GOOGLE_READ, MAX_ITEMS, syncData.getLastModified());
}
return Single.error(new Exception("Unknown sync type"));
@ -134,7 +136,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
* @param lastModified fetch only items created after this timestamp
* @return the items
*/
public Single<FreshRSSItems> getItems(@NonNull String excludeTarget, @NonNull Integer max, @Nullable Long lastModified) {
public Single<FreshRSSItems> getItems(@Nullable String excludeTarget, int max, @Nullable Long lastModified) {
return api.getItems(excludeTarget, max, lastModified);
}
@ -147,7 +149,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
* @param token token for modifications
* @return Completable
*/
public Completable markItemsReadUnread(@NonNull Boolean read, @NonNull List<String> itemIds, @NonNull String token) {
public Completable markItemsReadUnread(boolean read, @NonNull List<String> itemIds, @NonNull String token) {
if (read)
return api.setItemsReadState(token, GOOGLE_READ, null, itemIds);
else
@ -162,7 +164,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
* @return Completable
*/
public Completable createFeed(@NonNull String token, @NonNull String feedUrl) {
return api.createOrDeleteFeed(token, "feed/" + feedUrl, "subscribe");
return api.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "subscribe");
}
/**
@ -173,7 +175,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
* @return Completable
*/
public Completable deleteFeed(@NonNull String token, @NonNull String feedUrl) {
return api.createOrDeleteFeed(token, "feed/" + feedUrl, "unsubscribe");
return api.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "unsubscribe");
}
/**
@ -186,7 +188,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
* @return Completable
*/
public Completable updateFeed(@NonNull String token, @NonNull String feedUrl, @NonNull String title, @NonNull String folderId) {
return api.updateFeed(token, "feed/" + feedUrl, title, folderId, "edit");
return api.updateFeed(token, FEED_PREFIX + feedUrl, title, folderId, "edit");
}
/**

View File

@ -35,7 +35,7 @@ public interface FreshRSSService {
Single<FreshRSSFeeds> getFeeds();
@GET("reader/api/0/stream/contents/user/-/state/com.google/reading-list")
Single<FreshRSSItems> getItems(@Query("xt") String excludeTarget, @Query("n") Integer max, @Query("ot") Long lastModified);
Single<FreshRSSItems> getItems(@Query("xt") String excludeTarget, @Query("n") int max, @Query("ot") Long lastModified);
@GET("reader/api/0/tag/list?output=json")
Single<FreshRSSFolders> getFolders();

View File

@ -1,6 +1,8 @@
package com.readrops.readropslibrary.services.freshrss.json;
import com.readrops.readropslibrary.services.freshrss.FreshRSSAPI;
import java.util.List;
public class FreshRSSItem {
@ -105,4 +107,8 @@ public class FreshRSSItem {
this.title = title;
}
public boolean isRead() {
return categories.contains(FreshRSSAPI.GOOGLE_READ);
}
}

View File

@ -78,7 +78,7 @@ public class NextNewsAPI extends API<NextNewsService> {
private void initialSync(NextNewsSyncResult syncResult) throws IOException {
getFeedsAndFolders(syncResult);
Response<NextNewsItems> itemsResponse = api.getItems(3, false, -1).execute();
Response<NextNewsItems> itemsResponse = api.getItems(3, false, MAX_ITEMS).execute();
NextNewsItems itemList = itemsResponse.body();
if (!itemsResponse.isSuccessful())

View File

@ -1,6 +1,5 @@
package com.readrops.readropslibrary.utils;
import com.facebook.stetho.okhttp3.StethoInterceptor;
import com.readrops.readropslibrary.BuildConfig;
import com.readrops.readropslibrary.services.Credentials;
@ -11,6 +10,7 @@ import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
public class HttpManager {
@ -29,14 +29,15 @@ public class HttpManager {
private void buildOkHttp() {
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder()
.callTimeout(30, TimeUnit.SECONDS)
.callTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.HOURS);
httpBuilder.addInterceptor(new AuthInterceptor());
if (BuildConfig.DEBUG) {
StethoInterceptor loggingInterceptor = new StethoInterceptor();
httpBuilder.addNetworkInterceptor(loggingInterceptor);
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.level(HttpLoggingInterceptor.Level.BASIC);
httpBuilder.addInterceptor(interceptor);
}
okHttpClient = httpBuilder.build();

View File

@ -1,5 +1,9 @@
package com.readrops.readropslibrary.utils;
import android.content.Context;
import android.net.Uri;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Scanner;
@ -28,6 +32,10 @@ public final class LibUtils {
return scanner.hasNext() ? scanner.next() : "";
}
public static String fileToString(Uri uri, Context context) throws FileNotFoundException {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
return inputStreamToString(inputStream);
}
}