Merge branch 'develop'
This commit is contained in:
commit
70d0515ffb
.github/workflows
.gitignore.travis.ymlCHANGELOG.mdREADME.mdapi
.gitignorebuild.gradleproguard-rules.pro
src
androidTest/java/com/readrops/api
main
AndroidManifest.xml
java/com/readrops/api
localfeed
opml
services
API.javaCredentials.javaSyncResult.ktSyncType.java
freshrss
nextcloudnews
utils
res/values
test/java/com/readrops/api
app
build.gradleproguard-rules.pro
src
androidTest/java/com/readrops/app
debug
main
AndroidManifest.xml
java/com/readrops/app
ReadropsApp.java
activities
AccountTypeListActivity.javaAddAccountActivity.javaAddFeedActivity.javaItemActivity.javaMainActivity.javaManageFeedsFoldersActivity.javaNotificationPermissionActivity.ktSettingsActivity.javaSplashActivity.javaWebViewActivity.kt
adapters
AccountArrayAdapter.javaAccountTypeListAdapter.javaFeedsAdapter.javaFoldersAdapter.javaMainItemListAdapter.javaNotificationPermissionListAdapter.kt
database
fragments
EditFeedDialogFragment.javaFeedOptionsDialogFragment.ktFeedsFragment.javaFolderOptionsDialogFragment.ktFoldersFragment.java
settings
repositories
utils
31
.github/workflows/android.yml
vendored
Normal file
31
.github/workflows/android.yml
vendored
Normal 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
6
.gitignore
vendored
@ -125,3 +125,9 @@ Temporary Items
|
||||
|
||||
|
||||
\.idea/
|
||||
|
||||
fastlane/Appfile
|
||||
|
||||
fastlane/Fastfile
|
||||
|
||||
Gemfile
|
||||
|
16
.travis.yml
16
.travis.yml
@ -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"
|
@ -52,4 +52,4 @@ Fix a crash related to Proguard Rules.
|
||||
- Multiple accounts
|
||||
- Feeds and folders management (create, update and delete feeds/folders if your service API supports it)
|
||||
- Nextcloud news support
|
||||
- FreshRSS support
|
||||
- FreshRSS support
|
||||
|
27
README.md
27
README.md
@ -1,16 +1,10 @@
|
||||
# Readrops 
|
||||
# Readrops <img src="fastlane/metadata/android/en-US/images/icon.png" width=60>
|
||||
|
||||
[](https://travis-ci.org/readrops/Readrops)
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
<br/>
|
||||

|
||||
[<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`
|
||||
|
0
readropslibrary/.gitignore → api/.gitignore
vendored
0
readropslibrary/.gitignore → api/.gitignore
vendored
69
api/build.gradle
Normal file
69
api/build.gradle
Normal 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'
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
4
api/src/main/AndroidManifest.xml
Normal file
4
api/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.readrops.api">
|
||||
|
||||
</manifest>
|
9
api/src/main/java/com/readrops/api/localfeed/AFeed.kt
Normal file
9
api/src/main/java/com/readrops/api/localfeed/AFeed.kt
Normal 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
|
||||
}
|
@ -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;
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.readropslibrary.localfeed;
|
||||
package com.readrops.api.localfeed;
|
||||
|
||||
public class RSSQueryResult {
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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?)
|
@ -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()
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
@ -67,15 +64,7 @@ public class RSSChannel {
|
||||
public void setLastUpdated(String lastUpdated) {
|
||||
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)
|
@ -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() {
|
@ -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;
|
@ -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;
|
||||
@ -15,11 +15,7 @@ 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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();
|
39
api/src/main/java/com/readrops/api/services/Credentials.java
Normal file
39
api/src/main/java/com/readrops/api/services/Credentials.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
16
api/src/main/java/com/readrops/api/services/SyncResult.kt
Normal file
16
api/src/main/java/com/readrops/api/services/SyncResult.kt
Normal 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.readropslibrary.services;
|
||||
package com.readrops.api.services;
|
||||
|
||||
public enum SyncType {
|
||||
INITIAL_SYNC,
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.readropslibrary.services.freshrss;
|
||||
package com.readrops.api.services.freshrss;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
77
api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt
Normal file
77
api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt
Normal 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")
|
||||
}
|
||||
}
|
61
api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt
Normal file
61
api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt
Normal 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")
|
||||
}
|
||||
}
|
134
api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt
Normal file
134
api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.readropslibrary.services.nextcloudnews;
|
||||
package com.readrops.api.services.nextcloudnews;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
65
api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt
Normal file
65
api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt
Normal 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")
|
||||
}
|
||||
}
|
50
api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt
Normal file
50
api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt
Normal 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")
|
||||
}
|
||||
}
|
70
api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt
Normal file
70
api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt
Normal 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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.readropslibrary.utils;
|
||||
package com.readrops.api.utils;
|
||||
|
||||
public class ConflictException extends Exception {
|
||||
|
@ -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();
|
@ -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()
|
@ -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");
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.readropslibrary.utils;
|
||||
package com.readrops.api.utils;
|
||||
|
||||
public class ParseException extends Exception {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.readropslibrary.utils;
|
||||
package com.readrops.api.utils;
|
||||
|
||||
public class UnknownFormatException extends Exception {
|
||||
|
3
api/src/main/res/values/strings.xml
Normal file
3
api/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Readrops Library</string>
|
||||
</resources>
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.readropslibrary;
|
||||
package com.readrops.api;
|
||||
|
||||
import org.junit.Test;
|
||||
|
@ -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'
|
||||
}
|
||||
|
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
@ -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.** { *; }
|
@ -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()
|
||||
}
|
||||
}
|
19
app/src/debug/AndroidManifest.xml
Normal file
19
app/src/debug/AndroidManifest.xml
Normal 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>
|
85
app/src/debug/java/com/readrops/app/ReadropsDebugApp.java
Normal file
85
app/src/debug/java/com/readrops/app/ReadropsDebugApp.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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);*/
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user