Merge pull request #889 from Bnyro/push

Push notifcations for new streams
This commit is contained in:
Bnyro 2022-07-29 08:49:47 +02:00 committed by GitHub
commit 285a37bcd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 335 additions and 26 deletions

View File

@ -68,6 +68,7 @@ dependencies {
implementation libs.androidx.navigation.fragment
implementation libs.androidx.navigation.ui
implementation libs.androidx.preference
implementation libs.androidx.work.runtime
androidTestImplementation libs.androidx.test.junit
androidTestImplementation libs.androidx.test.espressoCore

View File

@ -7,6 +7,9 @@ import android.os.Build
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.NotificationHelper
import com.github.libretube.util.RetrofitInstance
class MyApp : Application() {
override fun onCreate() {
@ -27,6 +30,34 @@ class MyApp : Application() {
*/
val builder = VmPolicy.Builder()
StrictMode.setVmPolicy(builder.build())
/**
* set the api and the auth api url
*/
setRetrofitApiUrls()
/**
* initialize the notification listener in the background
*/
NotificationHelper.enqueueWork(this)
}
/**
* set the api urls needed for the [RetrofitInstance]
*/
private fun setRetrofitApiUrls() {
RetrofitInstance.url =
PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, PIPED_API_URL)
// set auth instance
RetrofitInstance.authUrl =
if (PreferenceHelper.getBoolean(PreferenceKeys.AUTH_INSTANCE_TOGGLE, false)) {
PreferenceHelper.getString(
PreferenceKeys.AUTH_INSTANCE,
PIPED_API_URL
)
} else {
RetrofitInstance.url
}
}
/**
@ -36,7 +67,7 @@ class MyApp : Application() {
createNotificationChannel(
"download_service",
"Download Service",
"DownloadService",
"Shows a notification when downloading media.",
NotificationManager.IMPORTANCE_NONE
)
createNotificationChannel(
@ -45,6 +76,12 @@ class MyApp : Application() {
"Shows a notification with buttons to control the audio player",
NotificationManager.IMPORTANCE_LOW
)
createNotificationChannel(
"notification_worker",
"Notification Worker",
"Shows a notification when new streams are available.",
NotificationManager.IMPORTANCE_DEFAULT
)
}
private fun createNotificationChannel(

View File

@ -23,7 +23,6 @@ import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
import coil.ImageLoader
import com.github.libretube.Globals
import com.github.libretube.PIPED_API_URL
import com.github.libretube.R
import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.fragments.PlayerFragment
@ -33,7 +32,6 @@ import com.github.libretube.services.ClosingService
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.CronetHelper
import com.github.libretube.util.LocaleHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.ThemeHelper
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.navigation.NavigationBarView
@ -70,19 +68,6 @@ class MainActivity : AppCompatActivity() {
.callFactory(CronetHelper.callFactory)
.build()
RetrofitInstance.url =
PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, PIPED_API_URL)!!
// set auth instance
RetrofitInstance.authUrl =
if (PreferenceHelper.getBoolean(PreferenceKeys.AUTH_INSTANCE_TOGGLE, false)) {
PreferenceHelper.getString(
PreferenceKeys.AUTH_INSTANCE,
PIPED_API_URL
)!!
} else {
RetrofitInstance.url
}
// save whether the data saver mode is enabled
Globals.DATA_SAVER_MODE_ENABLED = PreferenceHelper.getBoolean(
PreferenceKeys.DATA_SAVER_MODE,

View File

@ -158,6 +158,8 @@ class SubscriptionsFragment : Fragment() {
binding.subRefresh.isRefreshing = false
}
if (response.isNotEmpty()) {
// save the last recent video to the prefs for the notification worker
PreferenceHelper.setLatestVideoId(response[0].url.toString().replace("/watch?v=", ""))
channelRecView.adapter = SubscriptionChannelAdapter(response.toMutableList())
} else {
Toast.makeText(context, R.string.subscribeIsEmpty, Toast.LENGTH_SHORT).show()

View File

@ -62,6 +62,13 @@ class MainSettings : PreferenceFragmentCompat() {
true
}
val notifications = findPreference<Preference>("notifications")
notifications?.setOnPreferenceClickListener {
val newFragment = NotificationSettings()
navigateToSettingsFragment(newFragment)
true
}
val advanced = findPreference<Preference>("advanced")
advanced?.setOnPreferenceClickListener {
val newFragment = AdvancedSettings()

View File

@ -0,0 +1,32 @@
package com.github.libretube.preferences
import android.os.Bundle
import androidx.preference.ListPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
import com.github.libretube.util.NotificationHelper
class NotificationSettings : PreferenceFragmentCompat() {
val TAG = "SettingsFragment"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.notification_settings, rootKey)
val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.notifications))
val notificationsEnabled = findPreference<SwitchPreferenceCompat>(PreferenceKeys.NOTIFICATION_ENABLED)
notificationsEnabled?.setOnPreferenceChangeListener { _, _ ->
NotificationHelper.enqueueWork(requireContext())
true
}
val checkingFrequency = findPreference<ListPreference>(PreferenceKeys.CHECKING_FREQUENCY)
checkingFrequency?.setOnPreferenceChangeListener { _, _ ->
NotificationHelper.enqueueWork(requireContext())
true
}
}
}

View File

@ -15,6 +15,7 @@ object PreferenceHelper {
private lateinit var prefContext: Context
private lateinit var settings: SharedPreferences
private lateinit var editor: SharedPreferences.Editor
val mapper = ObjectMapper()
/**
* set the context that is being used to access the shared preferences
@ -62,8 +63,6 @@ object PreferenceHelper {
}
fun saveCustomInstance(customInstance: CustomInstance) {
val mapper = ObjectMapper()
val customInstancesList = getCustomInstances()
customInstancesList += customInstance
@ -72,8 +71,6 @@ object PreferenceHelper {
}
fun getCustomInstances(): ArrayList<CustomInstance> {
val mapper = ObjectMapper()
val json: String = settings.getString("customInstances", "")!!
val type = mapper.typeFactory.constructCollectionType(
List::class.java,
@ -163,8 +160,6 @@ object PreferenceHelper {
}
fun getWatchHistory(): ArrayList<WatchHistoryItem> {
val mapper = ObjectMapper()
val json: String = settings.getString("watch_history", "")!!
val type = mapper.typeFactory.constructCollectionType(
List::class.java,
@ -191,7 +186,6 @@ object PreferenceHelper {
watchPositions += watchPositionItem
val mapper = ObjectMapper()
val json = mapper.writeValueAsString(watchPositions)
editor.putString("watch_positions", json).commit()
}
@ -206,14 +200,11 @@ object PreferenceHelper {
if (indexToRemove != null) watchPositions.removeAt(indexToRemove!!)
val mapper = ObjectMapper()
val json = mapper.writeValueAsString(watchPositions)
editor.putString("watch_positions", json).commit()
}
fun getWatchPositions(): ArrayList<WatchPosition> {
val mapper = ObjectMapper()
val json: String = settings.getString("watch_positions", "")!!
val type = mapper.typeFactory.constructCollectionType(
List::class.java,
@ -227,6 +218,14 @@ object PreferenceHelper {
}
}
fun setLatestVideoId(videoId: String) {
setString(PreferenceKeys.LAST_STREAM_VIDEO_ID, videoId)
}
fun getLatestVideoId(): String {
return getString(PreferenceKeys.LAST_STREAM_VIDEO_ID, "")
}
private fun getDefaultSharedPreferences(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}

View File

@ -65,6 +65,13 @@ object PreferenceKeys {
const val DOWNLOAD_LOCATION = "download_location"
const val DOWNLOAD_FOLDER = "download_folder"
/**
* Notifications
*/
const val NOTIFICATION_ENABLED = "notification_toggle"
const val CHECKING_FREQUENCY = "checking_frequency"
const val LAST_STREAM_VIDEO_ID = "last_stream_video_id"
/**
* Advanced
*/

View File

@ -0,0 +1,148 @@
package com.github.libretube.util
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import com.github.libretube.R
import com.github.libretube.activities.MainActivity
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit
object NotificationHelper {
fun enqueueWork(
context: Context
) {
// get the notification preferences
PreferenceHelper.setContext(context)
val notificationsEnabled = PreferenceHelper.getBoolean(
PreferenceKeys.NOTIFICATION_ENABLED,
true
)
val checkingFrequency = PreferenceHelper.getString(
PreferenceKeys.CHECKING_FREQUENCY,
"60"
).toLong()
val uniqueWorkName = "NotificationService"
if (notificationsEnabled) {
// requirements for the work
// here: network needed to run the task
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// create the worker
val notificationWorker = PeriodicWorkRequest.Builder(
NotificationWorker::class.java,
checkingFrequency,
TimeUnit.MINUTES
)
.setConstraints(constraints)
.build()
// enqueue the task
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
uniqueWorkName,
ExistingPeriodicWorkPolicy.REPLACE,
notificationWorker
)
} else {
WorkManager.getInstance(context)
.cancelUniqueWork(uniqueWorkName)
}
}
/**
* check whether new streams are available in subscriptions
*/
fun checkForNewStreams(context: Context) {
val token = PreferenceHelper.getToken()
if (token == "") return
runBlocking {
val task = async {
RetrofitInstance.authApi.getFeed(token)
}
// fetch the users feed
val videoFeed = try {
task.await()
} catch (e: Exception) {
return@runBlocking
}
val lastSeenStreamId = PreferenceHelper.getLatestVideoId()
val latestFeedStreamId = videoFeed[0].url?.replace("/watch?v=", "")
// first time notifications enabled
if (lastSeenStreamId == "") PreferenceHelper.setLatestVideoId(lastSeenStreamId)
else if (lastSeenStreamId != latestFeedStreamId) {
// get the index of the last user-seen video
var newStreamIndex = -1
videoFeed.forEachIndexed { index, stream ->
if (stream.url?.replace("/watch?v=", "") == lastSeenStreamId) {
newStreamIndex = index
}
}
if (newStreamIndex == -1) return@runBlocking
val (title, description) = when (newStreamIndex) {
// only one new stream available
1 -> {
Pair(videoFeed[0].title, videoFeed[0].uploaderName)
}
else -> {
Pair(
// return the amount of new streams as title
context.getString(
R.string.new_streams_count,
newStreamIndex.toString()
),
// return the first few uploader as description
context.getString(
R.string.new_streams_by,
videoFeed[0].uploaderName + ", " + videoFeed[1].uploaderName + ", " + videoFeed[2].uploaderName
)
)
}
}
// save the id of the last recent video for the next time it's running
PreferenceHelper.setLatestVideoId(videoFeed[0].url?.replace("/watch?v=", "")!!)
createNotification(context, title!!, description!!)
}
}
}
/**
* Notification that is created when new streams are found
*/
fun createNotification(context: Context, title: String, description: String) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(context, "notification_worker")
.setContentTitle(title)
.setSmallIcon(R.drawable.ic_bell)
.setContentText(description)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// Set the intent that will fire when the user taps the notification
.setContentIntent(pendingIntent)
.setAutoCancel(true)
with(NotificationManagerCompat.from(context)) {
// notificationId is a unique int for each notification that you must define
notify(2, builder.build())
}
}
}

View File

@ -0,0 +1,20 @@
package com.github.libretube.util
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
/**
* The notification worker which checks for new streams in a certain frequency
*/
class NotificationWorker(appContext: Context, parameters: WorkerParameters) : Worker(appContext, parameters) {
private val TAG = "NotificationWorker"
override fun doWork(): Result {
// schedule the next task of the worker
NotificationHelper.enqueueWork(applicationContext)
// check whether there are new streams and notify if there are some
NotificationHelper.checkForNewStreams(applicationContext)
return Result.success()
}
}

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="22dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="18"
android:viewportHeight="20"
tools:ignore="VectorRaster">
<path
android:fillColor="#E6E1E5"
android:fillType="evenOdd"
android:pathData="M8.625,0H9.375C9.5821,0 9.75,0.1679 9.75,0.375C9.75,0.5821 9.9179,0.75 10.125,0.75C10.3321,0.75 10.5,0.9179 10.5,1.125V1.875C10.5,2.4963 11.0088,3.0566 11.5685,3.3265C12.4353,3.7446 13.5132,4.5489 14.0889,5.6503C14.2067,5.8758 14.25,6.1307 14.25,6.3852V12.0512C14.25,12.9156 14.8488,13.6151 15.5614,14.1044C15.8681,14.315 16.102,14.5525 16.3642,14.8462C16.4522,14.9448 16.5,15.0728 16.5,15.2049C16.5,15.506 16.256,15.75 15.955,15.75H2.051C1.7467,15.75 1.5,15.5033 1.5,15.199C1.5,15.0704 1.5445,14.9452 1.6287,14.848C1.8916,14.5448 2.1396,14.3062 2.4619,14.0946C3.1737,13.6273 3.75,12.9293 3.75,12.0778V6.3852C3.75,6.1307 3.7933,5.8758 3.9111,5.6503C4.4868,4.5489 5.5648,3.7446 6.4316,3.3265C6.9912,3.0566 7.5,2.4963 7.5,1.875V1.125C7.5,0.9179 7.6679,0.75 7.875,0.75C8.0821,0.75 8.25,0.5821 8.25,0.375C8.25,0.1679 8.4179,0 8.625,0ZM9,19.4998C7.7574,19.4998 6.7501,18.4925 6.75,17.2499H11.25C11.2499,18.4925 10.2426,19.4998 9,19.4998ZM0.0558,7.5132C-0.1246,6.2857 0.1356,5.034 0.7902,3.9801C1.4447,2.9262 2.4513,2.1382 3.6315,1.7557L3.9552,2.7546C3.0111,3.0605 2.2058,3.691 1.6821,4.5341C1.1585,5.3772 0.9503,6.3786 1.0946,7.3605L0.0558,7.5132ZM17.1808,3.934C16.5153,2.8869 15.5006,2.1094 14.3165,1.7392L14.0032,2.7414C14.9504,3.0375 15.7623,3.6596 16.2946,4.4972C16.827,5.3348 17.0455,6.334 16.9115,7.3174L17.9519,7.4592C18.1194,6.2299 17.8463,4.981 17.1808,3.934Z"
tools:ignore="VectorPath" />
</vector>

View File

@ -760,4 +760,22 @@
<item>worst</item>
</string-array>
<string-array name="checkingFrequency">
<item>15 minutes</item>
<item>30 minutes</item>
<item>60 minutes</item>
<item>2 hours</item>
<item>6 hours</item>
<item>12 hours</item>
</string-array>
<string-array name="checkingFrequencyValues">
<item>15</item>
<item>30</item>
<item>60</item>
<item>120</item>
<item>360</item>
<item>720</item>
</string-array>
</resources>

View File

@ -265,6 +265,12 @@
<string name="best_quality">Best quality</string>
<string name="worst_quality">Worst quality</string>
<string name="default_subtitle_language">Default subtitle language</string>
<string name="notifications">Notifications</string>
<string name="notify_new_streams">New streams notifications</string>
<string name="notify_new_streams_summary">Notify when new streams from subscriptions are available</string>
<string name="checking_frequency">Checking frequency</string>
<string name="new_streams_count">%1$s new streams are available</string>
<string name="new_streams_by">New streams by %1$s …</string>
<string name="irreversible">Are you sure? This can\'t be undone!</string>
<string name="history_empty">History is empty.</string>
</resources>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/notifications">
<SwitchPreferenceCompat
android:icon="@drawable/ic_notification"
app:defaultValue="true"
app:key="notification_toggle"
app:title="@string/notify_new_streams"
android:summary="@string/notify_new_streams_summary"/>
<ListPreference
android:icon="@drawable/ic_time"
app:defaultValue="60"
app:entries="@array/checkingFrequency"
app:entryValues="@array/checkingFrequencyValues"
app:key="checking_frequency"
app:title="@string/checking_frequency"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -40,6 +40,12 @@
app:key="history"
app:title="@string/history" />
<Preference
android:icon="@drawable/ic_notification"
android:summary="@string/notify_new_streams"
app:key="notifications"
app:title="@string/notifications" />
<Preference
android:icon="@drawable/ic_list"
app:key="advanced"

View File

@ -7,6 +7,7 @@ legacySupport = "1.0.0"
preference = "1.2.0"
extJunit = "1.1.3"
espresso = "3.4.0"
workRuntime = "2.7.1"
circleimageview = "3.1.0"
exoplayer = "2.17.1"
multidex = "2.0.1"
@ -29,6 +30,7 @@ androidx-legacySupport = { group = "androidx.legacy", name = "legacy-support-v4"
androidx-preference = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference" }
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "extJunit" }
androidx-test-espressoCore = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
androidx-work-runtime = { group = "androidx.work", name="work-runtime-ktx", version.ref="workRuntime" }
circleimageview = { group = "de.hdodenhof", name = "circleimageview", version.ref = "circleimageview" }
exoplayer = { group = "com.google.android.exoplayer", name = "exoplayer", version.ref = "exoplayer" }
exoplayer-extension-mediasession = { group = "com.google.android.exoplayer", name = "extension-mediasession", version.ref = "exoplayer" }