mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 06:10:31 +05:30
Merge pull request #889 from Bnyro/push
Push notifcations for new streams
This commit is contained in:
commit
285a37bcd5
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
14
app/src/main/res/drawable/ic_notification.xml
Normal file
14
app/src/main/res/drawable/ic_notification.xml
Normal 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>
|
@ -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>
|
@ -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>
|
25
app/src/main/res/xml/notification_settings.xml
Normal file
25
app/src/main/res/xml/notification_settings.xml
Normal 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>
|
@ -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"
|
||||
|
@ -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" }
|
||||
|
Loading…
Reference in New Issue
Block a user