From f139f281370805a034bb00f4304dd54ee96bc372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20D=C3=BCzg=C3=B6ren?= Date: Sat, 31 Aug 2019 16:12:36 +0200 Subject: [PATCH] Code optimisations, using ArrayList#filter for performance improvements and code reductions, fixed notification channel not having been created before opening ringtone settings --- app/build.gradle | 4 +- .../denizd/substitutionplan/DataFetcher.kt | 283 +++++++++--------- .../substitutionplan/FBNotificationService.kt | 4 +- .../substitutionplan/GeneralPlanFragment.kt | 4 +- .../com/denizd/substitutionplan/MiscData.kt | 49 ++- .../substitutionplan/PersonalPlanFragment.kt | 33 +- .../denizd/substitutionplan/PlanFragment.kt | 9 +- .../substitutionplan/SettingsFragment.kt | 1 + app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 10 files changed, 206 insertions(+), 185 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 96ae638..30f7525 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.denizd.substitutionplan" minSdkVersion 21 targetSdkVersion 28 - versionCode 29 - versionName "2.2.6" + versionCode 30 + versionName "2.2.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/app/src/main/java/com/denizd/substitutionplan/DataFetcher.kt b/app/src/main/java/com/denizd/substitutionplan/DataFetcher.kt index f3541c8..2021024 100644 --- a/app/src/main/java/com/denizd/substitutionplan/DataFetcher.kt +++ b/app/src/main/java/com/denizd/substitutionplan/DataFetcher.kt @@ -3,7 +3,6 @@ package com.denizd.substitutionplan import android.app.* import android.content.Context import android.content.Intent -import android.graphics.Color import android.media.RingtoneManager import android.net.Uri import android.os.AsyncTask @@ -18,22 +17,24 @@ import com.google.android.material.snackbar.Snackbar import org.jsoup.Jsoup import kotlin.collections.ArrayList -class DataFetcher(isplan: Boolean, ismenu: Boolean, isjobservice: Boolean, context: Context, application: Application, parentview: View?) : AsyncTask() { +/** + * This class gets data from the djd4rkn355.github.io domain asynchronously and persists it in the + * database. By using booleans as parameters, network usage and speed can be improved as only + * requested data will be downloaded and processed. + * + * @param isPlan substitution plan will be downloaded and persisted in the database if true + * @param isMenu food menu will be downloaded and persisted in the database if true + * @param isJobService a notification will be prepared and sent if this and isPlan is true + */ +class DataFetcher(isPlan: Boolean, isMenu: Boolean, isJobService: Boolean, context: Context, application: Application, parentView: View?) : AsyncTask() { - private var jobservice = isjobservice - private var plan = isplan - private var menu = ismenu - - /* booleans to improve speed and decrease network usage when fetching data by only grabbing the required HTML pages - "plan" = true is required to fetch the substitution data - "menu" = true is required to fetch the food menu - "jobservice" = true is required to send a notification - "jobservice" = true has no effect unless "plan" is also true - */ + private var jobService = isJobService + private var plan = isPlan + private var menu = isMenu private var mContext = context private var mApplication = application - private var mView = parentview + private var mView = parentView private var priority = 200 private var notifText = "" private var informational = "" @@ -46,146 +47,21 @@ class DataFetcher(isplan: Boolean, ismenu: Boolean, isjobservice: Boolean, conte override fun doInBackground(vararg params: Void?): Void? { try { - if (prefs.getBoolean("testUrls", false)) { - substUrl = "https://djd4rkn355.github.io/subst_test.html" - foodUrl = "https://djd4rkn355.github.io/food_test.html" - } - if (menu) { - val docFood = Jsoup.connect(foodUrl).get() - currentFoodTime = docFood.select("h1")[0].text() - if (currentFoodTime != prefs.getString("timeFoodNew", "")) { - val foodRepository = FoodRepository(mApplication) - val foodElements = docFood.select("th") - foodRepository.deleteAll() - - val indices = ArrayList() - val daysAndVon = arrayOf("Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "von") - for (i in 0 until foodElements.size) { - if (checkStringForArray(foodElements[i].text(), daysAndVon)) indices.add(i) - } - indices.add(foodElements.size) - - for ((priority, l) in (0 until indices.size - 1).withIndex()) { - var s = "" - for (i2 in indices[l] until indices[l + 1]) { - if (s.isEmpty()) { - s = foodElements[i2].text() - } else { - s += "\n${foodElements[i2].text()}" - } - } - foodRepository.insert(Food(s, priority)) - } - edit.putString("timeFoodNew", currentFoodTime).apply() - } - } - - if (plan) { - val doc = Jsoup.connect(substUrl).get() - currentTime = doc.select("h1")[0].text() - if (currentTime != prefs.getString("timeNew", "")) { - val substRepo = SubstRepository(mApplication) - val rows = doc.select("tr") - val paragraphs = doc.select("p") - - val coursePreference = prefs.getString("courses", "") ?: "" - val classPreference = prefs.getString("classes", "") ?: "" - - for (i in 0 until paragraphs.size) { - if (i == 0) { - informational = paragraphs[i].text() - } else { - informational += "\n\n" + paragraphs[i].text() - } - } - edit.putString("informational", informational).apply() - - substRepo.deleteAllSubst() - - for (i in 0 until rows.size) { - val row = rows[i] - val cols = row.select("th") - - val group = cols[0].text() - val course = cols[3].text() - val additional = cols[5].text() - substRepo.insert(Subst(group = group, date = cols[1].text(), time = cols[2].text(), course = course, - room = cols[4].text(), additional = additional, priority = priority)) - priority-- - - if (jobservice && prefs.getBoolean("notif", true)) { - if (coursePreference.isEmpty() && classPreference.isNotEmpty()) { - if (group.isNotEmpty() && group != "") { - if (classPreference.contains(group) || group.contains(classPreference)) { - - notifText += "${if (notifText.isNotEmpty()) ",\n" else ""}$course: ${if (additional.isNotEmpty()) additional else "---"}" - } - } - } else if (classPreference.isNotEmpty() && coursePreference.isNotEmpty()) { - if (group != "" && course != "") { - if (coursePreference.contains(course)) { - if (classPreference.contains(group) || group.contains(classPreference)) { - - notifText += "${if (notifText.isNotEmpty()) ",\n" else ""}$course: ${if (additional.isNotEmpty()) additional else "---"}" - } - } - } - } - } - } - - if (notifText.isNotEmpty()) { - - val openApp = Intent(mContext, Main::class.java) - openApp.flags = Intent.FLAG_ACTIVITY_NEW_TASK - openApp.flags += Intent.FLAG_ACTIVITY_CLEAR_TASK - val openAppPending = PendingIntent.getActivity(mContext, 0, openApp, 0) - - val notificationLayout = RemoteViews(mContext.packageName, R.layout.notification) - notificationLayout.setTextViewText(R.id.notification_title, mContext.getString(R.string.substitutionPlan)) - notificationLayout.setTextViewText(R.id.notification_textview, notifText) - - val manager = mContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val channelId = "general" - val channelName = mContext.getString(R.string.general) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT) - channel.enableLights(true) - channel.lightColor = Color.BLUE - manager.createNotificationChannel(channel) - } - - val notification = NotificationCompat.Builder(mContext, channelId) - .setStyle(NotificationCompat.DecoratedCustomViewStyle()) - .setCustomContentView(notificationLayout) - .setSmallIcon(R.drawable.ic_avh) - .setContentIntent(openAppPending) - .setAutoCancel(true) - .setColor(ContextCompat.getColor(mContext, R.color.colorAccent)) - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - val sound = if ((prefs.getString("ringtoneUri", "") ?: "").isNotEmpty()) { - Uri.parse(prefs.getString("ringtoneUri", "") ?: "") - } else { - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - } - notification.setSound(sound) - } - - manager.notify(1, notification.build()) - } - - edit.putString("timeNew", currentTime).apply() + when { + prefs.getBoolean("testUrls", false) -> { + substUrl = "https://djd4rkn355.github.io/subst_test.html" + foodUrl = "https://djd4rkn355.github.io/food_test.html" } + menu -> requestFoodMenuData() + plan -> requestSubstPlanAndNotification() } if (plan || menu) { mView?.let { v: View -> - val snackText = mContext.getText(R.string.lastUpdated).toString() + when { + val snackText = "${mContext.getText(R.string.lastUpdated)} ${when { menu -> currentFoodTime else -> currentTime - } + }}" val snackBarView = v.findViewById(R.id.coordination) Snackbar.make(snackBarView, snackText, Snackbar.LENGTH_LONG).show() } @@ -207,6 +83,121 @@ class DataFetcher(isplan: Boolean, ismenu: Boolean, isjobservice: Boolean, conte } } + private fun requestFoodMenuData() { + val docFood = Jsoup.connect(foodUrl).get() + currentFoodTime = docFood.select("h1")[0].text() + if (currentFoodTime != prefs.getString("timeFoodNew", "")) { + val foodRepository = FoodRepository(mApplication) + val foodElements = docFood.select("th") + foodRepository.deleteAll() + + val indices = ArrayList() + val daysAndVon = arrayOf("Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "von") + for (i in 0 until foodElements.size) { + if (checkStringForArray(foodElements[i].text(), daysAndVon)) indices.add(i) + } + indices.add(foodElements.size) + + for ((priority, l) in (0 until indices.size - 1).withIndex()) { + var s = "" + for (i2 in indices[l] until indices[l + 1]) { + if (s.isEmpty()) { + s = foodElements[i2].text() + } else { + s += "\n${foodElements[i2].text()}" + } + } + foodRepository.insert(Food(s, priority)) + } + edit.putString("timeFoodNew", currentFoodTime).apply() + } + } + + private fun requestSubstPlanAndNotification() { + val doc = Jsoup.connect(substUrl).get() + currentTime = doc.select("h1")[0].text() + if (currentTime != prefs.getString("timeNew", "")) { + val substRepo = SubstRepository(mApplication) + val rows = doc.select("tr") + val paragraphs = doc.select("p") + val substArray = ArrayList() + + val coursePreference = prefs.getString("courses", "") ?: "" + val classPreference = prefs.getString("classes", "") ?: "" + + for (i in 0 until paragraphs.size) { + if (i == 0) { + informational = paragraphs[i].text() + } else { + informational += "\n\n" + paragraphs[i].text() + } + } + edit.putString("informational", informational).apply() + + substRepo.deleteAllSubst() + + for (i in 0 until rows.size) { + val row = rows[i] + val cols = row.select("th") + + val group = cols[0].text() + val course = cols[3].text() + val additional = cols[5].text() + val subst = Subst(group = group, date = cols[1].text(), time = cols[2].text(), course = course, + room = cols[4].text(), additional = additional, priority = priority) + substArray.add(subst) + substRepo.insert(subst) + priority-- + } + + if (jobService && prefs.getBoolean("notif", true)) { + substArray.filter { MiscData.checkPersonalSubstitutions(it, coursePreference, classPreference, false) }.forEach { substItem -> + notifText += "${if (notifText.isNotEmpty()) ",\n" else ""}$substItem.course: ${if (substItem.additional.isNotEmpty()) substItem.additional else "---"}" + } + } + + if (notifText.isNotEmpty()) { + sendNotification() + } + + edit.putString("timeNew", currentTime).apply() + } + } + + private fun sendNotification() { + val openApp = Intent(mContext, Main::class.java) + openApp.flags = Intent.FLAG_ACTIVITY_NEW_TASK + openApp.flags += Intent.FLAG_ACTIVITY_CLEAR_TASK + val openAppPending = PendingIntent.getActivity(mContext, 0, openApp, 0) + + val notificationLayout = RemoteViews(mContext.packageName, R.layout.notification) + notificationLayout.setTextViewText(R.id.notification_title, mContext.getString(R.string.substitutionPlan)) + notificationLayout.setTextViewText(R.id.notification_textview, notifText) + + val manager = mContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + MiscData.getNotificationChannel(mContext, prefs) + + val notification = NotificationCompat.Builder(mContext, MiscData.notificationChannelId) + .setStyle(NotificationCompat.DecoratedCustomViewStyle()) + .setCustomContentView(notificationLayout) + .setSmallIcon(R.drawable.ic_avh) + .setContentIntent(openAppPending) + .setAutoCancel(true) + .setColor(ContextCompat.getColor(mContext, R.color.colorAccent)) + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + val sound = if ((prefs.getString("ringtoneUri", "") ?: "").isNotEmpty()) { + Uri.parse(prefs.getString("ringtoneUri", "") ?: "") + } else { + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + } + notification.setSound(sound) + } + + manager.notify(1, notification.build()) + } + private fun checkStringForArray(s: String, checking: Array): Boolean { for (i in checking.indices) { if (s.contains(checking[i])) { diff --git a/app/src/main/java/com/denizd/substitutionplan/FBNotificationService.kt b/app/src/main/java/com/denizd/substitutionplan/FBNotificationService.kt index fbf6dc6..1d9ed7e 100644 --- a/app/src/main/java/com/denizd/substitutionplan/FBNotificationService.kt +++ b/app/src/main/java/com/denizd/substitutionplan/FBNotificationService.kt @@ -6,7 +6,7 @@ import com.google.firebase.messaging.RemoteMessage class FBNotificationService : FirebaseMessagingService() { override fun onMessageReceived(p0: RemoteMessage) { - DataFetcher(isplan = true, ismenu = true, isjobservice = true, context = applicationContext, - application = application, parentview = null).execute() + DataFetcher(isPlan = true, isMenu = true, isJobService = true, context = applicationContext, + application = application, parentView = null).execute() } } \ No newline at end of file diff --git a/app/src/main/java/com/denizd/substitutionplan/GeneralPlanFragment.kt b/app/src/main/java/com/denizd/substitutionplan/GeneralPlanFragment.kt index 821fcbf..eafee9f 100644 --- a/app/src/main/java/com/denizd/substitutionplan/GeneralPlanFragment.kt +++ b/app/src/main/java/com/denizd/substitutionplan/GeneralPlanFragment.kt @@ -9,8 +9,8 @@ class GeneralPlanFragment : PlanFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - substViewModel.allSubst?.observe(this, Observer> { - mAdapter.setSubst(it) + substViewModel.allSubst?.observe(this, Observer> { substitutions -> + mAdapter.setSubst(substitutions) recyclerView.scheduleLayoutAnimation() }) } diff --git a/app/src/main/java/com/denizd/substitutionplan/MiscData.kt b/app/src/main/java/com/denizd/substitutionplan/MiscData.kt index 785e9ec..c7ab344 100644 --- a/app/src/main/java/com/denizd/substitutionplan/MiscData.kt +++ b/app/src/main/java/com/denizd/substitutionplan/MiscData.kt @@ -1,6 +1,11 @@ package com.denizd.substitutionplan +import android.annotation.TargetApi +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context import android.content.SharedPreferences +import android.graphics.Color import java.util.* object MiscData { @@ -15,11 +20,10 @@ object MiscData { R.color.bgTeal, R.color.bgCyan, R.color.bgBlue, R.color.bgPurple, R.color.bgPink, R.color.bgBrown, R.color.bgGrey, R.color.bgPureWhite, R.color.bgSalmon, R.color.bgTangerine, R.color.bgBanana, R.color.bgFlora, R.color.bgSpindrift, R.color.bgSky, R.color.bgOrchid, R.color.bgLavender, R.color.bgCarnation, R.color.bgBrown2, R.color.bgPureBlack) - - fun emoji(unicode: Int): String { return String(Character.toChars(unicode)) } + const val notificationChannelId = "general" fun getColourForString(name: String): Int { - return when(name) { + return when (name) { "red" -> R.color.bgRed "orange" -> R.color.bgOrange "yellow" -> R.color.bgYellow @@ -95,4 +99,43 @@ object MiscData { } edit.apply() } + + fun checkPersonalSubstitutions(subst: Subst, coursePreference: String, classPreference: String, psa: Boolean): Boolean { + val group = subst.group + val course = subst.course + if (psa && subst.date.isNotEmpty() && subst.date.substring(0, 3) == "psa") { + return true + } + if (coursePreference.isEmpty() && classPreference.isNotEmpty()) { + if (group.isNotEmpty() && group != "") { + if (classPreference.contains(group) || group.contains(classPreference)) { + return true + } + } + } else if (classPreference.isNotEmpty() && coursePreference.isNotEmpty()) { + if (group != "" && course != "") { + if (coursePreference.contains(course)) { + if (classPreference.contains(group) || group.contains(classPreference)) { + return true + } + } + } + } + return false + } + + @TargetApi(26) + fun getNotificationChannel(context: Context, prefs: SharedPreferences): NotificationChannel { + val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + return if (!prefs.getBoolean("notifChannelCreated", false)) { + val channel = NotificationChannel(notificationChannelId, context.getString(R.string.general), NotificationManager.IMPORTANCE_DEFAULT) + channel.enableLights(true) + channel.lightColor = Color.BLUE + manager.createNotificationChannel(channel) + prefs.edit().putBoolean("notifChannelCreated", true).apply() + channel + } else { + manager.getNotificationChannel(notificationChannelId) + } + } } diff --git a/app/src/main/java/com/denizd/substitutionplan/PersonalPlanFragment.kt b/app/src/main/java/com/denizd/substitutionplan/PersonalPlanFragment.kt index a480475..e817ef2 100644 --- a/app/src/main/java/com/denizd/substitutionplan/PersonalPlanFragment.kt +++ b/app/src/main/java/com/denizd/substitutionplan/PersonalPlanFragment.kt @@ -13,41 +13,28 @@ class PersonalPlanFragment : PlanFragment() { smileydowntext = view.findViewById(R.id.smileydowntext) linearsmiley = view.findViewById(R.id.linearsmiley) - substViewModel.allSubst?.observe(this, Observer> { + val coursePreference = prefs.getString("courses", "") ?: "" + val classPreference = prefs.getString("classes", "") ?: "" + + substViewModel.allSubst?.observe(this, Observer> { substitutions -> planCardList.clear() smileydown.visibility = View.GONE smileydowntext.visibility = View.GONE linearsmiley.visibility = View.GONE persPlanEmpty = true recyclerView.visibility = View.VISIBLE - for (i in 0 until it.size) { - if (it[i].date.isNotEmpty() && it[i].date.substring(0, 3) == "psa") { - planCardList.add(it[i]) - } else if ((prefs.getString("courses", "") ?: "").isEmpty() && (prefs.getString("classes", "") ?: "").isNotEmpty()) { - if (it[i].group.isNotEmpty() && it[i].group != "") { - if ((prefs.getString("classes", "") ?: "").contains(it[i].group) || it[i].group.contains((prefs.getString("classes", "") ?: "").toString())) { - planCardList.add(it[i]) - persPlanEmpty = false - } - } - } else if ((prefs.getString("classes", "") ?: "").isNotEmpty() && (prefs.getString("courses", "") ?: "").isNotEmpty()) { - if (it[i].group != "" && it[i].course != "") { - if ((prefs.getString("courses", "") ?: "").contains(it[i].course)) { - if ((prefs.getString("classes", "") ?: "").contains(it[i].group) || it[i].group.contains((prefs.getString("classes", "") ?: "").toString())) { - planCardList.add(it[i]) - persPlanEmpty = false - } - } - } - } + + substitutions.filter { MiscData.checkPersonalSubstitutions(it, coursePreference, classPreference, true) }.forEach { substItem -> + planCardList.add(substItem) } - planCardList.sortWith(Comparator { lhs, rhs -> Integer.compare(rhs.priority, lhs.priority) }) + persPlanEmpty = (planCardList.size == 1 && planCardList[0].date.substring(0, 3) == "psa") || planCardList.isEmpty() + + planCardList.sortWith(Comparator { lhs, rhs -> rhs.priority.compareTo(lhs.priority) }) recyclerView.scheduleLayoutAnimation() mAdapter.setSubst(planCardList) handler.postDelayed({ if (persPlanEmpty) { -// recyclerView.visibility = View.GONE smileydown.visibility = View.VISIBLE smileydowntext.visibility = View.VISIBLE linearsmiley.visibility = View.VISIBLE diff --git a/app/src/main/java/com/denizd/substitutionplan/PlanFragment.kt b/app/src/main/java/com/denizd/substitutionplan/PlanFragment.kt index a18974f..5f54a3e 100644 --- a/app/src/main/java/com/denizd/substitutionplan/PlanFragment.kt +++ b/app/src/main/java/com/denizd/substitutionplan/PlanFragment.kt @@ -20,7 +20,7 @@ open class PlanFragment : Fragment(R.layout.plan) { lateinit var recyclerView: RecyclerView lateinit var mAdapter: CardAdapter private lateinit var layoutManager: GridLayoutManager - val planCardList = ArrayList() + var planCardList = ArrayList() lateinit var substViewModel: SubstViewModel lateinit var mContext: Context lateinit var prefs: SharedPreferences @@ -36,7 +36,6 @@ open class PlanFragment : Fragment(R.layout.plan) { super.onAttach(context) mContext = context prefs = PreferenceManager.getDefaultSharedPreferences(mContext) - edit = prefs.edit() } override fun onConfigurationChanged(newConfig: Configuration) { @@ -83,11 +82,11 @@ open class PlanFragment : Fragment(R.layout.plan) { mAdapter = CardAdapter(planCardList) recyclerView.adapter = mAdapter - if (prefs.getInt("firstTimeOpening", 0) == 2) { + if (prefs.getInt("firstTimeOpening", 0) == 1) { if (!prefs.getBoolean("notif", false)) { pullToRefresh.isRefreshing = true - DataFetcher(true, false, false, mContext, activity!!.application, view.rootView).execute() - edit.putInt("firstTimeOpening", 3).apply() + DataFetcher(true, true, false, mContext, activity!!.application, view.rootView).execute() + prefs.edit().putInt("firstTimeOpening", 2).apply() } } diff --git a/app/src/main/java/com/denizd/substitutionplan/SettingsFragment.kt b/app/src/main/java/com/denizd/substitutionplan/SettingsFragment.kt index af3c498..0e82b8f 100644 --- a/app/src/main/java/com/denizd/substitutionplan/SettingsFragment.kt +++ b/app/src/main/java/com/denizd/substitutionplan/SettingsFragment.kt @@ -313,6 +313,7 @@ class SettingsFragment : Fragment(R.layout.content_settings), View.OnClickListen private fun createRingtoneDialog() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + MiscData.getNotificationChannel(mContext, prefs) val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { putExtra(Settings.EXTRA_APP_PACKAGE, mContext.applicationContext.packageName) putExtra(Settings.EXTRA_CHANNEL_ID, "general") diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 126af75..25f74e6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -94,7 +94,7 @@ WP & Methodenstunde Kein Chrome-kompatibler Browser gefunden YouTube-App nicht gefunden - "Zuletzt aktualisiert: " + Zuletzt aktualisiert: Aktiviert den Nachtmodus Dein Plan diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06ca0af..555f4df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,7 +107,7 @@ No Chrome-compatible browser found YouTube app not found - "Last updated: " + Last updated: Enables night mode Your Plan