Merge branch 'develop'

This commit is contained in:
Shinokuni 2020-07-31 17:16:47 +02:00
commit 70d0515ffb
265 changed files with 5615 additions and 4031 deletions

31
.github/workflows/android.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: Android CI
on:
push:
branches:
- master
- develop
pull_request:
branches:
- master
- develop
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Gradle
run: ./gradlew clean build
- name: Android Emulator Runner
uses: ReactiveCircus/android-emulator-runner@v2.5.0
with:
api-level: 29
script: ./gradlew connectedCheck

6
.gitignore vendored
View File

@ -125,3 +125,9 @@ Temporary Items
\.idea/
fastlane/Appfile
fastlane/Fastfile
Gemfile

View File

@ -1,16 +0,0 @@
language: android
android:
components:
- android-28
- build-tools-28.0.3
licenses:
- 'android-sdk-preview-license-52d11cd2'
- 'android-sdk-license-.+'
- 'google-gdk-license-.+'
script:
- ./gradlew clean assembleDebug assembleRelease testDebug
before_install:
- yes | sdkmanager "platforms;android-29"
- yes | sdkmanager "build-tools;29.0.2"

View File

@ -1,16 +1,10 @@
# Readrops ![alt text](images/readrops_logo.png "Readrops Logo")
# Readrops <img src="fastlane/metadata/android/en-US/images/icon.png" width=60>
[![Build Status](https://travis-ci.org/readrops/Readrops.svg?branch=develop)](https://travis-ci.org/readrops/Readrops)
![Build Status](https://github.com/readrops/Readrops/workflows/Android%20CI/badge.svg?branch=develop)
Readrops is a multi-services RSS client for Android. Its name is composed of "Read" and "drops", where drops are information drops in an ocean of news.
[<img src="images/google-play-badge.png" width=250>](https://play.google.com/store/apps/details?id=com.readrops.app)
# Screenshots
![](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_1.png)![](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_2.png)![](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_3.png)![](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_4.png)
<br/>
![](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_5.png)![](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_6.png)![](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_7.png)![](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_8.png)
[<img src="images/google-play-badge.png" width=250>](https://play.google.com/store/apps/details?id=com.readrops.app)[<img src="images/fdroid-badge.png" width=250>](https://f-droid.org/en/packages/com.readrops.app/)
# Features
@ -23,10 +17,21 @@ Readrops is a multi-services RSS client for Android. Its name is composed of "Re
Other features will come in the near future.
# Screenshots
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_1.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_2.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_3.png" width=250>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_4.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_5.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_6.png" width=250>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_7.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_8.png" width=250>
# Licence
This project is released under the GPLv3 licence.
# Donations
Bitcoin address : bc1qlkzlcsvvtn3y6mek5umv5tc4ln09l64x6y42hr<br/>
Litecoin address : MTuf45ZvxhMWWo4v8YBbFDTLsFcGtpcPNT
[<img src="images/paypal-badge.png" width=250>](https://paypal.me/readropsapp)
Bitcoin address : `bc1qlkzlcsvvtn3y6mek5umv5tc4ln09l64x6y42hr` <br />
Litecoin address : `MTuf45ZvxhMWWo4v8YBbFDTLsFcGtpcPNT`

69
api/build.gradle Normal file
View File

@ -0,0 +1,69 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
abortOnError false
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':db')
// xpp3 has a conflict with kxml when running connectedCheck task
configurations {
all*.exclude group: 'xpp3', module: 'xpp3'
}
implementation "androidx.core:core-ktx:1.2.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
implementation('com.squareup.retrofit2:converter-moshi:2.7.1') {
exclude group: 'moshi', module: 'moshi' // moshi converter uses moshi 1.8.0 which breaks codegen 1.9.2
}
implementation 'com.squareup.retrofit2:converter-simplexml:2.7.1'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1'
implementation 'com.squareup.moshi:moshi:1.9.2'
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.9.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0'
api 'io.reactivex.rxjava2:rxandroid:2.1.1'
api 'org.jsoup:jsoup:1.12.1'
}

View File

@ -1,4 +1,4 @@
package com.readrops.app;
package com.readrops.api;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
@ -21,6 +21,6 @@ public class ExampleInstrumentedTest {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.readrops.app", appContext.getPackageName());
assertEquals("com.readrops.api.test", appContext.getPackageName());
}
}

View File

@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.readrops.api">
</manifest>

View File

@ -0,0 +1,9 @@
package com.readrops.api.localfeed
/*
A simple class to give an abstract level to rss/atom/json feed classes
*/
abstract class AFeed {
var etag: String? = null
var lastModified: String? = null
}

View File

@ -1,16 +1,17 @@
package com.readrops.readropslibrary.localfeed;
package com.readrops.api.localfeed;
import android.accounts.NetworkErrorException;
import android.util.Log;
import com.google.gson.Gson;
import com.readrops.readropslibrary.utils.HttpBuilder;
import com.readrops.readropslibrary.utils.LibUtils;
import com.readrops.readropslibrary.utils.UnknownFormatException;
import com.readrops.readropslibrary.localfeed.atom.ATOMFeed;
import com.readrops.readropslibrary.localfeed.json.JSONFeed;
import com.readrops.readropslibrary.localfeed.rss.RSSFeed;
import com.readrops.readropslibrary.localfeed.rss.RSSLink;
import com.readrops.api.localfeed.atom.ATOMFeed;
import com.readrops.api.localfeed.json.JSONFeed;
import com.readrops.api.localfeed.rss.RSSFeed;
import com.readrops.api.localfeed.rss.RSSLink;
import com.readrops.api.utils.HttpManager;
import com.readrops.api.utils.LibUtils;
import com.readrops.api.utils.UnknownFormatException;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
@ -34,6 +35,8 @@ public class RSSQuery {
private static final String RSS_2_REGEX = "rss.*version=\"2.0\"";
private static final String ATOM_REGEX = "<feed.* xmlns=\"http://www.w3.org/2005/Atom\"";
/**
* Request the url given in parameter.
* This method is synchronous, it <b>has</b> to be called from another thread than the main one.
@ -74,7 +77,8 @@ public class RSSQuery {
}
private Response query(String url, Map<String, String> headers) throws IOException {
OkHttpClient okHttpClient = HttpBuilder.getBuilder().build();
OkHttpClient okHttpClient = HttpManager.getInstance().getOkHttpClient();
HttpManager.getInstance().setCredentials(null);
Request.Builder builder = new Request.Builder().url(url);
for (String header : headers.keySet()) {
@ -153,8 +157,11 @@ public class RSSQuery {
((ATOMFeed) feed).setUrl(response.request().url().toString());
break;
case RSS_JSON:
Gson gson = new Gson();
feed = gson.fromJson(xml, JSONFeed.class);
Moshi moshi = new Moshi.Builder()
.build();
JsonAdapter<JSONFeed> jsonFeedAdapter = moshi.adapter(JSONFeed.class);
feed = jsonFeedAdapter.fromJson(xml);
break;
}
@ -172,7 +179,7 @@ public class RSSQuery {
if (Pattern.compile(RSS_2_REGEX).matcher(content).find())
type = RSSType.RSS_2;
else if (content.contains("<feed xmlns=\"http://www.w3.org/2005/Atom\""))
else if (Pattern.compile(ATOM_REGEX).matcher(content).find())
type = RSSType.RSS_ATOM;
else
type = RSSType.RSS_UNKNOWN;

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.localfeed;
package com.readrops.api.localfeed;
public class RSSQueryResult {

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.localfeed.atom;
package com.readrops.api.localfeed.atom;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.localfeed.atom;
package com.readrops.api.localfeed.atom;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Element;

View File

@ -1,6 +1,6 @@
package com.readrops.readropslibrary.localfeed.atom;
package com.readrops.api.localfeed.atom;
import com.readrops.readropslibrary.localfeed.AFeed;
import com.readrops.api.localfeed.AFeed;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.localfeed.atom;
package com.readrops.api.localfeed.atom;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Root;

View File

@ -0,0 +1,9 @@
package com.readrops.api.localfeed.json
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class JSONAuthor(val name: String,
val url: String,
@Json(name = "avatar") val avatarUrl: String?)

View File

@ -0,0 +1,15 @@
package com.readrops.api.localfeed.json
import com.readrops.api.localfeed.AFeed
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class JSONFeed(val version: String,
val title: String,
@Json(name = "home_page_url") val homePageUrl: String?,
@Json(name = "feed_url") val feedUrl: String?,
val description: String?,
@Json(name = "icon") val iconUrl: String?,
@Json(name = "favicon") val faviconUrl: String?,
val items: List<JSONItem>) : AFeed()

View File

@ -0,0 +1,21 @@
package com.readrops.api.localfeed.json
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class JSONItem(val id: String,
val title: String?,
val summary: String?,
@Json(name = "content_text") val contentText: String?,
@Json(name = "content_html") val contentHtml: String?,
val url: String?,
@Json(name = "image") val imageUrl: String?,
@Json(name = "date_published") val pubDate: String,
@Json(name = "date_modified") val modDate: String?,
val author: JSONAuthor?) {
fun getContent(): String? {
return contentHtml ?: contentText
}
}

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.localfeed.rss;
package com.readrops.api.localfeed.rss;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;
@ -22,9 +22,6 @@ public class RSSChannel {
@Element(name = "lastBuildDate", required = false)
private String lastUpdated;
@Element(required = false)
private RSSImage image;
@ElementList(inline = true, required = false)
private List<RSSItem> items;
@ -68,14 +65,6 @@ public class RSSChannel {
this.lastUpdated = lastUpdated;
}
public RSSImage getImage() {
return image;
}
public void setImage(RSSImage image) {
this.image = image;
}
public String getFeedUrl() {
if (links.size() > 1) {
if (links.get(0).getHref() != null)

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.localfeed.rss;
package com.readrops.api.localfeed.rss;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Root;
@ -6,10 +6,10 @@ import org.simpleframework.xml.Root;
@Root(name = "enclosure", strict = false)
public class RSSEnclosure {
@Attribute
@Attribute(required = false)
private String type;
@Attribute
@Attribute(required = false)
private String url;
public String getType() {

View File

@ -1,6 +1,6 @@
package com.readrops.readropslibrary.localfeed.rss;
package com.readrops.api.localfeed.rss;
import com.readrops.readropslibrary.localfeed.AFeed;
import com.readrops.api.localfeed.AFeed;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.localfeed.rss;
package com.readrops.api.localfeed.rss;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;
@ -16,10 +16,6 @@ public class RSSItem {
@Element(name = "link", required = false)
private String link;
@ElementList(name = "link", inline = true, required = false)
@Namespace(prefix = "atom")
private List<String> otherLinks;
@Element(name = "imageLink", required = false)
private String imageLink;
@ -30,9 +26,9 @@ public class RSSItem {
@ElementList(name = "enclosure", inline = true, required = false)
private List<RSSEnclosure> enclosures;
@Element(name = "creator", required = false)
@ElementList(name = "creator", inline = true, required = false)
@Namespace(prefix = "dc", reference = "http://purl.org/dc/elements/1.1/")
private String creator;
private List<String> creator;
@Element(required = false)
private String author;
@ -51,7 +47,7 @@ public class RSSItem {
@Namespace(prefix = "content")
private String content;
@Element
@Element(required = false)
private String guid;
public String getTitle() {
@ -86,11 +82,11 @@ public class RSSItem {
this.imageLink = imageLink;
}
public String getCreator() {
public List<String> getCreator() {
return creator;
}
public void setCreator(String creator) {
public void setCreator(List<String> creator) {
this.creator = creator;
}
@ -146,8 +142,8 @@ public class RSSItem {
}
public String getAuthor() {
if (creator != null)
return creator;
if (creator != null && !creator.isEmpty())
return creator.get(0);
else
return author;
}
@ -155,12 +151,4 @@ public class RSSItem {
public void setAuthor(String author) {
this.author = author;
}
public List<String> getOtherLinks() {
return otherLinks;
}
public void setOtherLinks(List<String> otherLinks) {
this.otherLinks = otherLinks;
}
}

View File

@ -1,7 +1,6 @@
package com.readrops.readropslibrary.localfeed.rss;
package com.readrops.api.localfeed.rss;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.Text;

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.localfeed.rss;
package com.readrops.api.localfeed.rss;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Root;

View File

@ -1,9 +1,9 @@
package com.readrops.readropslibrary.opml
package com.readrops.api.opml
import android.content.Context
import android.net.Uri
import com.readrops.readropslibrary.opml.model.OPML
import com.readrops.readropslibrary.utils.LibUtils
import com.readrops.api.opml.model.OPML
import com.readrops.api.utils.LibUtils
import io.reactivex.Completable
import io.reactivex.Single
import org.simpleframework.xml.Serializer

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.opml.model
package com.readrops.api.opml.model
import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Root

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.opml.model
package com.readrops.api.opml.model
import org.simpleframework.xml.Element
import org.simpleframework.xml.Root

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.opml.model
package com.readrops.api.opml.model
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.Element

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.opml.model
package com.readrops.api.opml.model
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.ElementList

View File

@ -1,12 +1,13 @@
package com.readrops.readropslibrary.services;
package com.readrops.api.services;
import androidx.annotation.NonNull;
import com.readrops.readropslibrary.utils.HttpManager;
import com.readrops.api.utils.HttpManager;
import com.squareup.moshi.Moshi;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.moshi.MoshiConverterFactory;
/**
* Abstraction level for services APIs
@ -30,10 +31,12 @@ public abstract class API<T> {
api = createAPI(credentials);
}
protected abstract Moshi buildMoshi();
protected Retrofit getConfiguredRetrofitInstance() {
return new Retrofit.Builder()
.baseUrl(HttpManager.getInstance().getCredentials().getUrl() + endPoint)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(buildMoshi()))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(HttpManager.getInstance().getOkHttpClient())
.build();

View File

@ -0,0 +1,39 @@
package com.readrops.api.services;
import androidx.annotation.Nullable;
import com.readrops.db.entities.account.Account;
import com.readrops.api.services.freshrss.FreshRSSCredentials;
import com.readrops.api.services.nextcloudnews.NextNewsCredentials;
public abstract class Credentials {
private String authorization;
private String url;
public Credentials(String authorization, String url) {
this.authorization = authorization;
this.url = url;
}
public String getAuthorization() {
return authorization;
}
public String getUrl() {
return url;
}
@Nullable
public static Credentials toCredentials(Account account) {
switch (account.getAccountType()) {
case NEXTCLOUD_NEWS:
return new NextNewsCredentials(account.getLogin(), account.getPassword(), account.getUrl());
case FRESHRSS:
return new FreshRSSCredentials(account.getToken(), account.getUrl());
default:
return null;
}
}
}

View File

@ -0,0 +1,16 @@
package com.readrops.api.services
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.entities.Item
class SyncResult {
var items: List<Item> = mutableListOf()
var feeds: List<Feed> = listOf()
var folders: List<Folder> = listOf()
var isError: Boolean = false
}

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.services;
package com.readrops.api.services;
public enum SyncType {
INITIAL_SYNC,

View File

@ -1,15 +1,21 @@
package com.readrops.readropslibrary.services.freshrss;
package com.readrops.api.services.freshrss;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.readropslibrary.services.API;
import com.readrops.readropslibrary.services.Credentials;
import com.readrops.readropslibrary.services.SyncType;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSFeeds;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSFolders;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSItems;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSUserInfo;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.api.services.API;
import com.readrops.api.services.Credentials;
import com.readrops.api.services.SyncResult;
import com.readrops.api.services.SyncType;
import com.readrops.api.services.freshrss.adapters.FreshRSSFeedsAdapter;
import com.readrops.api.services.freshrss.adapters.FreshRSSFoldersAdapter;
import com.readrops.api.services.freshrss.adapters.FreshRSSItemsAdapter;
import com.readrops.api.services.freshrss.json.FreshRSSUserInfo;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import java.io.StringReader;
import java.util.List;
@ -30,6 +36,15 @@ public class FreshRSSAPI extends API<FreshRSSService> {
super(credentials, FreshRSSService.class, FreshRSSService.END_POINT);
}
@Override
protected Moshi buildMoshi() {
return new Moshi.Builder()
.add(Types.newParameterizedType(List.class, Item.class), new FreshRSSItemsAdapter())
.add(new FreshRSSFeedsAdapter())
.add(new FreshRSSFoldersAdapter())
.build();
}
/**
* Call token API to generate a new token from account credentials
*
@ -80,31 +95,27 @@ public class FreshRSSAPI extends API<FreshRSSService> {
* @param writeToken token for making modifications on the server
* @return the result of the synchronization
*/
public Single<FreshRSSSyncResult> sync(@NonNull SyncType syncType, @NonNull FreshRSSSyncData syncData, @NonNull String writeToken) {
FreshRSSSyncResult syncResult = new FreshRSSSyncResult();
public Single<SyncResult> sync(@NonNull SyncType syncType, @NonNull FreshRSSSyncData syncData, @NonNull String writeToken) {
SyncResult syncResult = new SyncResult();
return setItemsReadState(syncData, writeToken)
.andThen(getFolders()
.flatMap(freshRSSFolders -> {
syncResult.setFolders(freshRSSFolders.getTags());
syncResult.setFolders(freshRSSFolders);
return getFeeds();
})
.flatMap(freshRSSFeeds -> {
syncResult.setFeeds(freshRSSFeeds.getSubscriptions());
syncResult.setFeeds(freshRSSFeeds);
switch (syncType) {
case INITIAL_SYNC:
return getItems(GOOGLE_READ, MAX_ITEMS, null);
case CLASSIC_SYNC:
return getItems(GOOGLE_READ, MAX_ITEMS, syncData.getLastModified());
if (syncType == SyncType.INITIAL_SYNC) {
return getItems(GOOGLE_READ, MAX_ITEMS, null);
} else {
return getItems(null, MAX_ITEMS, syncData.getLastModified());
}
return Single.error(new Exception("Unknown sync type"));
})
.flatMap(freshRSSItems -> {
syncResult.setItems(freshRSSItems.getItems());
syncResult.setLastUpdated(freshRSSItems.getUpdated());
syncResult.setItems(freshRSSItems);
return Single.just(syncResult);
}));
@ -115,7 +126,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
*
* @return the feeds folders
*/
public Single<FreshRSSFolders> getFolders() {
public Single<List<Folder>> getFolders() {
return api.getFolders();
}
@ -124,7 +135,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
*
* @return the feeds
*/
public Single<FreshRSSFeeds> getFeeds() {
public Single<List<Feed>> getFeeds() {
return api.getFeeds();
}
@ -136,7 +147,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
* @param lastModified fetch only items created after this timestamp
* @return the items
*/
public Single<FreshRSSItems> getItems(@Nullable String excludeTarget, int max, @Nullable Long lastModified) {
public Single<List<Item>> getItems(@Nullable String excludeTarget, int max, @Nullable Long lastModified) {
return api.getItems(excludeTarget, max, lastModified);
}

View File

@ -1,6 +1,6 @@
package com.readrops.readropslibrary.services.freshrss;
package com.readrops.api.services.freshrss;
import com.readrops.readropslibrary.services.Credentials;
import com.readrops.api.services.Credentials;
public class FreshRSSCredentials extends Credentials {

View File

@ -1,9 +1,9 @@
package com.readrops.readropslibrary.services.freshrss;
package com.readrops.api.services.freshrss;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSFeeds;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSFolders;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSItems;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSUserInfo;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.api.services.freshrss.json.FreshRSSUserInfo;
import java.util.List;
@ -32,13 +32,13 @@ public interface FreshRSSService {
Single<FreshRSSUserInfo> getUserInfo();
@GET("reader/api/0/subscription/list?output=json")
Single<FreshRSSFeeds> getFeeds();
Single<List<Feed>> getFeeds();
@GET("reader/api/0/stream/contents/user/-/state/com.google/reading-list")
Single<FreshRSSItems> getItems(@Query("xt") String excludeTarget, @Query("n") int max, @Query("ot") Long lastModified);
Single<List<Item>> getItems(@Query("xt") String excludeTarget, @Query("n") int max, @Query("ot") Long lastModified);
@GET("reader/api/0/tag/list?output=json")
Single<FreshRSSFolders> getFolders();
Single<List<Folder>> getFolders();
@FormUrlEncoded
@POST("reader/api/0/edit-tag")

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.services.freshrss;
package com.readrops.api.services.freshrss;
import java.util.ArrayList;
import java.util.List;

View File

@ -0,0 +1,77 @@
package com.readrops.api.services.freshrss.adapters
import android.annotation.SuppressLint
import com.readrops.db.entities.Feed
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.ToJson
class FreshRSSFeedsAdapter {
@ToJson
fun toJson(feed: Feed): String = ""
@SuppressLint("CheckResult")
@FromJson
fun fromJson(reader: JsonReader): List<Feed> {
val feeds = mutableListOf<Feed>()
reader.beginObject()
reader.nextName() // "subscriptions", beginning of the feed array
reader.beginArray()
while (reader.hasNext()) {
reader.beginObject()
val feed = Feed()
while (reader.hasNext()) {
with(feed) {
when (reader.selectName(NAMES)) {
0 -> name = reader.nextString()
1 -> url = reader.nextString()
2 -> siteUrl = reader.nextString()
3 -> iconUrl = reader.nextString()
4 -> remoteId = reader.nextString()
5 -> remoteFolderId = getCategoryId(reader)
else -> reader.skipValue()
}
}
}
feeds += feed
reader.endObject()
}
reader.endArray()
reader.endObject()
return feeds
}
private fun getCategoryId(reader: JsonReader): String? {
var id: String? = null
reader.beginArray()
while (reader.hasNext()) {
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
"id" -> id = reader.nextString()
else -> reader.skipValue()
}
}
reader.endObject()
if (!id.isNullOrEmpty())
break
}
reader.endArray()
return id
}
companion object {
val NAMES: JsonReader.Options = JsonReader.Options.of("title", "url", "htmlUrl", "iconUrl", "id", "categories")
}
}

View File

@ -0,0 +1,61 @@
package com.readrops.api.services.freshrss.adapters
import android.annotation.SuppressLint
import com.readrops.db.entities.Folder
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.ToJson
import java.util.*
class FreshRSSFoldersAdapter {
@ToJson
fun toJson(folder: Folder): String = ""
@SuppressLint("CheckResult")
@FromJson
fun fromJson(reader: JsonReader): List<Folder> {
val folders = mutableListOf<Folder>()
reader.beginObject()
reader.nextName() // "tags", beginning of folder array
reader.beginArray()
while (reader.hasNext()) {
reader.beginObject()
val folder = Folder()
var type: String? = null
while (reader.hasNext()) {
with(folder) {
when (reader.selectName(NAMES)) {
0 -> {
val id = reader.nextString()
name = StringTokenizer(id, "/")
.toList()
.last() as String
remoteId = id
}
1 -> type = reader.nextString()
else -> reader.skipValue()
}
}
}
if (type == "folder") // add only folders and avoid tags
folders += folder
reader.endObject()
}
reader.endArray()
reader.endObject()
return folders
}
companion object {
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "type")
}
}

View File

@ -0,0 +1,134 @@
package com.readrops.api.services.freshrss.adapters
import android.util.TimingLogger
import com.readrops.db.entities.Item
import com.readrops.api.services.freshrss.FreshRSSAPI.GOOGLE_READ
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import org.joda.time.DateTimeZone
import org.joda.time.LocalDateTime
class FreshRSSItemsAdapter : JsonAdapter<List<Item>>() {
override fun toJson(writer: JsonWriter, value: List<Item>?) {
// no need of this
}
override fun fromJson(reader: JsonReader): List<Item>? {
val logger = TimingLogger(TAG, "item parsing")
val items = mutableListOf<Item>()
reader.beginObject()
while (reader.hasNext()) {
if (reader.nextName() == "items") parseItems(reader, items) else reader.skipValue()
}
reader.endObject()
logger.addSplit("item parsing done")
logger.dumpToLog()
return items
}
private fun parseItems(reader: JsonReader, items: MutableList<Item>) {
reader.beginArray()
while (reader.hasNext()) {
val item = Item()
reader.beginObject()
while (reader.hasNext()) {
with(item) {
when (reader.selectName(NAMES)) {
0 -> remoteId = reader.nextString()
1 -> pubDate = LocalDateTime(reader.nextLong() * 1000L,
DateTimeZone.getDefault())
2 -> title = reader.nextString()
3 -> content = getContent(reader)
4 -> link = getLink(reader)
5 -> isRead = getReadState(reader)
6 -> feedRemoteId = getRemoteFeedId(reader)
7 -> author = reader.nextString()
else -> reader.skipValue()
}
}
}
items += item
reader.endObject()
}
reader.endArray()
}
private fun getContent(reader: JsonReader): String? {
var content: String? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
"content" -> content = reader.nextString()
else -> reader.skipValue()
}
}
reader.endObject()
return content
}
private fun getLink(reader: JsonReader): String? {
var href: String? = null
reader.beginArray()
while (reader.hasNext()) {
reader.beginObject()
when (reader.nextName()) {
"href" -> href = reader.nextString()
else -> reader.skipValue()
}
reader.endObject()
}
reader.endArray()
return href
}
private fun getReadState(reader: JsonReader): Boolean {
var isRead = false
reader.beginArray()
while (reader.hasNext()) {
when (reader.nextString()) {
GOOGLE_READ -> isRead = true
}
}
reader.endArray()
return isRead
}
private fun getRemoteFeedId(reader: JsonReader): String? {
var remoteFeedId: String? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
"streamId" -> remoteFeedId = reader.nextString()
else -> reader.skipValue()
}
}
reader.endObject()
return remoteFeedId
}
companion object {
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "published", "title", "summary", "alternate", "categories", "origin", "author")
val TAG = FreshRSSItemsAdapter::class.java.simpleName
}
}

View File

@ -0,0 +1,9 @@
package com.readrops.api.services.freshrss.json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class FreshRSSUserInfo(val userEmail: String,
val userId: String,
val userName: String,
val userProfileId: String)

View File

@ -0,0 +1,257 @@
package com.readrops.api.services.nextcloudnews;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.api.services.API;
import com.readrops.api.services.Credentials;
import com.readrops.api.services.SyncResult;
import com.readrops.api.services.SyncType;
import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter;
import com.readrops.api.services.nextcloudnews.adapters.NextNewsFoldersAdapter;
import com.readrops.api.services.nextcloudnews.adapters.NextNewsItemsAdapter;
import com.readrops.api.services.nextcloudnews.json.NextNewsUser;
import com.readrops.api.utils.ConflictException;
import com.readrops.api.utils.LibUtils;
import com.readrops.api.utils.UnknownFormatException;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import retrofit2.Response;
public class NextNewsAPI extends API<NextNewsService> {
private static final String TAG = NextNewsAPI.class.getSimpleName();
public NextNewsAPI(Credentials credentials) {
super(credentials, NextNewsService.class, NextNewsService.END_POINT);
}
@Override
protected Moshi buildMoshi() {
return new Moshi.Builder()
.add(new NextNewsFeedsAdapter())
.add(new NextNewsFoldersAdapter())
.add(Types.newParameterizedType(List.class, Item.class), new NextNewsItemsAdapter())
.build();
}
@Nullable
public NextNewsUser login() throws IOException {
Response<NextNewsUser> response = api.getUser().execute();
if (!response.isSuccessful())
return null;
return response.body();
}
@Nullable
public List<Feed> createFeed(String url, int folderId) throws IOException, UnknownFormatException {
Response<List<Feed>> response = api.createFeed(url, folderId).execute();
if (!response.isSuccessful()) {
if (response.code() == LibUtils.HTTP_UNPROCESSABLE)
throw new UnknownFormatException();
else
return null;
}
return response.body();
}
public SyncResult sync(@NonNull SyncType syncType, @Nullable NextNewsSyncData data) throws IOException {
SyncResult syncResult = new SyncResult();
switch (syncType) {
case INITIAL_SYNC:
initialSync(syncResult);
break;
case CLASSIC_SYNC:
if (data == null)
throw new NullPointerException("NextNewsSyncData can't be null");
classicSync(syncResult, data);
break;
}
return syncResult;
}
private void initialSync(SyncResult syncResult) throws IOException {
getFeedsAndFolders(syncResult);
Response<List<Item>> itemsResponse = api.getItems(3, false, MAX_ITEMS).execute();
List<Item> itemList = itemsResponse.body();
if (!itemsResponse.isSuccessful())
syncResult.setError(true);
if (itemList != null)
syncResult.setItems(itemList);
}
private void classicSync(SyncResult syncResult, NextNewsSyncData data) throws IOException {
putModifiedItems(data, syncResult);
getFeedsAndFolders(syncResult);
Response<List<Item>> itemsResponse = api.getNewItems(data.getLastModified(), 3).execute();
List<Item> itemList = itemsResponse.body();
if (!itemsResponse.isSuccessful())
syncResult.setError(true);
if (itemList != null)
syncResult.setItems(itemList);
}
private void getFeedsAndFolders(SyncResult syncResult) throws IOException {
Response<List<Feed>> feedResponse = api.getFeeds().execute();
List<Feed> feedList = feedResponse.body();
if (!feedResponse.isSuccessful())
syncResult.setError(true);
Response<List<Folder>> folderResponse = api.getFolders().execute();
List<Folder> folderList = folderResponse.body();
if (!folderResponse.isSuccessful())
syncResult.setError(true);
if (folderList != null)
syncResult.setFolders(folderList);
if (feedList != null)
syncResult.setFeeds(feedList);
}
private void putModifiedItems(NextNewsSyncData data, SyncResult syncResult) throws IOException {
if (!data.getReadItems().isEmpty()) {
Map<String, List<String>> itemIdsMap = new HashMap<>();
itemIdsMap.put("items", data.getReadItems());
Response readItemsResponse = api.setArticlesState(StateType.READ.name().toLowerCase(),
itemIdsMap).execute();
if (!readItemsResponse.isSuccessful())
syncResult.setError(true);
}
if (!data.getUnreadItems().isEmpty()) {
Map<String, List<String>> itemIdsMap = new HashMap<>();
itemIdsMap.put("items", data.getUnreadItems());
Response unreadItemsResponse = api.setArticlesState(StateType.UNREAD.toString().toLowerCase(),
itemIdsMap).execute();
if (!unreadItemsResponse.isSuccessful())
syncResult.setError(true);
}
}
public List<Folder> createFolder(Folder folder) throws IOException, UnknownFormatException, ConflictException {
Map<String, String> folderNameMap = new HashMap<>();
folderNameMap.put("name", folder.getName());
Response<List<Folder>> foldersResponse = api.createFolder(folderNameMap).execute();
if (foldersResponse.isSuccessful())
return foldersResponse.body();
else if (foldersResponse.code() == LibUtils.HTTP_UNPROCESSABLE)
throw new UnknownFormatException();
else if (foldersResponse.code() == LibUtils.HTTP_CONFLICT)
throw new ConflictException();
else
return new ArrayList<>();
}
public boolean deleteFolder(Folder folder) throws IOException {
Response response = api.deleteFolder(Integer.parseInt(folder.getRemoteId())).execute();
if (response.isSuccessful())
return true;
else if (response.code() == LibUtils.HTTP_NOT_FOUND)
throw new Resources.NotFoundException();
else
return false;
}
public boolean renameFolder(Folder folder) throws IOException, UnknownFormatException, ConflictException {
Map<String, String> folderNameMap = new HashMap<>();
folderNameMap.put("name", folder.getName());
Response response = api.renameFolder(Integer.parseInt(folder.getRemoteId()), folderNameMap).execute();
if (response.isSuccessful())
return true;
else {
switch (response.code()) {
case LibUtils.HTTP_NOT_FOUND:
throw new Resources.NotFoundException();
case LibUtils.HTTP_UNPROCESSABLE:
throw new UnknownFormatException();
case LibUtils.HTTP_CONFLICT:
throw new ConflictException();
default:
return false;
}
}
}
public boolean deleteFeed(int feedId) throws IOException {
Response response = api.deleteFeed(feedId).execute();
if (response.isSuccessful())
return true;
else if (response.code() == LibUtils.HTTP_NOT_FOUND)
throw new Resources.NotFoundException();
else
return false;
}
public boolean changeFeedFolder(Feed feed) throws IOException {
Map<String, Integer> folderIdMap = new HashMap<>();
folderIdMap.put("folderId", Integer.parseInt(feed.getRemoteFolderId()));
Response response = api.changeFeedFolder(Integer.parseInt(feed.getRemoteId()), folderIdMap).execute();
if (response.isSuccessful())
return true;
else if (response.code() == LibUtils.HTTP_NOT_FOUND)
throw new Resources.NotFoundException();
else
return false;
}
public boolean renameFeed(Feed feed) throws IOException {
Map<String, String> feedTitleMap = new HashMap<>();
feedTitleMap.put("feedTitle", feed.getName());
Response response = api.renameFeed(Integer.parseInt(feed.getRemoteId()), feedTitleMap).execute();
if (response.isSuccessful())
return true;
else if (response.code() == LibUtils.HTTP_NOT_FOUND)
throw new Resources.NotFoundException();
else
return false;
}
public enum StateType {
READ,
UNREAD,
STARRED,
UNSTARRED
}
}

View File

@ -1,6 +1,6 @@
package com.readrops.readropslibrary.services.nextcloudnews;
package com.readrops.api.services.nextcloudnews;
import com.readrops.readropslibrary.services.Credentials;
import com.readrops.api.services.Credentials;
public class NextNewsCredentials extends Credentials {

View File

@ -0,0 +1,63 @@
package com.readrops.api.services.nextcloudnews;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.api.services.nextcloudnews.json.NextNewsUser;
import java.util.List;
import java.util.Map;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface NextNewsService {
String END_POINT = "/index.php/apps/news/api/v1-2/";
@GET("user")
Call<NextNewsUser> getUser();
@GET("folders")
Call<List<Folder>> getFolders();
@GET("feeds")
Call<List<Feed>> getFeeds();
@GET("items")
Call<List<Item>> getItems(@Query("type") int type, @Query("getRead") boolean read, @Query("batchSize") int batchSize);
@GET("items/updated")
Call<List<Item>> getNewItems(@Query("lastModified") long lastModified, @Query("type") int type);
@PUT("items/{stateType}/multiple")
Call<ResponseBody> setArticlesState(@Path("stateType") String stateType, @Body Map<String, List<String>> itemIdsMap);
@POST("feeds")
Call<List<Feed>> createFeed(@Query("url") String url, @Query("folderId") int folderId);
@DELETE("feeds/{feedId}")
Call<Void> deleteFeed(@Path("feedId") int feedId);
@PUT("feeds/{feedId}/move")
Call<ResponseBody> changeFeedFolder(@Path("feedId") int feedId, @Body Map<String, Integer> folderIdMap);
@PUT("feeds/{feedId}/rename")
Call<ResponseBody> renameFeed(@Path("feedId") int feedId, @Body Map<String, String> feedTitleMap);
@POST("folders")
Call<List<Folder>> createFolder(@Body Map<String, String> folderName);
@DELETE("folders/{folderId}")
Call<ResponseBody> deleteFolder(@Path("folderId") int folderId);
@PUT("folders/{folderId}")
Call<ResponseBody> renameFolder(@Path("folderId") int folderId, @Body Map<String, String> folderName);
}

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.services.nextcloudnews;
package com.readrops.api.services.nextcloudnews;
import java.util.ArrayList;
import java.util.List;

View File

@ -0,0 +1,65 @@
package com.readrops.api.services.nextcloudnews.adapters
import android.annotation.SuppressLint
import com.readrops.db.entities.Feed
import com.readrops.api.utils.nextNullableString
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.ToJson
class NextNewsFeedsAdapter {
@ToJson
fun toJson(feed: Feed): String = ""
@SuppressLint("CheckResult")
@FromJson
fun fromJson(reader: JsonReader): List<Feed> {
val feeds = mutableListOf<Feed>()
reader.beginObject()
while (reader.hasNext()) {
if (reader.nextName() == "feeds") parseFeeds(reader, feeds) else reader.skipValue()
}
reader.endObject()
return feeds
}
private fun parseFeeds(reader: JsonReader, feeds: MutableList<Feed>) {
reader.beginArray()
while (reader.hasNext()) {
val feed = Feed()
reader.beginObject()
while (reader.hasNext()) {
with(feed) {
when (reader.selectName(NAMES)) {
0 -> remoteId = reader.nextString()
1 -> url = reader.nextString()
2 -> name = reader.nextString()
3 -> iconUrl = reader.nextString()
4 -> {
val nextInt = reader.nextInt()
remoteFolderId = if (nextInt > 0) nextInt.toString() else null
}
5 -> siteUrl = reader.nextNullableString()
else -> reader.skipValue()
}
}
}
feeds += feed
reader.endObject()
}
reader.endArray()
}
companion object {
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "url", "title", "faviconLink", "folderId", "link")
}
}

View File

@ -0,0 +1,50 @@
package com.readrops.api.services.nextcloudnews.adapters
import android.annotation.SuppressLint
import com.readrops.db.entities.Folder
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.ToJson
class NextNewsFoldersAdapter {
@ToJson
fun toJson(folder: Folder): String = ""
@SuppressLint("CheckResult")
@FromJson
fun fromJson(reader: JsonReader): List<Folder> {
val folders = mutableListOf<Folder>()
reader.beginObject()
reader.nextName() // "folders", beginning of folders array
reader.beginArray()
while (reader.hasNext()) {
val folder = Folder()
reader.beginObject()
while (reader.hasNext()) {
with(folder) {
when (reader.selectName(NAMES)) {
0 -> remoteId = reader.nextInt().toString()
1 -> name = reader.nextString()
else -> reader.skipValue()
}
}
}
folders += folder
reader.endObject()
}
reader.endArray()
reader.endObject()
return folders
}
companion object {
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "name")
}
}

View File

@ -0,0 +1,70 @@
package com.readrops.api.services.nextcloudnews.adapters
import android.annotation.SuppressLint
import com.readrops.db.entities.Item
import com.readrops.api.utils.LibUtils
import com.readrops.api.utils.nextNullableString
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import org.joda.time.DateTimeZone
import org.joda.time.LocalDateTime
class NextNewsItemsAdapter : JsonAdapter<List<Item>>() {
override fun toJson(writer: JsonWriter, value: List<Item>?) {
// no need of this
}
@SuppressLint("CheckResult")
@Override
override fun fromJson(reader: JsonReader): List<Item> {
val items = mutableListOf<Item>()
reader.beginObject()
reader.nextName() // "items", beginning of items array
reader.beginArray()
while (reader.hasNext()) {
val item = Item()
reader.beginObject()
var enclosureMime: String? = null
var enclosureLink: String? = null
while (reader.hasNext()) {
with(item) {
when (reader.selectName(NAMES)) {
0 -> remoteId = reader.nextInt().toString()
1 -> link = reader.nextNullableString()
2 -> title = reader.nextString()
3 -> author = reader.nextString()
4 -> pubDate = LocalDateTime(reader.nextLong() * 1000L, DateTimeZone.getDefault())
5 -> content = reader.nextString()
6 -> enclosureMime = reader.nextNullableString()
7 -> enclosureLink = reader.nextNullableString()
8 -> feedRemoteId = reader.nextInt().toString()
9 -> isRead = !reader.nextBoolean()
else -> reader.skipValue()
}
}
}
if (enclosureMime != null && LibUtils.isMimeImage(enclosureMime!!))
item.imageLink = enclosureLink
items += item
reader.endObject()
}
reader.endArray()
reader.endObject()
return items
}
companion object {
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "url", "title", "author",
"pubDate", "body", "enclosureMime", "enclosureLink", "feedId", "unread")
}
}

View File

@ -0,0 +1,14 @@
package com.readrops.api.services.nextcloudnews.json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NextNewsUser(val userId: String,
val displayName: String,
val lastLoginTimestamp: Long,
val avatar: Avatar?) {
@JsonClass(generateAdapter = true)
data class Avatar(val data: String,
val mime: String)
}

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.utils;
package com.readrops.api.utils;
public class ConflictException extends Exception {

View File

@ -1,7 +1,8 @@
package com.readrops.readropslibrary.utils;
package com.readrops.api.utils;
import com.readrops.readropslibrary.BuildConfig;
import com.readrops.readropslibrary.services.Credentials;
import androidx.annotation.Nullable;
import com.readrops.api.services.Credentials;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@ -10,7 +11,6 @@ import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
public class HttpManager {
@ -21,33 +21,19 @@ public class HttpManager {
buildOkHttp();
}
public HttpManager(final Credentials credentials) {
this.credentials = credentials;
buildOkHttp();
}
private void buildOkHttp() {
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder()
okHttpClient = new OkHttpClient.Builder()
.callTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.HOURS);
httpBuilder.addInterceptor(new AuthInterceptor());
if (BuildConfig.DEBUG) {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.level(HttpLoggingInterceptor.Level.BASIC);
httpBuilder.addInterceptor(interceptor);
}
okHttpClient = httpBuilder.build();
.readTimeout(1, TimeUnit.HOURS)
.addInterceptor(new AuthInterceptor())
.build();
}
public OkHttpClient getOkHttpClient() {
return okHttpClient;
}
public void setCredentials(Credentials credentials) {
public void setCredentials(@Nullable Credentials credentials) {
this.credentials = credentials;
}
@ -55,7 +41,6 @@ public class HttpManager {
return credentials;
}
private static HttpManager instance;
public static HttpManager getInstance() {
@ -66,6 +51,10 @@ public class HttpManager {
return instance;
}
public static void setInstance(OkHttpClient client) {
instance.okHttpClient = client;
}
public class AuthInterceptor implements Interceptor {
public AuthInterceptor() {
@ -76,7 +65,7 @@ public class HttpManager {
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (credentials.getAuthorization() != null) {
if (credentials != null && credentials.getAuthorization() != null) {
request = request.newBuilder()
.addHeader("Authorization", credentials.getAuthorization())
.build();

View File

@ -0,0 +1,6 @@
package com.readrops.api.utils
import com.squareup.moshi.JsonReader
fun JsonReader.nextNullableString(): String? =
if (peek() != JsonReader.Token.NULL) nextString() else nextNull()

View File

@ -1,8 +1,10 @@
package com.readrops.readropslibrary.utils;
package com.readrops.api.utils;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Scanner;
@ -38,4 +40,8 @@ public final class LibUtils {
return inputStreamToString(inputStream);
}
public static boolean isMimeImage(@NonNull String type) {
return type.equals("image") || type.equals("image/jpeg") || type.equals("image/jpg")
|| type.equals("image/png");
}
}

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.utils;
package com.readrops.api.utils;
public class ParseException extends Exception {

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary.utils;
package com.readrops.api.utils;
public class UnknownFormatException extends Exception {

View File

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

View File

@ -1,4 +1,4 @@
package com.readrops.readropslibrary;
package com.readrops.api;
import org.junit.Test;

View File

@ -1,30 +1,27 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.readrops.app"
minSdkVersion 21
targetSdkVersion 29
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
versionCode 10
versionName "1.1.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = [
"room.incremental": "true"
]
}
}
}
testOptions {
unitTests.returnDefaultValues = true
}
lintOptions {
abortOnError false
}
buildTypes {
release {
minifyEnabled true
@ -42,30 +39,38 @@ android {
}
}
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
dataBinding {
enabled = true
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':readropslibrary')
implementation project(':api')
implementation project(':db')
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.palette:palette: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 "androidx.core:core-ktx:1.2.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.work:work-runtime-ktx:2.4.0"
implementation "androidx.fragment:fragment-ktx:1.2.3"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
@ -77,20 +82,8 @@ dependencies {
transitive = false
}
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
kapt 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
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.5'
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
kapt 'androidx.lifecycle:lifecycle-common-java8:2.2.0'
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
@ -99,5 +92,10 @@ dependencies {
implementation 'com.mikepenz:materialdrawer:6.1.2'
implementation "com.mikepenz:aboutlibraries:6.2.3"
implementation 'com.facebook.stetho:stetho:1.5.1'
debugImplementation 'com.facebook.flipper:flipper:0.30.1'
debugImplementation 'com.facebook.soloader:soloader:0.8.0'
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.30.1'
debugImplementation 'com.icapps.niddler:niddler:1.2.0'
releaseImplementation 'com.icapps.niddler:niddler-noop:1.2.0'
}

View File

@ -26,9 +26,9 @@
-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.api.services.freshrss.json.** { *; }
-keep class com.readrops.api.services.nextcloudnews.json.** { *; }
-keep class com.readrops.readropslibrary.localfeed.** { *; }
-keep class com.readrops.api.localfeed.** { *; }
-keep class com.readrops.readropslibrary.opml.model.** { *; }
-keep class com.readrops.api.opml.model.** { *; }

View File

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

View File

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

View File

@ -0,0 +1,85 @@
package com.readrops.app;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Configuration;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.android.utils.FlipperUtils;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin;
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.soloader.SoLoader;
import com.icapps.niddler.core.AndroidNiddler;
import com.icapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor;
import com.readrops.api.utils.HttpManager;
public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provider {
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
initFlipper();
initNiddler();
}
private void initFlipper() {
if (FlipperUtils.shouldEnableFlipper(this)) {
FlipperClient client = AndroidFlipperClient.getInstance(this);
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
NetworkFlipperPlugin networkPlugin = new NetworkFlipperPlugin();
client.addPlugin(networkPlugin);
HttpManager.setInstance(
HttpManager.getInstance()
.getOkHttpClient()
.newBuilder()
.addInterceptor(new FlipperOkhttpInterceptor(networkPlugin))
.build());
client.addPlugin(new DatabasesFlipperPlugin(this));
client.addPlugin(CrashReporterPlugin.getInstance());
client.addPlugin(NavigationFlipperPlugin.getInstance());
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
client.start();
}
}
private void initNiddler() {
AndroidNiddler niddler = new AndroidNiddler.Builder()
.setNiddlerInformation(AndroidNiddler.fromApplication(this))
.setPort(0)
.setMaxStackTraceSize(10)
.build();
niddler.attachToApplication(this);
HttpManager.setInstance(HttpManager.getInstance().
getOkHttpClient().
newBuilder().
addInterceptor(new NiddlerOkHttpInterceptor(niddler, "default"))
.build());
niddler.start();
}
@NonNull
@Override
public Configuration getWorkManagerConfiguration() {
return new Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.build();
}
}

View File

@ -11,18 +11,17 @@
<application
android:name=".ReadropsApp"
android:allowBackup="true"
android:icon="@drawable/ic_readrops"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@drawable/ic_readrops"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<provider
android:authorities="${applicationId}"
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
@ -30,12 +29,19 @@
android:resource="@xml/file_paths" />
</provider>
<activity
android:name=".activities.NotificationPermissionActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".activities.WebViewActivity"
android:theme="@style/AppTheme.NoActionBar" />
<service android:name=".utils.feedscolors.FeedsColorsIntentService" />
<receiver android:name=".utils.SyncWorker$MarkReadReceiver" />
<receiver android:name=".utils.SyncWorker$ReadLaterReceiver" />
<activity android:name=".activities.SettingsActivity" />
<activity
android:name=".activities.SplashActivity"
@ -58,6 +64,7 @@
<activity
android:name=".activities.MainActivity"
android:label="@string/articles"
android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".activities.ItemActivity"
@ -66,7 +73,15 @@
<activity
android:name=".activities.AddFeedActivity"
android:label="@string/add_feed_title"
android:parentActivityName=".activities.MainActivity" />
android:parentActivityName=".activities.MainActivity">
<intent-filter android:label="@string/new_feed">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,5 +1,6 @@
package com.readrops.app;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@ -8,15 +9,16 @@ 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;
@SuppressLint("Registered")
public class ReadropsApp extends Application {
public static final String FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel";
public static final String OPML_EXPORT_CHANNEL_ID = "opmlExportChannel";
public static final String SYNC_CHANNEL_ID = "syncChannel";
@Override
public void onCreate() {
@ -25,11 +27,8 @@ public class ReadropsApp extends Application {
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)))
@ -48,10 +47,15 @@ public class ReadropsApp extends Application {
getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT);
opmlExportChannel.setDescription(getString(R.string.opml_export_description));
NotificationChannel syncChannel = new NotificationChannel(SYNC_CHANNEL_ID,
getString(R.string.auto_synchro), NotificationManager.IMPORTANCE_LOW);
syncChannel.setDescription(getString(R.string.account_synchro));
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(feedsColorsChannel);
manager.createNotificationChannel(opmlExportChannel);
manager.createNotificationChannel(syncChannel);
}
}
}

View File

@ -11,19 +11,18 @@ import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
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.db.entities.account.Account;
import com.readrops.db.entities.account.AccountType;
import java.util.ArrayList;
import java.util.List;
@ -52,8 +51,10 @@ public class AccountTypeListActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_account_type_list);
viewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
binding = ActivityAccountTypeListBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this).get(AccountViewModel.class);
setTitle(R.string.new_account);

View File

@ -8,16 +8,15 @@ import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import com.readrops.app.R;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.entities.account.AccountType;
import com.readrops.app.databinding.ActivityAddAccountBinding;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.app.viewmodels.AccountViewModel;
import com.readrops.db.entities.account.Account;
import com.readrops.db.entities.account.AccountType;
import io.reactivex.Completable;
import io.reactivex.CompletableObserver;
@ -44,8 +43,10 @@ public class AddAccountActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_add_account);
viewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
binding = ActivityAddAccountBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this).get(AccountViewModel.class);
accountType = getIntent().getParcelableExtra(ACCOUNT_TYPE);
@ -68,6 +69,9 @@ public class AddAccountActivity extends AppCompatActivity {
binding.providerImage.setImageResource(accountType.getIconRes());
binding.providerName.setText(accountType.getName());
binding.addAccountName.setText(accountType.getName());
if (accountType == AccountType.FRESHRSS) {
binding.addAccountPasswordLayout.setHelperText(getString(R.string.password_helper));
}
}
} catch (Exception e) {
// TODO : see how to handle this exception
@ -83,8 +87,8 @@ public class AddAccountActivity extends AppCompatActivity {
String login = binding.addAccountLogin.getText().toString().trim();
String password = binding.addAccountPassword.getText().toString().trim();
if (!(url.toLowerCase().contains("http://") || url.toLowerCase().contains("https://"))) {
url = "https://" + url;
if (!(url.toLowerCase().contains(Utils.HTTP_PREFIX) || url.toLowerCase().contains(Utils.HTTPS_PREFIX))) {
url = Utils.HTTPS_PREFIX + url;
}
if (editAccount) {

View File

@ -12,8 +12,7 @@ import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -25,8 +24,6 @@ import com.mikepenz.fastadapter.commons.utils.DiffCallback;
import com.mikepenz.fastadapter.commons.utils.FastAdapterDiffUtil;
import com.readrops.app.R;
import com.readrops.app.adapters.AccountArrayAdapter;
import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.databinding.ActivityAddFeedBinding;
import com.readrops.app.utils.FeedInsertionResult;
import com.readrops.app.utils.ParsingResult;
@ -34,14 +31,18 @@ import com.readrops.app.utils.ReadropsItemTouchCallback;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.app.viewmodels.AddFeedsViewModel;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.account.Account;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
public class AddFeedActivity extends AppCompatActivity implements View.OnClickListener {
@ -61,7 +62,9 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_add_feed);
binding = ActivityAddFeedBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.addFeedLoad.setOnClickListener(this);
binding.addFeedOk.setOnClickListener(this);
@ -80,7 +83,7 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi
return false;
});
viewModel = ViewModelProviders.of(this).get(AddFeedsViewModel.class);
viewModel = new ViewModelProvider(this).get(AddFeedsViewModel.class);
parseItemsAdapter = new ItemAdapter<>();
fastAdapter = FastAdapter.with(parseItemsAdapter);
@ -106,8 +109,8 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi
new ItemTouchHelper(new ReadropsItemTouchCallback(this,
new ReadropsItemTouchCallback.Config.Builder()
.swipeDirs(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)
.leftDraw(Color.RED, R.drawable.ic_delete)
.rightDraw(Color.RED, R.drawable.ic_delete)
.leftDraw(Color.RED, R.drawable.ic_delete, null)
.rightDraw(Color.RED, R.drawable.ic_delete, null)
.swipeCallback((viewHolder, direction) -> {
parseItemsAdapter.remove(viewHolder.getAdapterPosition());
@ -125,6 +128,18 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi
binding.addFeedInsertedResultsRecyclerview.setLayoutManager(layoutManager1);
viewModel.getAccounts().observe(this, accounts -> {
// set the current account at the top of the list
int currentAccountId = getIntent().getIntExtra(ACCOUNT_ID, 1);
Collections.sort(accounts, (o1, o2) -> {
if (o1.getId() == currentAccountId) {
return -1;
} else if (o2.getId() == currentAccountId) {
return 1;
} else {
return 0;
}
});
arrayAdapter = new AccountArrayAdapter(this, accounts);
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
@ -132,6 +147,13 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi
});
feedsToUpdate = new ArrayList<>();
// new feed intent
if (getIntent().getAction() != null && getIntent().getAction().equals(Intent.ACTION_SEND)) {
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
binding.addFeedTextInput.setText(text);
onClick(binding.addFeedLoad);
}
}
@Override

View File

@ -29,7 +29,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ShareCompat;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import com.afollestad.materialdialogs.MaterialDialog;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
@ -39,8 +39,6 @@ import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.readrops.app.R;
import com.readrops.app.database.entities.Item;
import com.readrops.app.database.pojo.ItemWithFeed;
import com.readrops.app.utils.DateUtils;
import com.readrops.app.utils.GlideApp;
import com.readrops.app.utils.PermissionManager;
@ -48,6 +46,11 @@ 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 com.readrops.db.entities.Item;
import com.readrops.db.pojo.ItemWithFeed;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR;
import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL;
@ -79,6 +82,7 @@ public class ItemActivity extends AppCompatActivity {
private CoordinatorLayout rootLayout;
private String urlToDownload;
private String imageTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -142,7 +146,7 @@ public class ItemActivity extends AppCompatActivity {
}
}));
viewModel = ViewModelProviders.of(this).get(ItemViewModel.class);
viewModel = new ViewModelProvider(this).get(ItemViewModel.class);
viewModel.getItemById(itemId).observe(this, this::bindUI);
actionButton.setOnClickListener(v -> openInNavigator());
}
@ -172,7 +176,7 @@ public class ItemActivity extends AppCompatActivity {
Utils.setDrawableColor(dateLayout.getBackground(), itemWithFeed.getColor());
}
if (item.getAuthor() != null) {
if (item.getAuthor() != null && !item.getAuthor().isEmpty()) {
author.setText(getString(R.string.by_author, item.getAuthor()));
author.setVisibility(View.VISIBLE);
}
@ -282,15 +286,40 @@ public class ItemActivity extends AppCompatActivity {
.title(R.string.image_options)
.items(R.array.image_options)
.itemsCallback((dialog, itemView, position, text) -> {
if (position == 0)
shareImage(hitTestResult.getExtra());
else {
if (PermissionManager.isPermissionGranted(this, Manifest.permission.WRITE_EXTERNAL_STORAGE))
downloadImage(hitTestResult.getExtra());
else {
switch (position) {
case 0:
shareImage(hitTestResult.getExtra());
break;
case 1:
if (PermissionManager.isPermissionGranted(this, Manifest.permission.WRITE_EXTERNAL_STORAGE))
downloadImage(hitTestResult.getExtra());
else {
urlToDownload = hitTestResult.getExtra();
PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST, Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
break;
case 2:
urlToDownload = hitTestResult.getExtra();
PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST, Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
String content = webView.getItemContent();
Pattern p = Pattern.compile("(<img.*src=\"" + urlToDownload + "\".*>)");
Matcher m = p.matcher(content);
if (m.matches()) {
Pattern p2 = Pattern.compile("<img.*(title|alt)=\"(.*?)\".*>");
Matcher m2 = p2.matcher(content);
if (m2.matches()) {
imageTitle = m2.group(2);
} else {
imageTitle = "";
}
}
new MaterialDialog.Builder(this)
.title(urlToDownload)
.content(imageTitle)
.show();
break;
default:
throw new IllegalStateException("Unexpected value: " + position);
}
})

View File

@ -1,7 +1,7 @@
package com.readrops.app.activities;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
@ -14,12 +14,15 @@ import android.widget.ProgressBar;
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.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import androidx.paging.PagedList;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
@ -31,6 +34,7 @@ import com.afollestad.materialdialogs.MaterialDialog;
import com.bumptech.glide.Glide;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.mikepenz.aboutlibraries.Libs;
import com.mikepenz.aboutlibraries.LibsBuilder;
import com.mikepenz.materialdrawer.Drawer;
@ -39,21 +43,22 @@ import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import com.readrops.app.R;
import com.readrops.app.adapters.MainItemListAdapter;
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.utils.DrawerManager;
import com.readrops.app.utils.GlideApp;
import com.readrops.app.utils.ReadropsItemTouchCallback;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.app.viewmodels.MainViewModel;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import com.readrops.db.filters.FilterType;
import com.readrops.db.filters.ListSortType;
import com.readrops.db.pojo.ItemWithFeed;
import org.jetbrains.annotations.NotNull;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -65,6 +70,7 @@ 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.ACCOUNT_ID;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY;
import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL;
@ -99,6 +105,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
private RelativeLayout syncProgressLayout;
private TextView syncProgress;
private ProgressBar syncProgressBar;
private FloatingActionButton actionButton;
private int feedCount;
private int feedNb;
@ -127,12 +134,12 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
syncProgressLayout = findViewById(R.id.sync_progress_layout);
syncProgress = findViewById(R.id.sync_progress_text_view);
syncProgressBar = findViewById(R.id.sync_progress_bar);
syncProgressBar = findViewById(R.id.sync_progress_bar);
actionButton = findViewById(R.id.add_feed_fab);
feedCount = 0;
initRecyclerView();
viewModel = ViewModelProviders.of(this).get(MainViewModel.class);
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
viewModel.setShowReadItems(SharedPreferencesManager.readBoolean(this,
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES));
@ -140,7 +147,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
viewModel.getItemsWithFeed().observe(this, itemWithFeeds -> {
allItems = itemWithFeeds;
if (itemWithFeeds.size() > 0)
if (!itemWithFeeds.isEmpty())
emptyListLayout.setVisibility(View.GONE);
else
emptyListLayout.setVisibility(View.VISIBLE);
@ -197,14 +204,23 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
getAccountCredentials(accounts);
viewModel.setAccounts(accounts);
// the activity was just opened
if (drawer == null) {
drawer = drawerManager.buildDrawer(accounts);
int currentAccountId = 0;
if (getIntent().hasExtra(ACCOUNT_ID)) { // coming from a notification
currentAccountId = getIntent().getIntExtra(ACCOUNT_ID, 1);
viewModel.setCurrentAccount(currentAccountId);
}
drawer = drawerManager.buildDrawer(accounts, currentAccountId);
drawer.setSelection(DrawerManager.ARTICLES_ITEM_ID);
updateDrawerFeeds();
} else if (accounts.size() < drawerManager.getNumberOfProfiles() && accounts.size() > 0) {
openItemActivity(getIntent());
} else if (accounts.size() < drawerManager.getNumberOfProfiles() && !accounts.isEmpty()) {
drawerManager.updateHeader(accounts);
updateDrawerFeeds();
} else if (accounts.size() == 0) {
} else if (accounts.isEmpty()) {
Intent intent = new Intent(this, AccountTypeListActivity.class);
startActivity(intent);
finish();
@ -219,9 +235,32 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
onRefresh();
savedInstanceState.clear();
}
});
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
openItemActivity(intent);
}
private void openItemActivity(Intent intent) {
if (intent.hasExtra(ITEM_ID) && intent.hasExtra(IMAGE_URL)) {
Intent itemIntent = new Intent(this, ItemActivity.class);
itemIntent.putExtras(intent);
startActivity(itemIntent);
viewModel.setItemReadState(intent.getIntExtra(ITEM_ID, 0), true, true)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage()))
.subscribe();
}
}
private void handleDrawerClick(IDrawerItem drawerItem) {
if (drawerItem instanceof PrimaryDrawerItem) {
@ -230,12 +269,12 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
switch (id) {
case DrawerManager.ARTICLES_ITEM_ID:
viewModel.setFilterType(MainViewModel.FilterType.NO_FILTER);
viewModel.setFilterType(FilterType.NO_FILTER);
scrollToTop = true;
viewModel.invalidate();
break;
case DrawerManager.READ_LATER_ID:
viewModel.setFilterType(MainViewModel.FilterType.READ_IT_LATER_FILTER);
viewModel.setFilterType(FilterType.READ_IT_LATER_FILTER);
viewModel.invalidate();
break;
case DrawerManager.ABOUT_ID:
@ -252,7 +291,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
drawer.closeDrawer();
viewModel.setFilterFeedId((int) drawerItem.getIdentifier());
viewModel.setFilterType(MainViewModel.FilterType.FEED_FILTER);
viewModel.setFilterType(FilterType.FEED_FILTER);
viewModel.invalidate();
}
}
@ -308,8 +347,11 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
updateDrawerFeeds();
} else {
adapter.toggleSelection(position);
int selectionSize = adapter.getSelection().size();
if (adapter.getSelection().isEmpty())
if (selectionSize > 0)
actionMode.setTitle(String.valueOf(selectionSize));
else
actionMode.finish();
}
}
@ -321,7 +363,9 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
selectedItemWithFeed = itemWithFeed;
adapter.toggleSelection(position);
actionMode = startActionMode(MainActivity.this);
actionMode.setTitle(String.valueOf(adapter.getSelection().size()));
}
});
@ -341,12 +385,16 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
recyclerView.setAdapter(adapter);
Drawable readLater = ContextCompat.getDrawable(this, R.drawable.ic_read_later).mutate();
DrawableCompat.setTint(readLater, ContextCompat.getColor(this, android.R.color.white));
new ItemTouchHelper(new ReadropsItemTouchCallback(this,
new ReadropsItemTouchCallback.Config.Builder()
.swipeDirs(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)
.swipeCallback(this)
.leftDraw(Color.DKGRAY, R.drawable.ic_read_later)
.rightDraw(Color.DKGRAY, R.drawable.ic_read)
.leftDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read_later, readLater)
.rightDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read, null)
.build()))
.attachToRecyclerView(recyclerView);
@ -361,13 +409,24 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
if (scrollToTop) {
;if (scrollToTop) {
recyclerView.scrollToPosition(0);
scrollToTop = false;
} else
super.onItemRangeMoved(fromPosition, toPosition, itemCount);
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy > 0) {
actionButton.hide();
} else {
actionButton.show();
}
}
});
}
@Override
@ -391,7 +450,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
.doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage()))
.subscribe();
if (viewModel.getFilterType() == MainViewModel.FilterType.READ_IT_LATER_FILTER)
if (viewModel.getFilterType() == FilterType.READ_IT_LATER_FILTER)
adapter.notifyItemChanged(viewHolder.getAdapterPosition());
}
}
@ -400,7 +459,9 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
refreshLayout.setEnabled(false);
actionMode.getMenuInflater().inflate(R.menu.item_list_contextual_menu, menu);
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.primary_dark));
return true;
}
@ -492,49 +553,51 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
Utils.showSnackbar(rootLayout, e.getMessage());
}
});
} else
} else {
sync(null);
}
}
public void openAddFeedActivity(View view) {
Intent intent = new Intent(this, AddFeedActivity.class);
intent.putExtra(ACCOUNT_ID, viewModel.getCurrentAccount().getId());
startActivityForResult(intent, ADD_FEED_REQUEST);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == ADD_FEED_REQUEST && resultCode == RESULT_OK) {
if (data != null) {
ArrayList<Feed> feeds = data.getParcelableArrayListExtra(FEEDS);
if (requestCode == ADD_FEED_REQUEST && resultCode == RESULT_OK && data != null) {
List<Feed> feeds = data.getParcelableArrayListExtra(FEEDS);
if (feeds != null && feeds.size() > 0 && viewModel.isAccountLocal()) {
refreshLayout.setRefreshing(true);
feedNb = feeds.size();
sync(feeds);
}
if (feeds != null && !feeds.isEmpty() && viewModel.isAccountLocal()) {
refreshLayout.setRefreshing(true);
feedNb = feeds.size();
sync(feeds);
}
} else if (requestCode == MANAGE_ACCOUNT_REQUEST) {
updateDrawerFeeds();
} else if (requestCode == ADD_ACCOUNT_REQUEST && resultCode == RESULT_OK) {
if (data != null) {
Account newAccount = data.getParcelableExtra(ACCOUNT);
} else if (requestCode == ADD_ACCOUNT_REQUEST && resultCode == RESULT_OK && data != null) {
Account newAccount = data.getParcelableExtra(ACCOUNT);
if (newAccount != null) {
viewModel.addAccount(newAccount);
adapter.clearData();
if (!viewModel.isAccountLocal()) {
getAccountCredentials(Collections.singletonList(newAccount));
refreshLayout.setRefreshing(true);
onRefresh();
}
drawerManager.resetItems();
drawerManager.addAccount(newAccount, true);
if (newAccount != null) {
// get credentials before creating the repository
if (!newAccount.isLocal()) {
getAccountCredentials(Collections.singletonList(newAccount));
}
viewModel.addAccount(newAccount);
adapter.clearData();
// start syncing only if the account is not local
if (!viewModel.isAccountLocal()) {
refreshLayout.setRefreshing(true);
onRefresh();
}
drawerManager.resetItems();
drawerManager.addAccount(newAccount, true);
}
}
@ -635,6 +698,12 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
case R.id.item_sort:
displayFilterDialog();
return true;
case R.id.start_sync:
if (!viewModel.isAccountLocal()) {
refreshLayout.setRefreshing(true);
}
onRefresh();
break;
}
return super.onOptionsItemSelected(item);
@ -707,8 +776,4 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
super.onSaveInstanceState(outState);
}
public enum ListSortType {
NEWEST_TO_OLDEST,
OLDEST_TO_NEWEST
}
}

View File

@ -6,23 +6,22 @@ import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.app.R;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.databinding.ActivityManageFeedsFoldersBinding;
import com.readrops.app.fragments.FeedsFragment;
import com.readrops.app.fragments.FoldersFragment;
import com.readrops.app.utils.Utils;
import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
import com.readrops.readropslibrary.utils.ConflictException;
import com.readrops.readropslibrary.utils.UnknownFormatException;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import com.readrops.api.utils.ConflictException;
import com.readrops.api.utils.UnknownFormatException;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@ -31,7 +30,6 @@ import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
public class ManageFeedsFoldersActivity extends AppCompatActivity {
private ActivityManageFeedsFoldersBinding binding;
private ManageFeedsFoldersViewModel viewModel;
@ -41,7 +39,9 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_manage_feeds_folders);
binding = ActivityManageFeedsFoldersBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.manageFeedsFoldersToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -52,7 +52,7 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity {
binding.manageFeedsFoldersViewpager.setAdapter(pageAdapter);
binding.manageFeedsFoldersTablayout.setupWithViewPager(binding.manageFeedsFoldersViewpager);
viewModel = ViewModelProviders.of(this).get(ManageFeedsFoldersViewModel.class);
viewModel = new ViewModelProvider(this).get(ManageFeedsFoldersViewModel.class);
viewModel.setAccount(account);
}

View File

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

View File

@ -7,7 +7,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.readrops.app.R;
import com.readrops.app.database.entities.account.Account;
import com.readrops.db.entities.account.Account;
import com.readrops.app.fragments.settings.AccountSettingsFragment;
import com.readrops.app.fragments.settings.SettingsFragment;

View File

@ -4,7 +4,7 @@ import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import com.readrops.app.R;
import com.readrops.app.viewmodels.AccountViewModel;
@ -22,7 +22,7 @@ public class SplashActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
viewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
viewModel = new ViewModelProvider(this).get(AccountViewModel.class);
viewModel.getAccountCount()
.subscribeOn(Schedulers.io())
@ -47,5 +47,12 @@ public class SplashActivity extends AppCompatActivity {
}
});
/*PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(SyncWorker.class, 15, TimeUnit.MINUTES)
.addTag(SyncWorker.Companion.getTAG())
.build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(SyncWorker.Companion.getTAG(), ExistingPeriodicWorkPolicy.REPLACE, request);*/
}
}

View File

@ -2,22 +2,22 @@ package com.readrops.app.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.PorterDuff
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
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
import com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR
import com.readrops.app.utils.ReadropsKeys.WEB_URL
class WebViewActivity : AppCompatActivity() {
@ -25,21 +25,26 @@ class WebViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_web_view)
binding = ActivityWebViewBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.activityWebViewToolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
title = ""
val actionBarColor = intent.getIntExtra(ACTION_BAR_COLOR, ContextCompat.getColor(this, R.color.colorPrimary))
supportActionBar?.setBackgroundDrawable(ColorDrawable(actionBarColor))
setWebViewSettings()
binding.activityWebViewSwipe.setOnRefreshListener { binding.webView.reload() }
binding.activityWebViewProgress.indeterminateDrawable.setColorFilter(actionBarColor, PorterDuff.Mode.SRC_IN)
binding.activityWebViewProgress.max = 100
with(binding) {
activityWebViewSwipe.setOnRefreshListener { binding.webView.reload() }
activityWebViewProgress.progressTintList = ColorStateList.valueOf(actionBarColor)
activityWebViewProgress.max = 100
val url: String = intent.getStringExtra(ReadropsKeys.WEB_URL)!!
webView.loadUrl(url)
}
val url: String = intent.getStringExtra(WEB_URL)
binding.webView.loadUrl(url)
}
@SuppressLint("SetJavaScriptEnabled")
@ -56,9 +61,11 @@ class WebViewActivity : AppCompatActivity() {
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
binding.activityWebViewSwipe.isRefreshing = false
binding.activityWebViewProgress.progress = 0
binding.activityWebViewProgress.visibility = View.VISIBLE
with(binding) {
activityWebViewSwipe.isRefreshing = false
activityWebViewProgress.progress = 0
activityWebViewProgress.visibility = View.VISIBLE
}
super.onPageStarted(view, url, favicon)
}
@ -73,9 +80,11 @@ class WebViewActivity : AppCompatActivity() {
}
override fun onProgressChanged(view: WebView?, newProgress: Int) {
binding.activityWebViewProgress.progress = newProgress
if (newProgress == 100)
binding.activityWebViewProgress.visibility = View.GONE
with(binding) {
activityWebViewProgress.progress = newProgress
if (newProgress == 100) activityWebViewProgress.visibility = View.GONE
}
super.onProgressChanged(view, newProgress)
}
@ -106,13 +115,15 @@ class WebViewActivity : AppCompatActivity() {
}
}
return super.onOptionsItemSelected(item)
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())
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, binding.webView.url.toString())
}
startActivity(Intent.createChooser(intent, getString(R.string.share_url)))
}

View File

@ -12,7 +12,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.app.R;
import com.readrops.app.database.entities.account.Account;
import com.readrops.db.entities.account.Account;
import java.util.List;
@ -20,11 +20,8 @@ public class AccountArrayAdapter extends ArrayAdapter<Account> {
public AccountArrayAdapter(@NonNull Context context, @NonNull List<Account> objects) {
super(context, 0, objects);
}
@Override
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
return createItemView(position, convertView, parent);
@ -46,8 +43,8 @@ public class AccountArrayAdapter extends ArrayAdapter<Account> {
ImageView accountIcon = convertView.findViewById(R.id.account_type_logo);
TextView accountName = convertView.findViewById(R.id.account_type_name);
accountIcon.setImageResource(account.getAccountType().getIconRes());
accountName.setText(account.getAccountType().getName());
accountIcon.setImageResource(account.getAccountType().getIconRes());
accountName.setText(account.getAccountType().getName());
return convertView;
}

View File

@ -4,12 +4,10 @@ import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.readrops.app.R;
import com.readrops.app.database.entities.account.AccountType;
import com.readrops.app.databinding.AccountTypeItemBinding;
import com.readrops.db.entities.account.AccountType;
import java.util.List;
@ -25,8 +23,8 @@ public class AccountTypeListAdapter extends RecyclerView.Adapter<AccountTypeList
@NonNull
@Override
public AccountTypeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
AccountTypeItemBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
R.layout.account_type_item, parent, false);
AccountTypeItemBinding binding = AccountTypeItemBinding.inflate(LayoutInflater.from(parent.getContext()),
parent, false);
return new AccountTypeViewHolder(binding);
}

