Commit a5f43c30 authored by Christophe Henry's avatar Christophe Henry

Add refresh feature

parent 8bcbd87e
......@@ -42,20 +42,24 @@ dependencies {
}
implementation fileTree(dir: "libs", include: ["*.jar"])
// Kotlin stuff
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// Compat libraries
implementation "com.android.support:appcompat-v7:$android_support_version"
implementation "com.android.support:preference-v7:$android_support_version"
implementation "com.android.support:support-core-utils:$android_support_version"
implementation "com.android.support:support-fragment:$android_support_version"
implementation "com.android.support:support-compat:28.0.0"
// AndroidX layout
implementation "androidx.appcompat:appcompat:1.0.0-beta01"
implementation "androidx.core:core-ktx:1.1.0-alpha03"
implementation "com.google.android.material:material:1.0.0-beta01"
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.recyclerview:recyclerview:1.0.0"
testImplementation "junit:junit:4.12"
androidTestImplementation "androidx.test:runner:$test_runnner_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
......@@ -63,8 +67,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"
testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
// Room
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
......@@ -79,5 +82,11 @@ dependencies {
implementation "nl.komponents.kovenant:kovenant:$promise_version"
implementation "nl.komponents.kovenant:kovenant-android:$promise_version"
// Utils
implementation "org.apache.commons:commons-text:1.4"
// Tests
testImplementation "junit:junit:4.12"
androidTestImplementation "androidx.test:runner:$test_runnner_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
}
package fr.chenry.android.freshrss
import android.app.Application
import android.app.NotificationManager
import androidx.core.app.NotificationManagerCompat
import fr.chenry.android.freshrss.store.Store
import nl.komponents.kovenant.android.startKovenant
import nl.komponents.kovenant.android.stopKovenant
......@@ -9,6 +11,9 @@ import java.util.*
class FreshRSSApplication: Application() {
override fun onCreate() {
super.onCreate()
// Stupid hack because Android is still not able to provide the current application application globally
// even though it is effectively a singleton...
FreshRSSApplication.application = this
startKovenant()
Store.debugMode = try {
val properties = Properties()
......@@ -21,4 +26,12 @@ class FreshRSSApplication: Application() {
super.onTerminate()
stopKovenant()
}
companion object {
lateinit var application: FreshRSSApplication
private set
val notificationManager: NotificationManagerCompat
get() = NotificationManagerCompat.from(FreshRSSApplication.application)
}
}
\ No newline at end of file
......@@ -17,12 +17,7 @@ class MainActivity: AppCompatActivity() {
private val deferred = deferred<Unit, Throwable>()
init {
Store.getSubscriptions()
.bind {Store.getUnreadCount()}
.bind {all(Store.subscriptions.keys.map {Store.getStreamContents(it)}, cancelOthersOnError = false)}
.bind {Store.getUnreadItems()}
.successUi {deferred.resolve()}
.failUi(deferred::reject)
Store.refresh().successUi {deferred.resolve()}.failUi(deferred::reject)
}
override fun onCreate(savedInstanceState: Bundle?) {
......
......@@ -37,7 +37,7 @@ class SubscriptionArticlesDetailFragment: Fragment() {
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.actionbar, menu)
inflater?.inflate(R.menu.article_actionbar, menu)
menu?.findItem(R.id.action_share)
.let {MenuItemCompat.getActionProvider(it) as ShareActionProvider}
.setShareIntent(
......
......@@ -9,6 +9,7 @@ import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.dao.common.Subscription
import fr.chenry.android.freshrss.store.dao.common.Subscriptions
import fr.chenry.android.freshrss.utils.getOrDefault
import kotlinx.android.synthetic.main.fragment_main_subscription.*
import kotlin.reflect.KClass
......@@ -21,6 +22,11 @@ class MainSubscriptionFragment: Fragment() {
else -> TODO("Handle bad section")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_main_subscription, container, false)
......@@ -55,6 +61,15 @@ class MainSubscriptionFragment: Fragment() {
true
}
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.main_actionbar, menu)
menu?.findItem(R.id.action_refresh)?.setOnMenuItemClickListener {
Store.refresh().getOrDefault(null).let {true}
}
super.onCreateOptionsMenu(menu, inflater)
}
}
abstract class SubscriptionsFragment: Fragment() {
......
package fr.chenry.android.freshrss.store
import androidx.lifecycle.MutableLiveData
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.R.string
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection
import fr.chenry.android.freshrss.store.api.Api
import fr.chenry.android.freshrss.store.dao.common.StreamId
......@@ -8,10 +10,12 @@ import fr.chenry.android.freshrss.store.dao.common.Subscription
import fr.chenry.android.freshrss.store.dao.store.Article
import fr.chenry.android.freshrss.store.dao.store.ItemId
import fr.chenry.android.freshrss.store.databindingsupport.GenericLiveData
import fr.chenry.android.freshrss.utils.NotificationHelper
import fr.chenry.android.freshrss.utils.NotificationHelper.NotificationChanels.REFRESH
import fr.chenry.android.freshrss.utils.e
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.*
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.then
import nl.komponents.kovenant.ui.*
object Store {
var debugMode = false
......@@ -22,11 +26,30 @@ object Store {
val subscriptionsSection = MutableLiveData<SubscriptionSection>().apply {this.value = SubscriptionSection.ALL}
val tags = MutableLiveData<List<String>>().apply {this.value = listOf()}
val favorites = GenericLiveData<String, Subscription>(mutableMapOf())
private val refreshingPromise = MutableLiveData<Promise<Unit, Exception>>()
private var lastFetchTimestamp = 0L
fun login(instance: String, user: String, password: String): Promise<Unit, Exception> =
api.login(instance, user, password) bind {getToken()}
fun refresh(): Promise<Unit, Exception> {
if(refreshingPromise.value != null) return refreshingPromise.value!!
val notificationId = NotificationHelper.post(REFRESH, R.string.notification_refresh_title)
val promise = getSubscriptions()
.bind {getUnreadCount()}
.bind {all(subscriptions.keys.map {getStreamContents(it)}, cancelOthersOnError = false)}
.bind {getUnreadItems()}
refreshingPromise.postValue(promise)
promise.always {refreshingPromise.postValue(null)}
.successUi {NotificationHelper.cancel(notificationId)}
.failUi {NotificationHelper.update(REFRESH, notificationId, string.notification_refresh_failed_title)}
return promise
}
fun getSubscriptions(): Promise<Unit, Exception> =
api.getSubscriptions() then {subscriptions.addAll(it.map {self -> self.id to self})}
......
......@@ -71,7 +71,7 @@ class Api {
return Fuel
.getJson(endpoints.streamItemsEndpoint, listOf("s" to id))
.authorize(this.authTokens)
.promiseString() successUi {this.e(it)}
.promiseString() successUi {this.e("::getStreamItems: TODO")}
}
fun getStreamContents(id: String): Promise<ContentItemsHandler, Exception> {
......
......@@ -8,7 +8,6 @@ class UnreadArticlesViewModel: ArticlesViewModel() {
override val articles: Articles get() = liveData.value ?: listOf()
override fun load(): Articles {
this.e(this::class.qualifiedName!!)
val subscription = Store.subscriptions[streamId.value!!]
if(subscription == null) {
this.w("Unable to find unread articles for stream id ${streamId.value}")
......
package fr.chenry.android.freshrss.utils
import android.app.*
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.R
object NotificationHelper {
fun post(channel: NotificationChanels, contentTitleId: Int, notificationId: Int = channel.channelId): Int {
NotificationManagerCompat.from(FreshRSSApplication.application)
.notify(notificationId, getNotification(channel, contentTitleId))
return notificationId
}
fun update(channel: NotificationChanels, notificationId: Int, contentTitleId: Int) =
FreshRSSApplication
.notificationManager
.notify(notificationId,
getNotification(channel, contentTitleId))
private fun getNotification(channel: NotificationChanels, contentTitleId: Int): Notification =
NotificationCompat
.Builder(FreshRSSApplication.application, channel.channelTitle)
.setSmallIcon(R.drawable.ic_rss_feed_black_24dp)
.setContentTitle(FreshRSSApplication.application.resources.getString(contentTitleId))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build()
fun cancel(id: Int) = NotificationManagerCompat.from(FreshRSSApplication.application).cancel(id)
private fun createNotificationChannel(channelNameId: Int, channelDescription: Int): Pair<Int, String> {
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) return 0 to ""
return try {
val name = FreshRSSApplication.application.resources.getString(channelNameId)
val descriptionText = FreshRSSApplication.application.resources.getString(channelDescription)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channelId = FreshRSSApplication.application.resources.getString(R.string.notification_channel_refresh)
val channel = NotificationChannel(channelId, name, importance).apply {description = descriptionText}
// Register the channel with the system
val notificationManager =
FreshRSSApplication.application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
channelNameId to channelId
} catch(_: Throwable) {
0 to "DEFAULT"
}
}
enum class NotificationChanels(channel: Pair<Int, String>) {
REFRESH(
createNotificationChannel(
R.string.notification_refresh_title,
R.string.notification_refresh_description
)
);
val channelId = channel.first
val channelTitle = channel.second
}
}
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>
......@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:fillColor="#FFF"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>
......@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:fillColor="#FFF"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_refresh"
android:orderInCategory="100"
android:icon="@drawable/ic_refresh_white_24dp"
android:title="@string/refresh"
app:showAsAction="always" />
</menu>
\ No newline at end of file
......@@ -32,4 +32,11 @@
<string name="next">Next</string>
<string name="error_invalid_password">This password is too short</string>
<string name="original_page">Original page</string>
<string name="notification_refresh_title">Refreshing your RSS feeds</string>
<!-- Do not translate -->
<string name="notification_channel_refresh">REFRESH</string>
<string name="notification_refresh_description">FreshRSS is fetching your content from the server</string>
<string name="notification_refresh_failed_title">FresshRSS failed to retreive content from your FreshRSS server</string>
<string name="refresh">Refresh</string>
</resources>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment