diff --git a/app/build.gradle b/app/build.gradle index 3dbb1a7a7..f999bb61a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/com/github/libretube/MyApp.kt b/app/src/main/java/com/github/libretube/MyApp.kt index 1baa1f2dc..52d9919b9 100644 --- a/app/src/main/java/com/github/libretube/MyApp.kt +++ b/app/src/main/java/com/github/libretube/MyApp.kt @@ -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( diff --git a/app/src/main/java/com/github/libretube/activities/MainActivity.kt b/app/src/main/java/com/github/libretube/activities/MainActivity.kt index f22d35ba5..0a07f5699 100644 --- a/app/src/main/java/com/github/libretube/activities/MainActivity.kt +++ b/app/src/main/java/com/github/libretube/activities/MainActivity.kt @@ -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, diff --git a/app/src/main/java/com/github/libretube/fragments/SubscriptionsFragment.kt b/app/src/main/java/com/github/libretube/fragments/SubscriptionsFragment.kt index 731c150c7..467703d1c 100644 --- a/app/src/main/java/com/github/libretube/fragments/SubscriptionsFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/SubscriptionsFragment.kt @@ -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() diff --git a/app/src/main/java/com/github/libretube/preferences/MainSettings.kt b/app/src/main/java/com/github/libretube/preferences/MainSettings.kt index b21b41bd4..3622e3ee7 100644 --- a/app/src/main/java/com/github/libretube/preferences/MainSettings.kt +++ b/app/src/main/java/com/github/libretube/preferences/MainSettings.kt @@ -62,6 +62,13 @@ class MainSettings : PreferenceFragmentCompat() { true } + val notifications = findPreference("notifications") + notifications?.setOnPreferenceClickListener { + val newFragment = NotificationSettings() + navigateToSettingsFragment(newFragment) + true + } + val advanced = findPreference("advanced") advanced?.setOnPreferenceClickListener { val newFragment = AdvancedSettings() diff --git a/app/src/main/java/com/github/libretube/preferences/NotificationSettings.kt b/app/src/main/java/com/github/libretube/preferences/NotificationSettings.kt new file mode 100644 index 000000000..d90e4f71e --- /dev/null +++ b/app/src/main/java/com/github/libretube/preferences/NotificationSettings.kt @@ -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(PreferenceKeys.NOTIFICATION_ENABLED) + notificationsEnabled?.setOnPreferenceChangeListener { _, _ -> + NotificationHelper.enqueueWork(requireContext()) + true + } + + val checkingFrequency = findPreference(PreferenceKeys.CHECKING_FREQUENCY) + checkingFrequency?.setOnPreferenceChangeListener { _, _ -> + NotificationHelper.enqueueWork(requireContext()) + true + } + } +} diff --git a/app/src/main/java/com/github/libretube/preferences/PreferenceHelper.kt b/app/src/main/java/com/github/libretube/preferences/PreferenceHelper.kt index d2c038768..ecd3025e1 100644 --- a/app/src/main/java/com/github/libretube/preferences/PreferenceHelper.kt +++ b/app/src/main/java/com/github/libretube/preferences/PreferenceHelper.kt @@ -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 { - 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 { - 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 { - 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) } diff --git a/app/src/main/java/com/github/libretube/preferences/PreferenceKeys.kt b/app/src/main/java/com/github/libretube/preferences/PreferenceKeys.kt index 30954e1ce..2dd315501 100644 --- a/app/src/main/java/com/github/libretube/preferences/PreferenceKeys.kt +++ b/app/src/main/java/com/github/libretube/preferences/PreferenceKeys.kt @@ -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 */ diff --git a/app/src/main/java/com/github/libretube/util/NotificationHelper.kt b/app/src/main/java/com/github/libretube/util/NotificationHelper.kt new file mode 100644 index 000000000..0d196af74 --- /dev/null +++ b/app/src/main/java/com/github/libretube/util/NotificationHelper.kt @@ -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()) + } + } +} diff --git a/app/src/main/java/com/github/libretube/util/NotificationWorker.kt b/app/src/main/java/com/github/libretube/util/NotificationWorker.kt new file mode 100644 index 000000000..711aecb4e --- /dev/null +++ b/app/src/main/java/com/github/libretube/util/NotificationWorker.kt @@ -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() + } +} diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 000000000..5abed7187 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index e28789a05..56cfb59a0 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -760,4 +760,22 @@ worst + + 15 minutes + 30 minutes + 60 minutes + 2 hours + 6 hours + 12 hours + + + + 15 + 30 + 60 + 120 + 360 + 720 + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee9a0bd40..16783b4b8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -265,6 +265,12 @@ Best quality Worst quality Default subtitle language + Notifications + New streams notifications + Notify when new streams from subscriptions are available + Checking frequency + %1$s new streams are available + New streams by %1$s … Are you sure? This can\'t be undone! History is empty. \ No newline at end of file diff --git a/app/src/main/res/xml/notification_settings.xml b/app/src/main/res/xml/notification_settings.xml new file mode 100644 index 000000000..9bd0e37f6 --- /dev/null +++ b/app/src/main/res/xml/notification_settings.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index f9bfcaea8..0d79b3805 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -40,6 +40,12 @@ app:key="history" app:title="@string/history" /> + +