View File

@ -14,7 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.readrops.app.R;
import com.readrops.app.database.pojo.FeedWithFolder;
import com.readrops.db.pojo.FeedWithFolder;
import com.readrops.app.utils.GlideApp;
import java.util.List;
@ -72,10 +72,10 @@ public class FeedsAdapter extends ListAdapter<FeedWithFolder, FeedsAdapter.ViewH
GlideApp.with(viewHolder.itemView.getContext())
.load(feedWithFolder.getFeed().getIconUrl())
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_rss_feed)
.placeholder(R.drawable.ic_rss_feed_grey)
.into(viewHolder.feedIcon);
} else
viewHolder.feedIcon.setImageResource(R.drawable.ic_rss_feed);
viewHolder.feedIcon.setImageResource(R.drawable.ic_rss_feed_grey);
viewHolder.feedName.setText(feedWithFolder.getFeed().getName());
if (feedWithFolder.getFeed().getDescription() != null) {

View File

@ -6,21 +6,21 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.readrops.app.R;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.pojo.FolderWithFeedCount;
import com.readrops.app.databinding.FolderLayoutBinding;
import com.readrops.db.entities.Folder;
import com.readrops.db.pojo.FolderWithFeedCount;
import java.util.List;
public class FoldersAdapter extends ListAdapter<FolderWithFeedCount, FoldersAdapter.FolderViewHolder> {
private ManageFoldersListener listener;
private int totalFeedCount;
public FoldersAdapter(ManageFoldersListener listener) {
super(DIFF_CALLBACK);
@ -28,6 +28,9 @@ public class FoldersAdapter extends ListAdapter<FolderWithFeedCount, FoldersAdap
this.listener = listener;
}
public void setTotalFeedCount(int totalFeedCount) {
this.totalFeedCount = totalFeedCount;
}
private static final DiffUtil.ItemCallback<FolderWithFeedCount> DIFF_CALLBACK = new DiffUtil.ItemCallback<FolderWithFeedCount>() {
@Override
@ -51,21 +54,18 @@ public class FoldersAdapter extends ListAdapter<FolderWithFeedCount, FoldersAdap
@NonNull
@Override
public FolderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
FolderLayoutBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
R.layout.folder_layout, parent, false);
FolderLayoutBinding binding = FolderLayoutBinding.inflate(LayoutInflater.from(parent.getContext()),
parent, false);
return new FolderViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull FolderViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.size() > 0) {
FolderWithFeedCount folder = (FolderWithFeedCount) payloads.get(0);
if (!payloads.isEmpty()) {
FolderWithFeedCount folderWithFeedCount = (FolderWithFeedCount) payloads.get(0);
holder.binding.folderName.setText(folder.getFolder().getName());
int stringRes = folder.getFeedCount() > 1 ? R.string.feeds_number : R.string.feed_number;
holder.binding.folderFeedsCount.setText(holder.itemView.getContext().getString(stringRes, String.valueOf(folder.getFeedCount())));
holder.bind(folderWithFeedCount);
} else
onBindViewHolder(holder, position);
@ -73,18 +73,10 @@ public class FoldersAdapter extends ListAdapter<FolderWithFeedCount, FoldersAdap
@Override
public void onBindViewHolder(@NonNull FolderViewHolder holder, int position) {
FolderWithFeedCount folder = getItem(position);
FolderWithFeedCount folderWithFeedCount = getItem(position);
holder.binding.folderName.setText(folder.getFolder().getName());
int stringRes = folder.getFeedCount() > 1 ? R.string.feeds_number : R.string.feed_number;
holder.binding.folderFeedsCount.setText(holder.itemView.getContext().getString(stringRes, String.valueOf(folder.getFeedCount())));
holder.itemView.setOnClickListener(v -> listener.onClick(folder.getFolder()));
}
public Folder getFolder(int position) {
return getItem(position).getFolder();
holder.bind(folderWithFeedCount);
holder.itemView.setOnClickListener(v -> listener.onClick(folderWithFeedCount.getFolder()));
}
public interface ManageFoldersListener {
@ -100,5 +92,15 @@ public class FoldersAdapter extends ListAdapter<FolderWithFeedCount, FoldersAdap
this.binding = binding;
}
private void bind(FolderWithFeedCount folderWithFeedCount) {
binding.folderName.setText(folderWithFeedCount.getFolder().getName());
int stringRes = folderWithFeedCount.getFeedCount() > 1 ? R.string.feeds_number : R.string.feed_number;
binding.folderFeedsCount.setText(itemView.getContext().getString(stringRes, String.valueOf(folderWithFeedCount.getFeedCount())));
binding.folderProgressBar.setMax(totalFeedCount);
binding.folderProgressBar.setProgress(folderWithFeedCount.getFeedCount());
}
}
}

