Add legacy self-update so v3 users can get v4 update
This commit is contained in:
@@ -150,6 +150,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="com.novoda.merlin.MerlinService"
|
android:name="com.novoda.merlin.MerlinService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<service android:name=".data.service.SelfUpdateService" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ object Constants {
|
|||||||
const val EXODUS_BASE_URL = "https://reports.exodus-privacy.eu.org/api/search/"
|
const val EXODUS_BASE_URL = "https://reports.exodus-privacy.eu.org/api/search/"
|
||||||
const val EXODUS_REPORT_URL = "https://reports.exodus-privacy.eu.org/reports/"
|
const val EXODUS_REPORT_URL = "https://reports.exodus-privacy.eu.org/reports/"
|
||||||
const val SHARE_URL = "http://play.google.com/store/apps/details?id="
|
const val SHARE_URL = "http://play.google.com/store/apps/details?id="
|
||||||
|
const val UPDATE_URL = "https://gitlab.com/AuroraOSS/AuroraStore/raw/master/updates.json"
|
||||||
|
|
||||||
const val NOTIFICATION_CHANNEL_ALERT = "NOTIFICATION_CHANNEL_ALERT"
|
const val NOTIFICATION_CHANNEL_ALERT = "NOTIFICATION_CHANNEL_ALERT"
|
||||||
const val NOTIFICATION_CHANNEL_GENERAL = "NOTIFICATION_CHANNEL_GENERAL"
|
const val NOTIFICATION_CHANNEL_GENERAL = "NOTIFICATION_CHANNEL_GENERAL"
|
||||||
|
|||||||
@@ -54,6 +54,20 @@ fun Context.showDialog(title: String?, message: String?) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.showDialog(title: String?, message: String?, listener: DialogInterface.OnDismissListener) {
|
||||||
|
runOnUiThread {
|
||||||
|
val backgroundColor: Int = getStyledAttributeColor(android.R.attr.colorBackground)
|
||||||
|
val builder = MaterialAlertDialogBuilder(this).apply {
|
||||||
|
setTitle(title)
|
||||||
|
setMessage(message)
|
||||||
|
setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _ -> listener }
|
||||||
|
background = ColorDrawable(backgroundColor)
|
||||||
|
}.create()
|
||||||
|
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Fragment.showDialog(@StringRes titleId: Int, @StringRes messageId: Int) {
|
fun Fragment.showDialog(@StringRes titleId: Int, @StringRes messageId: Int) {
|
||||||
requireContext().showDialog(titleId, messageId)
|
requireContext().showDialog(titleId, messageId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.aurora.store
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
@@ -39,13 +40,15 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.NavDestination
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import androidx.navigation.ui.NavigationUI
|
import androidx.navigation.ui.NavigationUI
|
||||||
import com.aurora.extensions.getEmptyActivityBundle
|
import com.aurora.Constants
|
||||||
import com.aurora.extensions.getStyledAttributeColor
|
import com.aurora.extensions.*
|
||||||
import com.aurora.extensions.load
|
|
||||||
import com.aurora.extensions.open
|
|
||||||
import com.aurora.gplayapi.data.models.AuthData
|
import com.aurora.gplayapi.data.models.AuthData
|
||||||
|
import com.aurora.store.data.model.SelfUpdate
|
||||||
|
import com.aurora.store.data.network.HttpClient
|
||||||
import com.aurora.store.data.providers.AuthProvider
|
import com.aurora.store.data.providers.AuthProvider
|
||||||
|
import com.aurora.store.data.service.SelfUpdateService
|
||||||
import com.aurora.store.databinding.ActivityMainBinding
|
import com.aurora.store.databinding.ActivityMainBinding
|
||||||
|
import com.aurora.store.util.CertUtil.isFDroidApp
|
||||||
import com.aurora.store.util.Log
|
import com.aurora.store.util.Log
|
||||||
import com.aurora.store.util.Preferences
|
import com.aurora.store.util.Preferences
|
||||||
import com.aurora.store.view.ui.about.AboutActivity
|
import com.aurora.store.view.ui.about.AboutActivity
|
||||||
@@ -60,7 +63,10 @@ import com.aurora.store.view.ui.search.SearchSuggestionActivity
|
|||||||
import com.aurora.store.view.ui.spoof.SpoofActivity
|
import com.aurora.store.view.ui.spoof.SpoofActivity
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.livinglifetechway.quickpermissions_kotlin.runWithPermissions
|
import com.livinglifetechway.quickpermissions_kotlin.runWithPermissions
|
||||||
|
import nl.komponents.kovenant.task
|
||||||
|
import nl.komponents.kovenant.ui.successUi
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : BaseActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
@@ -116,6 +122,9 @@ class MainActivity : BaseActivity() {
|
|||||||
checkExternalStorageAccessPermission()
|
checkExternalStorageAccessPermission()
|
||||||
checkExternalStorageManagerPermission()
|
checkExternalStorageManagerPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Check self update */
|
||||||
|
checkSelfUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attachToolbar() {
|
private fun attachToolbar() {
|
||||||
@@ -237,4 +246,63 @@ class MainActivity : BaseActivity() {
|
|||||||
startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION))
|
startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkSelfUpdate() {
|
||||||
|
task {
|
||||||
|
HttpClient.getPreferredClient().get(Constants.UPDATE_URL, mapOf())
|
||||||
|
} successUi { playResponse ->
|
||||||
|
if (playResponse.isSuccessful) {
|
||||||
|
val selfUpdate = gson.fromJson(
|
||||||
|
String(playResponse.responseBytes),
|
||||||
|
SelfUpdate::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
selfUpdate?.let { it ->
|
||||||
|
if (it.versionCode > BuildConfig.VERSION_CODE) {
|
||||||
|
if (isFDroidApp(this, BuildConfig.APPLICATION_ID)) {
|
||||||
|
if (it.fdroidBuild.isNotEmpty()) {
|
||||||
|
showUpdatesDialog(it)
|
||||||
|
}
|
||||||
|
} else if (it.auroraBuild.isNotEmpty()) {
|
||||||
|
showUpdatesDialog(it)
|
||||||
|
} else {
|
||||||
|
Log.i(getString(R.string.details_no_updates))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i(getString(R.string.details_no_updates))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("Failed to check self update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUpdatesDialog(selfUpdate: SelfUpdate) {
|
||||||
|
val messages: List<String> = listOf(
|
||||||
|
selfUpdate.versionCode.toString(),
|
||||||
|
if (selfUpdate.changelog.isEmpty())
|
||||||
|
getString(R.string.details_changelog_unavailable)
|
||||||
|
else
|
||||||
|
selfUpdate.changelog,
|
||||||
|
getString(R.string.dialog_desc_self_update)
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(getString(R.string.dialog_title_self_update))
|
||||||
|
.setMessage(messages.joinToString(separator = "\n"))
|
||||||
|
.setPositiveButton(getString(R.string.action_update)) { _, _ ->
|
||||||
|
val intent = Intent(this, SelfUpdateService::class.java)
|
||||||
|
intent.putExtra(Constants.STRING_EXTRA, gson.toJson(selfUpdate))
|
||||||
|
startService(intent)
|
||||||
|
}
|
||||||
|
.setNegativeButton(getString(R.string.action_later)) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
val backGroundColor: Int = getStyledAttributeColor(android.R.attr.colorBackground)
|
||||||
|
builder.background = ColorDrawable(backGroundColor)
|
||||||
|
builder.create()
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ class NativeInstaller(context: Context) : InstallerBase(context) {
|
|||||||
if (isAlreadyQueued(packageName)) {
|
if (isAlreadyQueued(packageName)) {
|
||||||
Log.i("$packageName already queued")
|
Log.i("$packageName already queued")
|
||||||
} else {
|
} else {
|
||||||
|
Log.i("Received native install request for $packageName")
|
||||||
files.map {
|
files.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is File -> it
|
is File -> it
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class SessionInstaller(context: Context) : InstallerBase(context) {
|
|||||||
if (isAlreadyQueued(packageName)) {
|
if (isAlreadyQueued(packageName)) {
|
||||||
Log.i("$packageName already queued")
|
Log.i("$packageName already queued")
|
||||||
} else {
|
} else {
|
||||||
Log.i("Received service install request for $packageName")
|
Log.i("Received session install request for $packageName")
|
||||||
val uriList = files.map {
|
val uriList = files.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is File -> getUri(it)
|
is File -> getUri(it)
|
||||||
|
|||||||
39
app/src/main/java/com/aurora/store/data/model/SelfUpdate.kt
Normal file
39
app/src/main/java/com/aurora/store/data/model/SelfUpdate.kt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Aurora Store
|
||||||
|
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
||||||
|
*
|
||||||
|
* Aurora Store is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Aurora Store is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.data.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class SelfUpdate {
|
||||||
|
@SerializedName("version_name")
|
||||||
|
var versionName: String = String()
|
||||||
|
|
||||||
|
@SerializedName("version_code")
|
||||||
|
var versionCode: Int = 0
|
||||||
|
|
||||||
|
@SerializedName("aurora_build")
|
||||||
|
var auroraBuild: String = String()
|
||||||
|
|
||||||
|
@SerializedName("fdroid_build")
|
||||||
|
var fdroidBuild: String = String()
|
||||||
|
|
||||||
|
@SerializedName("changelog")
|
||||||
|
var changelog: String = String()
|
||||||
|
}
|
||||||
@@ -23,12 +23,12 @@ import android.app.NotificationManager
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import com.aurora.store.BuildConfig
|
||||||
import com.aurora.store.data.event.BusEvent.InstallEvent
|
import com.aurora.store.data.event.BusEvent.InstallEvent
|
||||||
import com.aurora.store.data.event.BusEvent.UninstallEvent
|
import com.aurora.store.data.event.BusEvent.UninstallEvent
|
||||||
import com.aurora.store.data.installer.AppInstaller
|
import com.aurora.store.data.installer.AppInstaller
|
||||||
import com.aurora.store.util.PathUtil
|
import com.aurora.store.util.PathUtil
|
||||||
import com.aurora.store.util.Preferences
|
import com.aurora.store.util.Preferences
|
||||||
import com.aurora.store.util.isExternalStorageEnable
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -62,6 +62,10 @@ open class PackageManagerReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
if (isAutoDeleteAPKEnabled)
|
if (isAutoDeleteAPKEnabled)
|
||||||
clearDownloads(context, packageName)
|
clearDownloads(context, packageName)
|
||||||
|
|
||||||
|
//Clear self update apk
|
||||||
|
if (packageName == BuildConfig.APPLICATION_ID)
|
||||||
|
clearDownloads(context, packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package com.aurora.store.data.service
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.aurora.Constants
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.gplayapi.data.models.File
|
||||||
|
import com.aurora.store.BuildConfig
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.data.downloader.DownloadManager
|
||||||
|
import com.aurora.store.data.downloader.RequestBuilder.buildRequest
|
||||||
|
import com.aurora.store.data.installer.AppInstaller
|
||||||
|
import com.aurora.store.data.model.SelfUpdate
|
||||||
|
import com.aurora.store.util.CertUtil.isFDroidApp
|
||||||
|
import com.aurora.store.util.Log
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.tonyodev.fetch2.*
|
||||||
|
import nl.komponents.kovenant.task
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class SelfUpdateService : Service() {
|
||||||
|
private lateinit var app: App
|
||||||
|
private lateinit var fetch: Fetch
|
||||||
|
private lateinit var fetchListener: FetchListener
|
||||||
|
|
||||||
|
private var gson: Gson = GsonBuilder()
|
||||||
|
.excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
private val hashCode = BuildConfig.APPLICATION_ID.hashCode()
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
val rawSelfUpdate = intent.getStringExtra(Constants.STRING_EXTRA)
|
||||||
|
if (StringUtils.isNotEmpty(rawSelfUpdate)) {
|
||||||
|
val selfUpdate = gson.fromJson(rawSelfUpdate, SelfUpdate::class.java)
|
||||||
|
selfUpdate?.let {
|
||||||
|
downloadAndUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
startForeground(1, notification)
|
||||||
|
} else {
|
||||||
|
val notification = getNotification(
|
||||||
|
NotificationCompat.Builder(
|
||||||
|
this,
|
||||||
|
Constants.NOTIFICATION_CHANNEL_GENERAL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
startForeground(1, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun destroyService() {
|
||||||
|
Log.d("Self-update service destroyed")
|
||||||
|
fetch.removeListener(fetchListener)
|
||||||
|
stopForeground(true)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadAndUpdate(update: SelfUpdate) {
|
||||||
|
app = App(BuildConfig.APPLICATION_ID)
|
||||||
|
app.id = hashCode
|
||||||
|
app.packageName = BuildConfig.APPLICATION_ID
|
||||||
|
app.displayName = getString(R.string.app_name)
|
||||||
|
app.versionName = update.versionName
|
||||||
|
app.versionCode = update.versionCode
|
||||||
|
|
||||||
|
val file = File().apply {
|
||||||
|
name = "AuroraStore.apk"
|
||||||
|
url = if (isFDroidVariant)
|
||||||
|
update.fdroidBuild
|
||||||
|
else
|
||||||
|
update.auroraBuild
|
||||||
|
type = File.FileType.BASE
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = buildRequest(this, app, file)
|
||||||
|
request.enqueueAction = EnqueueAction.REPLACE_EXISTING
|
||||||
|
|
||||||
|
val requestList: MutableList<Request> = ArrayList()
|
||||||
|
requestList.add(request)
|
||||||
|
|
||||||
|
fetch = DownloadManager.with(this).fetch
|
||||||
|
fetch.enqueue(requestList) {
|
||||||
|
Log.i("Downloading latest update")
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchListener = getFetchListener()
|
||||||
|
fetch.addListener(fetchListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isFDroidVariant: Boolean
|
||||||
|
get() = isFDroidApp(this, BuildConfig.APPLICATION_ID)
|
||||||
|
|
||||||
|
@get:RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private val notification: Notification
|
||||||
|
get() {
|
||||||
|
val notificationBuilder =
|
||||||
|
NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_GENERAL)
|
||||||
|
return getNotification(notificationBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNotification(builder: NotificationCompat.Builder): Notification {
|
||||||
|
return builder.setAutoCancel(true)
|
||||||
|
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
||||||
|
.setContentTitle("Self update")
|
||||||
|
.setContentText("Updating Aurora Store in background")
|
||||||
|
.setOngoing(true)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification_outlined)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFetchListener(): FetchListener {
|
||||||
|
return object : AbstractFetchGroupListener() {
|
||||||
|
override fun onError(
|
||||||
|
groupId: Int, download: Download, error: Error,
|
||||||
|
throwable: Throwable?, fetchGroup: FetchGroup
|
||||||
|
) {
|
||||||
|
if (groupId == app.id) {
|
||||||
|
Log.e("Error self-updating ${app.displayName}")
|
||||||
|
destroyService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||||
|
if (groupId == app.id && fetchGroup.groupDownloadProgress == 100) {
|
||||||
|
Log.d("Calling installer ${app.displayName}")
|
||||||
|
AppInstaller(this@SelfUpdateService).getPreferredInstaller().install(
|
||||||
|
BuildConfig.APPLICATION_ID,
|
||||||
|
listOf(download.file)
|
||||||
|
)
|
||||||
|
|
||||||
|
task {
|
||||||
|
TimeUnit.SECONDS.sleep(10)
|
||||||
|
} success {
|
||||||
|
destroyService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||||
|
if (groupId == app.id) {
|
||||||
|
Log.d("Self-update cancelled ${app.displayName}")
|
||||||
|
destroyService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -238,6 +238,11 @@
|
|||||||
<string name="device_miui_description">"Please, disable MIUI optimizations to allow installations, otherwise you can choose Root or Services installer."</string>
|
<string name="device_miui_description">"Please, disable MIUI optimizations to allow installations, otherwise you can choose Root or Services installer."</string>
|
||||||
<string name="device_miui_extra">"Optionally you can choose Native installer, but then you can not install bundled (split) APKs, so choice is yours."</string>
|
<string name="device_miui_extra">"Optionally you can choose Native installer, but then you can not install bundled (split) APKs, so choice is yours."</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="dialog_title_self_update">"New update available"</string>
|
||||||
|
<string name="dialog_desc_self_update">"Do you wish to update now ?"</string>
|
||||||
|
<string name="list_update_all_txt_one">"update available"</string>
|
||||||
|
|
||||||
<string name="notification_channel_alert">"Quick notification"</string>
|
<string name="notification_channel_alert">"Quick notification"</string>
|
||||||
<string name="notification_channel_general">"General notification"</string>
|
<string name="notification_channel_general">"General notification"</string>
|
||||||
<string name="notification_installation_auto">"Click to install"</string>
|
<string name="notification_installation_auto">"Click to install"</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user