View File

@ -26,8 +26,8 @@ import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.transition.DrawableCrossFadeFactory;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.readrops.app.R;
import com.readrops.app.database.entities.Item;
import com.readrops.app.database.pojo.ItemWithFeed;
import com.readrops.db.entities.Item;
import com.readrops.db.pojo.ItemWithFeed;
import com.readrops.app.databinding.ListItemBinding;
import com.readrops.app.utils.DateUtils;
import com.readrops.app.utils.GlideRequests;

View File

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

View File

@ -1,40 +0,0 @@
package com.readrops.app.database;
import android.content.Context;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
import com.readrops.app.database.dao.AccountDao;
import com.readrops.app.database.dao.FeedDao;
import com.readrops.app.database.dao.FolderDao;
import com.readrops.app.database.dao.ItemDao;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.Item;
@androidx.room.Database(entities = {Feed.class, Item.class, Folder.class, Account.class}, version = 1, exportSchema = false)
@TypeConverters({Converters.class})
public abstract class Database extends RoomDatabase {
public abstract FeedDao feedDao();
public abstract ItemDao itemDao();
public abstract FolderDao folderDao();
public abstract AccountDao accountDao();
private static Database database;
public static Database getInstance(Context context) {
if (database == null)
database = Room.databaseBuilder(context, Database.class, "readrops-db")
.build();
return database;
}
}

View File

@ -1,36 +0,0 @@
package com.readrops.app.database.dao;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Query;
import com.readrops.app.database.entities.account.Account;
import java.util.List;
import io.reactivex.Single;
@Dao
public abstract class AccountDao implements BaseDao<Account> {
@Query("Select * from Account")
public abstract LiveData<List<Account>> selectAll();
@Query("Update Account set last_modified = :lastModified Where id = :accountId")
public abstract void updateLastModified(int accountId, long lastModified);
@Query("Update Account set current_account = 0 Where id Not In (:accountId)")
public abstract void deselectOldCurrentAccount(int accountId);
@Query("Update Account set current_account = 1 Where id = :accountId")
public abstract void setCurrentAccount(int accountId);
@Query("Select count(*) From Account Where account_type = :accountType")
public abstract Integer getAccountCountByType(int accountType);
@Query("Select count(*) From Account")
public abstract Single<Integer> getAccountCount();
@Query("Update Account set writeToken = :writeToken Where id = :accountId")
public abstract void updateWriteToken(int accountId, String writeToken);
}

View File

@ -11,15 +11,15 @@ import android.widget.Spinner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.textfield.TextInputEditText;
import com.readrops.app.R;
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.FeedWithFolder;
import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import com.readrops.db.pojo.FeedWithFolder;
import java.util.ArrayList;
import java.util.Map;
@ -59,7 +59,7 @@ public class EditFeedDialogFragment extends DialogFragment implements AdapterVie
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
viewModel = ViewModelProviders.of(getActivity()).get(ManageFeedsFoldersViewModel.class);
viewModel = new ViewModelProvider(getActivity()).get(ManageFeedsFoldersViewModel.class);
feedWithFolder = getArguments().getParcelable("feedWithFolder");
account = getArguments().getParcelable(ACCOUNT);

View File

@ -6,19 +6,19 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.readrops.app.R
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
import com.readrops.db.entities.account.Account
import com.readrops.db.pojo.FeedWithFolder
class FeedOptionsDialogFragment : BottomSheetDialogFragment() {
private lateinit var feedWithFolder: FeedWithFolder
private lateinit var account: Account
private lateinit var binding: FeedOptionsLayoutBinding
private var _binding: FeedOptionsLayoutBinding? = null
private val binding get() = _binding!!
companion object {
const val FEED_KEY = "FEED_KEY"
@ -43,7 +43,7 @@ class FeedOptionsDialogFragment : BottomSheetDialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.feed_options_layout, container, false)
_binding = FeedOptionsLayoutBinding.inflate(inflater, container, false)
return binding.root
}
@ -58,6 +58,11 @@ class FeedOptionsDialogFragment : BottomSheetDialogFragment() {
binding.feedOptionsDeleteLayout.setOnClickListener { deleteFeed() }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun openEditFeedDialog() {
dismiss()
val editFeedDialogFragment = EditFeedDialogFragment.newInstance(feedWithFolder, account)

View File

@ -10,19 +10,19 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.app.R;
import com.readrops.app.adapters.FeedsAdapter;
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.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.account.Account;
import com.readrops.db.pojo.FeedWithFolder;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableCompletableObserver;
@ -64,7 +64,7 @@ public class FeedsFragment extends Fragment {
if (account.getPassword() == null)
account.setPassword(SharedPreferencesManager.readString(getContext(), account.getPasswordKey()));
viewModel = ViewModelProviders.of(this).get(ManageFeedsFoldersViewModel.class);
viewModel = new ViewModelProvider(this).get(ManageFeedsFoldersViewModel.class);
viewModel.setAccount(account);
viewModel.getFeedsWithFolder().observe(this, feedWithFolders -> {
@ -81,7 +81,7 @@ public class FeedsFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentFeedsBinding.inflate(inflater);
binding = FragmentFeedsBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@ -107,6 +107,12 @@ public class FeedsFragment extends Fragment {
binding.feedsRecyclerview.setAdapter(adapter);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
public void deleteFeed(Feed feed) {
new MaterialDialog.Builder(getContext())
.title(R.string.delete_feed)

View File

@ -4,16 +4,51 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.readrops.app.R
import com.readrops.app.database.entities.Folder
import com.readrops.app.databinding.FolderOptionsLayoutBinding
import com.readrops.db.entities.Folder
class FolderOptionsDialogFragment : BottomSheetDialogFragment() {
private lateinit var folder: Folder
private lateinit var foldersOptionsLayoutBinding: FolderOptionsLayoutBinding
private var _binding: FolderOptionsLayoutBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
folder = arguments?.getParcelable(FOLDER_KEY)!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FolderOptionsLayoutBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.folderOptionsTitle.text = folder.name
binding.folderOptionsEdit.setOnClickListener { openEditFolderDialog() }
binding.folderOptionsDelete.setOnClickListener { deleteFolder() }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun openEditFolderDialog() {
dismiss()
(parentFragment as FoldersFragment).editFolder(folder)
}
private fun deleteFolder() {
dismiss()
(parentFragment as FoldersFragment).deleteFolder(folder)
}
companion object {
const val FOLDER_KEY = "FOLDER_KEY"
@ -28,34 +63,4 @@ class FolderOptionsDialogFragment : BottomSheetDialogFragment() {
return fragment
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
folder = arguments?.getParcelable(FOLDER_KEY)!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
foldersOptionsLayoutBinding = DataBindingUtil.inflate(inflater, R.layout.folder_options_layout, container, false)
return foldersOptionsLayoutBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
foldersOptionsLayoutBinding.folderOptionsTitle.text = folder.name
foldersOptionsLayoutBinding.folderOptionsEdit.setOnClickListener { openEditFolderDialog() }
foldersOptionsLayoutBinding.folderOptionsDelete.setOnClickListener { deleteFolder() }
}
private fun openEditFolderDialog() {
dismiss()
(parentFragment as FoldersFragment).editFolder(folder)
}
private fun deleteFolder() {
dismiss()
(parentFragment as FoldersFragment).deleteFolder(folder)
}
}

View File

@ -9,24 +9,24 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.afollestad.materialdialogs.MaterialDialog;
import com.readrops.app.R;
import com.readrops.app.adapters.FoldersAdapter;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.account.Account;
import com.readrops.app.databinding.FragmentFoldersBinding;
import com.readrops.app.utils.SharedPreferencesManager;
import com.readrops.app.utils.Utils;
import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
import com.readrops.readropslibrary.utils.ConflictException;
import com.readrops.readropslibrary.utils.UnknownFormatException;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import com.readrops.api.utils.ConflictException;
import com.readrops.api.utils.UnknownFormatException;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
@ -65,13 +65,31 @@ public class FoldersFragment extends Fragment {
account.setPassword(SharedPreferencesManager.readString(getContext(), account.getPasswordKey()));
adapter = new FoldersAdapter(this::openFolderOptionsDialog);
viewModel = ViewModelProviders.of(this).get(ManageFeedsFoldersViewModel.class);
viewModel = new ViewModelProvider(this).get(ManageFeedsFoldersViewModel.class);
viewModel.setAccount(account);
viewModel.getFeedCountByAccount()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableSingleObserver<Integer>() {
@Override
public void onSuccess(Integer feedCount) {
adapter.setTotalFeedCount(feedCount);
getFoldersWithFeedCount();
}
@Override
public void onError(Throwable e) {
Utils.showSnackbar(binding.foldersRoot, e.getMessage());
}
});
}
private void getFoldersWithFeedCount() {
viewModel.getFoldersWithFeedCount().observe(this, folders -> {
adapter.submitList(folders);
if (folders.size() > 0) {
if (!folders.isEmpty()) {
binding.foldersEmptyList.setVisibility(View.GONE);
} else {
binding.foldersEmptyList.setVisibility(View.VISIBLE);
@ -82,7 +100,7 @@ public class FoldersFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_folders, container, false);
binding = FragmentFoldersBinding.inflate(inflater, container, false);
return binding.getRoot();
}

View File

@ -17,7 +17,7 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
@ -26,14 +26,16 @@ 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.activities.NotificationPermissionActivity;
import com.readrops.app.utils.PermissionManager;
import com.readrops.app.utils.SharedPreferencesManager;
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 com.readrops.db.entities.account.Account;
import com.readrops.db.entities.account.AccountType;
import com.readrops.api.opml.OPMLParser;
import com.readrops.api.opml.model.OPML;
import java.io.File;
import java.io.FileOutputStream;
@ -46,6 +48,7 @@ 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.ACCOUNT_ID;
import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
/**
@ -85,6 +88,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
Preference credentialsPref = findPreference("credentials_key");
Preference deleteAccountPref = findPreference("delete_account_key");
Preference opmlPref = findPreference("opml_import_export");
Preference notificationPref = findPreference("notifications");
if (account.is(AccountType.LOCAL))
credentialsPref.setVisible(false);
@ -131,13 +135,21 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
.show();
return true;
});
notificationPref.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(getContext(), NotificationPermissionActivity.class);
intent.putExtra(ACCOUNT_ID, account.getId());
startActivity(intent);
return true;
});
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
viewModel = new ViewModelProvider(this).get(AccountViewModel.class);
viewModel.setAccount(account);
}
@ -146,20 +158,25 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
.title(R.string.delete_account_question)
.positiveText(R.string.validate)
.negativeText(R.string.cancel)
.onPositive(((dialog, which) -> viewModel.delete(account)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
getActivity().finish();
}
.onPositive(((dialog, which) -> {
SharedPreferencesManager.remove(getContext(), account.getLoginKey());
SharedPreferencesManager.remove(getContext(), account.getPasswordKey());
@Override
public void onError(Throwable e) {
Utils.showSnackbar(getView(), e.getMessage());
}
})))
viewModel.delete(account)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
getActivity().finish();
}
@Override
public void onError(Throwable e) {
Utils.showSnackbar(getView(), e.getMessage());
}
});
}))
.show();
}

View File

@ -2,16 +2,25 @@ package com.readrops.app.fragments.settings;
import android.content.Intent;
import android.os.Bundle;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.readrops.app.R;
import com.readrops.app.database.Database;
import com.readrops.app.utils.SyncWorker;
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
import com.readrops.db.Database;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
@ -24,6 +33,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
Preference feedsColorsPreference = findPreference("reload_feeds_colors");
Preference themePreference = findPreference("dark_theme");
Preference synchroPreference = findPreference("auto_synchro");
AtomicBoolean serviceStarted = new AtomicBoolean(false);
feedsColorsPreference.setOnPreferenceClickListener(preference -> {
@ -53,6 +64,70 @@ public class SettingsFragment extends PreferenceFragmentCompat {
return true;
});
synchroPreference.setOnPreferenceChangeListener(((preference, newValue) -> {
WorkManager workManager = WorkManager.getInstance(getContext());
Pair<Integer, TimeUnit> interval = getWorkerInterval((String) newValue);
if (interval != null) {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(SyncWorker.class, interval.first, interval.second)
.addTag(SyncWorker.Companion.getTAG())
.setConstraints(constraints)
.setInitialDelay(interval.first, interval.second)
.build();
workManager.enqueueUniquePeriodicWork(SyncWorker.Companion.getTAG(), ExistingPeriodicWorkPolicy.REPLACE, request);
} else {
workManager.cancelAllWorkByTag(SyncWorker.Companion.getTAG());
}
return true;
}));
}
@Nullable
private Pair<Integer, TimeUnit> getWorkerInterval(String newValue) {
int interval;
TimeUnit timeUnit;
switch (newValue) {
case "0.30":
interval = 30;
timeUnit = TimeUnit.MINUTES;
break;
case "1":
interval = 1;
timeUnit = TimeUnit.HOURS;
break;
case "2":
interval = 2;
timeUnit = TimeUnit.HOURS;
break;
case "3":
interval = 3;
timeUnit = TimeUnit.HOURS;
break;
case "6":
interval = 6;
timeUnit = TimeUnit.HOURS;
break;
case "12":
interval = 12;
timeUnit = TimeUnit.HOURS;
break;
case "24":
interval = 1;
timeUnit = TimeUnit.DAYS;
break;
default:
return null;
}
return new Pair<>(interval, timeUnit);
}
}

View File

@ -1,21 +1,22 @@
package com.readrops.app.repositories;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.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.ParsingResult;
import com.readrops.app.utils.feedscolors.FeedColorsKt;
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
import com.readrops.db.Database;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import com.readrops.db.entities.account.AccountType;
import com.readrops.api.services.SyncResult;
import java.util.ArrayList;
import java.util.List;
@ -30,15 +31,17 @@ import static com.readrops.app.utils.ReadropsKeys.FEEDS;
public abstract class ARepository<T> {
protected Application application;
protected Context context;
protected Database database;
protected Account account;
protected T api;
protected ARepository(@NonNull Application application, @Nullable Account account) {
this.application = application;
this.database = Database.getInstance(application);
protected SyncResult syncResult;
protected ARepository(@NonNull Context context, @Nullable Account account) {
this.context = context;
this.database = Database.getInstance(context);
this.account = account;
api = createAPI();
@ -46,6 +49,7 @@ public abstract class ARepository<T> {
protected abstract T createAPI();
// TODO : replace Single by Completable
public abstract Single<Boolean> login(Account account, boolean insert);
public abstract Observable<Feed> sync(List<Feed> feeds);
@ -106,7 +110,11 @@ 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);
return setItemReadState(item.getId(), read, !item.isReadChanged());
}
public Completable setItemReadState(int itemId, boolean read, boolean readChanged) {
return database.itemDao().setReadState(itemId, read, readChanged);
}
public Completable setAllItemsReadState(boolean read) {
@ -157,26 +165,30 @@ public abstract class ARepository<T> {
}
protected void setFeedsColors(List<Feed> feeds) {
Intent intent = new Intent(application, FeedsColorsIntentService.class);
Intent intent = new Intent(context, FeedsColorsIntentService.class);
intent.putParcelableArrayListExtra(FEEDS, new ArrayList<>(feeds));
application.startService(intent);
context.startService(intent);
}
public static ARepository repositoryFactory(Account account, AccountType accountType, Application application) throws Exception {
public static ARepository repositoryFactory(Account account, AccountType accountType, Context context) throws Exception {
switch (accountType) {
case LOCAL:
return new LocalFeedRepository(application, account);
return new LocalFeedRepository(context, account);
case NEXTCLOUD_NEWS:
return new NextNewsRepository(application, account);
return new NextNewsRepository(context, account);
case FRESHRSS:
return new FreshRSSRepository(application, account);
return new FreshRSSRepository(context, account);
default:
throw new Exception("account type not supported");
}
}
public static ARepository repositoryFactory(Account account, Application application) throws Exception {
return ARepository.repositoryFactory(account, account.getAccountType(), application);
public static ARepository repositoryFactory(Account account, Context context) throws Exception {
return ARepository.repositoryFactory(account, account.getAccountType(), context);
}
public SyncResult getSyncResult() {
return syncResult;
}
}

View File

@ -1,28 +1,26 @@
package com.readrops.app.repositories;
import android.app.Application;
import android.content.Context;
import android.util.Log;
import android.util.TimingLogger;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.app.database.entities.Feed;
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.ParsingResult;
import com.readrops.app.utils.Utils;
import com.readrops.app.utils.matchers.FeedMatcher;
import com.readrops.app.utils.matchers.ItemMatcher;
import com.readrops.readropslibrary.services.SyncType;
import com.readrops.readropslibrary.services.freshrss.FreshRSSAPI;
import com.readrops.readropslibrary.services.freshrss.FreshRSSCredentials;
import com.readrops.readropslibrary.services.freshrss.FreshRSSSyncData;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSFeed;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSFolder;
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSItem;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import com.readrops.api.services.Credentials;
import com.readrops.api.services.SyncType;
import com.readrops.api.services.freshrss.FreshRSSAPI;
import com.readrops.api.services.freshrss.FreshRSSCredentials;
import com.readrops.api.services.freshrss.FreshRSSSyncData;
import org.joda.time.DateTime;
import java.util.ArrayList;
import java.util.Collections;
@ -36,14 +34,14 @@ 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);
public FreshRSSRepository(@NonNull Context context, @Nullable Account account) {
super(context, account);
}
@Override
protected FreshRSSAPI createAPI() {
if (account != null)
return new FreshRSSAPI(account.toCredentials());
return new FreshRSSAPI(Credentials.toCredentials(account));
return null;
}
@ -51,9 +49,9 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
@Override
public Single<Boolean> login(Account account, boolean insert) {
if (api == null)
api = new FreshRSSAPI(account.toCredentials());
api = new FreshRSSAPI(Credentials.toCredentials(account));
else
api.setCredentials(account.toCredentials());
api.setCredentials(Credentials.toCredentials(account));
return api.login(account.getLogin(), account.getPassword())
.flatMap(token -> {
@ -94,6 +92,7 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
} else
syncType = SyncType.INITIAL_SYNC;
long newLastModified = DateTime.now().getMillis() / 1000L;
TimingLogger logger = new TimingLogger(TAG, "FreshRSS sync timer");
return Single.<FreshRSSSyncData>create(emitter -> {
@ -113,12 +112,15 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
insertItems(syncResult.getItems(), syncType == SyncType.INITIAL_SYNC);
logger.addSplit("items insertion");
account.setLastModified(syncResult.getLastUpdated());
database.accountDao().updateLastModified(account.getId(), syncResult.getLastUpdated());
account.setLastModified(newLastModified);
database.accountDao().updateLastModified(account.getId(), newLastModified);
database.itemDao().resetReadChanges(account.getId());
logger.addSplit("reset read changes");
logger.dumpToLog();
this.syncResult = syncResult;
return Observable.empty();
});
}
@ -190,14 +192,12 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
.andThen(super.deleteFolder(folder));
}
private void insertFeeds(List<FreshRSSFeed> freshRSSFeeds) {
List<Feed> feeds = new ArrayList<>();
for (FreshRSSFeed freshRSSFeed : freshRSSFeeds) {
feeds.add(FeedMatcher.freshRSSFeedToFeed(freshRSSFeed, account));
private void insertFeeds(List<Feed> freshRSSFeeds) {
for (Feed feed : freshRSSFeeds) {
feed.setAccountId(account.getId());
}
List<Long> insertedFeedsIds = database.feedDao().feedsUpsert(feeds, account);
List<Long> insertedFeedsIds = database.feedDao().feedsUpsert(freshRSSFeeds, account);
if (!insertedFeedsIds.isEmpty()) {
setFeedsColors(database.feedDao().selectFromIdList(insertedFeedsIds));
@ -205,42 +205,28 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
}
private void insertFolders(List<FreshRSSFolder> freshRSSFolders) {
List<Folder> folders = new ArrayList<>();
for (FreshRSSFolder freshRSSFolder : freshRSSFolders) {
if (freshRSSFolder.getType() != null && freshRSSFolder.getType().equals("folder")) {
String id = freshRSSFolder.getId().replace("user/-/label/", "");
Folder folder = new Folder(id);
folder.setRemoteId(freshRSSFolder.getId());
folder.setAccountId(account.getId());
folders.add(folder);
}
private void insertFolders(List<Folder> freshRSSFolders) {
for (Folder folder : freshRSSFolders) {
folder.setAccountId(account.getId());
}
database.folderDao().foldersUpsert(folders, account);
database.folderDao().foldersUpsert(freshRSSFolders, account);
}
private void insertItems(List<FreshRSSItem> items, boolean initialSync) {
List<Item> newItems = new ArrayList<>();
private void insertItems(List<Item> items, boolean initialSync) {
for (Item item : items) {
int feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId());
for (FreshRSSItem freshRSSItem : items) {
int feedId = database.feedDao().getFeedIdByRemoteId(String.valueOf(freshRSSItem.getOrigin().getStreamId()), account.getId());
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(freshRSSItem.getId(), feedId)) {
database.itemDao().setReadState(freshRSSItem.getId(), freshRSSItem.isRead());
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(item.getRemoteId(), feedId)) {
database.itemDao().setReadState(item.getRemoteId(), item.isRead());
continue;
}
Item item = ItemMatcher.freshRSSItemtoItem(freshRSSItem, feedId);
item.setFeedId(feedId);
item.setReadTime(Utils.readTimeFromString(item.getContent()));
newItems.add(item);
}
Collections.sort(newItems, Item::compareTo);
database.itemDao().insert(newItems);
Collections.sort(items, Item::compareTo);
database.itemDao().insert(items);
}
}

View File

@ -1,30 +1,31 @@
package com.readrops.app.repositories;
import android.accounts.NetworkErrorException;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.matchers.FeedMatcher;
import com.readrops.app.utils.HtmlParser;
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;
import com.readrops.readropslibrary.localfeed.AFeed;
import com.readrops.readropslibrary.localfeed.RSSQuery;
import com.readrops.readropslibrary.localfeed.RSSQueryResult;
import com.readrops.readropslibrary.localfeed.atom.ATOMFeed;
import com.readrops.readropslibrary.localfeed.json.JSONFeed;
import com.readrops.readropslibrary.localfeed.rss.RSSFeed;
import com.readrops.readropslibrary.utils.LibUtils;
import com.readrops.readropslibrary.utils.ParseException;
import com.readrops.readropslibrary.utils.UnknownFormatException;
import com.readrops.app.utils.matchers.FeedMatcher;
import com.readrops.app.utils.matchers.ItemMatcher;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import com.readrops.api.localfeed.AFeed;
import com.readrops.api.localfeed.RSSQuery;
import com.readrops.api.localfeed.RSSQueryResult;
import com.readrops.api.localfeed.atom.ATOMFeed;
import com.readrops.api.localfeed.json.JSONFeed;
import com.readrops.api.localfeed.rss.RSSFeed;
import com.readrops.api.services.SyncResult;
import com.readrops.api.utils.LibUtils;
import com.readrops.api.utils.ParseException;
import com.readrops.api.utils.UnknownFormatException;
import org.jsoup.Jsoup;
@ -42,8 +43,10 @@ public class LocalFeedRepository extends ARepository<Void> {
private static final String TAG = LocalFeedRepository.class.getSimpleName();
public LocalFeedRepository(@NonNull Application application, @Nullable Account account) {
super(application, account);
public LocalFeedRepository(@NonNull Context context, @Nullable Account account) {
super(context, account);
syncResult = new SyncResult();
}
@Override
@ -173,7 +176,7 @@ public class LocalFeedRepository extends ARepository<Void> {
database.feedDao().updateHeaders(dbFeed.getEtag(), dbFeed.getLastModified(), dbFeed.getId());
Collections.sort(items, Item::compareTo);
int maxItems = Integer.parseInt(SharedPreferencesManager.readString(application, SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB));
int maxItems = Integer.parseInt(SharedPreferencesManager.readString(context, SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB));
if (maxItems > 0 && items.size() > maxItems)
items = items.subList(items.size() - maxItems, items.size());
@ -247,6 +250,7 @@ public class LocalFeedRepository extends ARepository<Void> {
}
}
syncResult.getItems().addAll(itemsToInsert);
database.itemDao().insert(itemsToInsert);
}

View File

@ -1,33 +1,26 @@
package com.readrops.app.repositories;
import android.app.Application;
import android.content.Context;
import android.database.sqlite.SQLiteConstraintException;
import android.util.TimingLogger;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.app.database.entities.Feed;
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.api.services.Credentials;
import com.readrops.api.services.SyncResult;
import com.readrops.api.services.SyncType;
import com.readrops.api.services.nextcloudnews.NextNewsAPI;
import com.readrops.api.services.nextcloudnews.NextNewsSyncData;
import com.readrops.api.services.nextcloudnews.json.NextNewsUser;
import com.readrops.api.utils.UnknownFormatException;
import com.readrops.app.utils.FeedInsertionResult;
import com.readrops.app.utils.ParsingResult;
import com.readrops.app.utils.Utils;
import com.readrops.app.utils.matchers.FeedMatcher;
import com.readrops.app.utils.matchers.ItemMatcher;
import com.readrops.readropslibrary.services.SyncType;
import com.readrops.readropslibrary.services.nextcloudnews.NextNewsAPI;
import com.readrops.readropslibrary.services.nextcloudnews.NextNewsSyncData;
import com.readrops.readropslibrary.services.nextcloudnews.NextNewsSyncResult;
import com.readrops.readropslibrary.services.nextcloudnews.json.NextNewsFeed;
import com.readrops.readropslibrary.services.nextcloudnews.json.NextNewsFeeds;
import com.readrops.readropslibrary.services.nextcloudnews.json.NextNewsFolder;
import com.readrops.readropslibrary.services.nextcloudnews.json.NextNewsFolders;
import com.readrops.readropslibrary.services.nextcloudnews.json.NextNewsItem;
import com.readrops.readropslibrary.services.nextcloudnews.json.NextNewsRenameFeed;
import com.readrops.readropslibrary.services.nextcloudnews.json.NextNewsUser;
import com.readrops.readropslibrary.utils.UnknownFormatException;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.Item;
import com.readrops.db.entities.account.Account;
import org.joda.time.LocalDateTime;
@ -44,14 +37,14 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
private static final String TAG = NextNewsRepository.class.getSimpleName();
public NextNewsRepository(@NonNull Application application, @Nullable Account account) {
super(application, account);
public NextNewsRepository(@NonNull Context context, @Nullable Account account) {
super(context, account);
}
@Override
protected NextNewsAPI createAPI() {
if (account != null)
return new NextNewsAPI(account.toCredentials());
return new NextNewsAPI(Credentials.toCredentials(account));
return null;
}
@ -60,29 +53,30 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
public Single<Boolean> login(Account account, boolean insert) {
return Single.<NextNewsUser>create(emitter -> {
if (api == null)
api = new NextNewsAPI(account.toCredentials());
api = new NextNewsAPI(Credentials.toCredentials(account));
else
api.setCredentials(account.toCredentials());
api.setCredentials(Credentials.toCredentials(account));
NextNewsUser user = api.login();
emitter.onSuccess(user);
}).flatMap(user -> {
if (user != null) {
account.setDisplayedName(user.getDisplayName());
account.setCurrentAccount(true);
emitter.onSuccess(user);
} else {
emitter.onError(new Exception("Login failed. Please check your credentials and your Nextcloud News setup."));
}
}).flatMap(user -> {
account.setDisplayedName(user.getDisplayName());
account.setCurrentAccount(true);
if (insert) {
return database.accountDao().insert(account)
.flatMap(id -> {
account.setId(id.intValue());
return Single.just(true);
});
}
if (insert) {
return database.accountDao().insert(account)
.flatMap(id -> {
account.setId(id.intValue());
return Single.just(true);
});
}
return Single.just(true);
} else
return Single.just(false);
return Single.just(true);
});
}
@ -107,18 +101,19 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
}
TimingLogger timings = new TimingLogger(TAG, "nextcloud news " + syncType.name().toLowerCase());
NextNewsSyncResult syncResult = api.sync(syncType, syncData);
SyncResult result = api.sync(syncType, syncData);
timings.addSplit("server queries");
if (!syncResult.isError()) {
if (!result.isError()) {
syncResult = new SyncResult();
insertFolders(syncResult.getFolders());
insertFolders(result.getFolders());
timings.addSplit("insert folders");
insertFeeds(syncResult.getFeeds(), false);
insertFeeds(result.getFeeds(), false);
timings.addSplit("insert feeds");
insertItems(syncResult.getItems(), syncType == SyncType.INITIAL_SYNC);
insertItems(result.getItems(), syncType == SyncType.INITIAL_SYNC);
timings.addSplit("insert items");
timings.dumpToLog();
@ -146,10 +141,10 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
FeedInsertionResult insertionResult = new FeedInsertionResult();
try {
NextNewsFeeds nextNewsFeeds = api.createFeed(result.getUrl(), 0);
List<Feed> nextNewsFeeds = api.createFeed(result.getUrl(), 0);
if (nextNewsFeeds != null) {
List<Feed> newFeeds = insertFeeds(nextNewsFeeds.getFeeds(), true);
List<Feed> newFeeds = insertFeeds(nextNewsFeeds, true);
// there is always only one object in the list, see nextcloud news api doc
insertionResult.setFeed(newFeeds.get(0));
} else
@ -178,16 +173,14 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
public Completable updateFeed(Feed feed) {
return Completable.create(emitter -> {
Folder folder = feed.getFolderId() == null ? null : database.folderDao().select(feed.getFolderId());
NextNewsRenameFeed newsRenameFeed = new NextNewsRenameFeed(Integer.parseInt(feed.getRemoteId()), feed.getName());
NextNewsFeed newsFeed;
if (folder != null)
newsFeed = new NextNewsFeed(Integer.parseInt(feed.getRemoteId()), Integer.parseInt(folder.getRemoteId()));
feed.setRemoteFolderId(folder.getRemoteId());
else
newsFeed = new NextNewsFeed(Integer.parseInt(feed.getRemoteId()), 0); // 0 for no folder
feed.setRemoteFolderId(String.valueOf(0)); // 0 for no folder
try {
if (api.renameFeed(newsRenameFeed) && api.changeFeedFolder(newsFeed)) {
if (api.renameFeed(feed) && api.changeFeedFolder(feed)) {
emitter.onComplete();
} else
emitter.onError(new Exception("Unknown error when updating feed"));
@ -217,14 +210,12 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
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()));
List<Folder> folders = api.createFolder(folder);
if (folders != null) {
NextNewsFolder nextNewsFolder = folders.getFolders().get(0); // always only one item returned by the server, see doc
Folder nextNewsFolder = folders.get(0); // always only one item returned by the server, see doc
folder.setRemoteId(nextNewsFolder.getRemoteId());
folder.setName(nextNewsFolder.getName());
folder.setRemoteId(String.valueOf(nextNewsFolder.getId()));
emitter.onSuccess(folder);
} else
emitter.onError(new Exception("Unknown error"));
@ -238,7 +229,7 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
public Completable updateFolder(Folder folder) {
return Completable.create(emitter -> {
try {
if (api.renameFolder(new NextNewsFolder(Integer.parseInt(folder.getRemoteId()), folder.getName()))) {
if (api.renameFolder(folder)) {
emitter.onComplete();
} else
emitter.onError(new Exception("Unknown error"));
@ -255,7 +246,7 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
public Completable deleteFolder(Folder folder) {
return Completable.create(emitter -> {
try {
if (api.deleteFolder(new NextNewsFolder(Integer.parseInt(folder.getRemoteId()), folder.getName()))) {
if (api.deleteFolder(folder)) {
emitter.onComplete();
} else
emitter.onError(new Exception("Unknown error"));
@ -268,18 +259,16 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
}).andThen(super.deleteFolder(folder));
}
private List<Feed> insertFeeds(List<NextNewsFeed> nextNewsFeeds, boolean newFeeds) {
List<Feed> feeds = new ArrayList<>();
for (NextNewsFeed nextNewsFeed : nextNewsFeeds) {
feeds.add(FeedMatcher.nextNewsFeedToFeed(nextNewsFeed, account));
private List<Feed> insertFeeds(List<Feed> nextNewsFeeds, boolean newFeeds) {
for (Feed nextNewsFeed : nextNewsFeeds) {
nextNewsFeed.setAccountId(account.getId());
}
List<Long> insertedFeedsIds;
if (newFeeds) {
insertedFeedsIds = database.feedDao().insert(feeds);
insertedFeedsIds = database.feedDao().insert(nextNewsFeeds);
} else {
insertedFeedsIds = database.feedDao().feedsUpsert(feeds, account);
insertedFeedsIds = database.feedDao().feedsUpsert(nextNewsFeeds, account);
}
List<Feed> insertedFeeds = new ArrayList<>();
@ -291,40 +280,37 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
return insertedFeeds;
}
private void insertFolders(List<NextNewsFolder> nextNewsFolders) {
List<Folder> folders = new ArrayList<>();
for (NextNewsFolder nextNewsFolder : nextNewsFolders) {
Folder folder = new Folder(nextNewsFolder.getName());
private void insertFolders(List<Folder> nextNewsFolders) {
for (Folder folder : nextNewsFolders) {
folder.setAccountId(account.getId());
folder.setRemoteId(String.valueOf(nextNewsFolder.getId()));
folders.add(folder);
}
database.folderDao().foldersUpsert(folders, account);
database.folderDao().foldersUpsert(nextNewsFolders, account);
}
private void insertItems(List<NextNewsItem> items, boolean initialSync) {
List<Item> newItems = new ArrayList<>();
private void insertItems(List<Item> items, boolean initialSync) {
List<Item> itemsToInsert = new ArrayList<>();
for (NextNewsItem nextNewsItem : items) {
int feedId = database.feedDao().getFeedIdByRemoteId(String.valueOf(nextNewsItem.getFeedId()), account.getId());
for (Item item : items) {
int feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId());
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(String.valueOf(nextNewsItem.getId()), feedId)) {
database.itemDao().setReadState(String.valueOf(nextNewsItem.getId()), !nextNewsItem.isUnread());
//if the item already exists, update only its read state
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(String.valueOf(item.getRemoteId()), feedId)) {
database.itemDao().setReadState(item.getRemoteId(), item.isRead());
continue;
}
Item item = ItemMatcher.nextNewsItemToItem(nextNewsItem, feedId);
item.setFeedId(feedId);
item.setReadTime(Utils.readTimeFromString(item.getContent()));
newItems.add(item);
itemsToInsert.add(item);
}
if (!newItems.isEmpty()) {
Collections.sort(newItems, Item::compareTo);
database.itemDao().insert(newItems);
if (!itemsToInsert.isEmpty()) {
syncResult.setItems(itemsToInsert);
Collections.sort(itemsToInsert, Item::compareTo);
database.itemDao().insert(itemsToInsert);
}
}
}

View File

@ -15,12 +15,16 @@ public final class DateUtils {
* Fri, 04 Jan 2019 22:21:46 GMT
* Fri, 04 Jan 2019 22:21:46 +0000
*/
private static final String RSS_2_BASE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss ";
private static final String RSS_2_BASE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss";
private static final String GMT_PATTERN = "ZZZ";
private static final String OFFSET_PATTERN = "Z";
private static final String ISO_PATTERN = ".SSSZZ";
private static final String EDT_PATTERN = "zzz";
/**
* Date pattern for format : 2019-01-04T22:21:46+00:00
*/
@ -28,10 +32,13 @@ public final class DateUtils {
public static LocalDateTime stringToLocalDateTime(String value) {
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).getParser())
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).getParser())
.toFormatter()
.withLocale(Locale.ENGLISH)
.withOffsetParsed();

View File

@ -26,9 +26,9 @@ import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
import com.readrops.app.R;
import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.entities.account.Account;
import com.readrops.db.entities.Feed;
import com.readrops.db.entities.Folder;
import com.readrops.db.entities.account.Account;
import java.util.ArrayList;
import java.util.HashMap;
@ -64,8 +64,8 @@ public class DrawerManager {
this.headerListener = headerListener;
}
public Drawer buildDrawer(List<Account> accounts) {
createAccountHeader(accounts);
public Drawer buildDrawer(List<Account> accounts, int currentAccountId) {
createAccountHeader(accounts, currentAccountId);
drawer = new DrawerBuilder()
.withActivity(activity)
@ -129,14 +129,14 @@ public class DrawerManager {
}
}
private void createAccountHeader(List<Account> accounts) {
private void createAccountHeader(List<Account> accounts, int currentAccountId) {
ProfileDrawerItem[] profileItems = new ProfileDrawerItem[accounts.size()];
int currentAccountId = 1;
for (int i = 0; i < accounts.size(); i++) {
Account account = accounts.get(i);
if (account.isCurrentAccount())
// if currentAccount > 0, it means that the current account is no longer
if (account.isCurrentAccount() && currentAccountId == 0)
currentAccountId = account.getId();
ProfileDrawerItem profileItem = createProfileItem(account);
@ -156,6 +156,7 @@ public class DrawerManager {
.build();
addProfileSettingItems();
header.setActiveProfile(currentAccountId);
}
@ -250,6 +251,10 @@ public class DrawerManager {
header.setActiveProfile(profileItem.getIdentifier());
}
public void setAccount(int accountId) {
header.setActiveProfile(accountId);
}
public void updateHeader(List<Account> accounts) {
header.clear();
addProfileSettingItems();

View File

@ -2,26 +2,22 @@ package com.readrops.app.utils
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.TextView
import com.readrops.app.R
import com.readrops.app.databinding.EmptyListViewBinding
/**
* A simple custom view to display a empty list message
*/
class EmptyListView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
init {
// no binding here, it makes the view rendering fail
View.inflate(context, R.layout.empty_list_view, this)
val imageView: ImageView = findViewById(R.id.empty_list_image)
val textView: TextView = findViewById(R.id.empty_list_text_v)
val binding: EmptyListViewBinding = EmptyListViewBinding.inflate(LayoutInflater.from(context), this, true)
init {
val attributes = context.obtainStyledAttributes(attrs, R.styleable.EmptyListView)
imageView.setImageDrawable(attributes.getDrawable(R.styleable.EmptyListView_image))
textView.text = attributes.getString(R.styleable.EmptyListView_text)
binding.emptyListImage.setImageDrawable(attributes.getDrawable(R.styleable.EmptyListView_image))
binding.emptyListText.text = attributes.getString(R.styleable.EmptyListView_text)
attributes.recycle()
}

View File

@ -10,7 +10,7 @@ import androidx.annotation.StringRes;
import com.mikepenz.fastadapter.FastAdapter;
import com.mikepenz.fastadapter.items.AbstractItem;
import com.readrops.app.R;
import com.readrops.app.database.entities.Feed;
import com.readrops.db.entities.Feed;
import org.jetbrains.annotations.NotNull;

View File

@ -5,9 +5,9 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readrops.readropslibrary.utils.LibUtils;
import com.readrops.api.utils.HttpManager;
import com.readrops.api.utils.LibUtils;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
@ -18,6 +18,9 @@ import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import okhttp3.Request;
import okhttp3.Response;
public final class HtmlParser {
private static final String TAG = HtmlParser.class.getSimpleName();
@ -81,7 +84,7 @@ public final class HtmlParser {
}
@Nullable
public static String getFaviconLink(@NonNull String url) throws IOException {
public static String getFaviconLink(@NonNull String url) {
String favUrl = null;
String head = getHTMLHeadFromUrl(url);
@ -102,20 +105,28 @@ public final class HtmlParser {
}
@Nullable
private static String getHTMLHeadFromUrl(@NonNull String url) throws IOException {
private static String getHTMLHeadFromUrl(@NonNull String url) {
long start = System.currentTimeMillis();
Connection.Response response = Jsoup.connect(url).execute();
if (response.contentType().contains(LibUtils.HTML_CONTENT_TYPE)) {
String body = response.body();
String head = body.substring(body.indexOf("<head"), body.indexOf("</head>"));
try {
Response response = HttpManager.getInstance().getOkHttpClient()
.newCall(new Request.Builder().url(url).build()).execute();
long end = System.currentTimeMillis();
Log.d(TAG, "parsing time : " + (end - start));
if (response.header("Content-Type").contains(LibUtils.HTML_CONTENT_TYPE)) {
String body = response.body().string();
String head = body.substring(body.indexOf("<head"), body.indexOf("</head>"));
return head;
} else
long end = System.currentTimeMillis();
Log.d(TAG, "parsing time : " + (end - start));
return head;
} else {
return null;
}
} catch (Exception e) {
return null;
}
}
public static String getDescImageLink(String description, String url) {

View File

@ -9,7 +9,7 @@ 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 com.readrops.db.entities.Feed;
import org.jetbrains.annotations.NotNull;

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