Merge branch 'dev'
This commit is contained in:
@@ -9,7 +9,8 @@ enum class DownloadStatus(@StringRes val localized: Int) {
|
|||||||
CANCELLED(R.string.status_cancelled),
|
CANCELLED(R.string.status_cancelled),
|
||||||
COMPLETED(R.string.status_completed),
|
COMPLETED(R.string.status_completed),
|
||||||
QUEUED(R.string.status_queued),
|
QUEUED(R.string.status_queued),
|
||||||
UNAVAILABLE(R.string.status_unavailable);
|
UNAVAILABLE(R.string.status_unavailable),
|
||||||
|
VERIFYING(R.string.status_verifying);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val finished = listOf(FAILED, CANCELLED, COMPLETED)
|
val finished = listOf(FAILED, CANCELLED, COMPLETED)
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.aurora.store.data.model
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
data class Request(
|
|
||||||
val url: String,
|
|
||||||
val file: File,
|
|
||||||
val size: Long,
|
|
||||||
val sha1: String,
|
|
||||||
val sha256: String
|
|
||||||
)
|
|
||||||
@@ -27,7 +27,6 @@ import com.aurora.store.data.installer.AppInstaller
|
|||||||
import com.aurora.store.data.model.Algorithm
|
import com.aurora.store.data.model.Algorithm
|
||||||
import com.aurora.store.data.model.DownloadInfo
|
import com.aurora.store.data.model.DownloadInfo
|
||||||
import com.aurora.store.data.model.DownloadStatus
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
import com.aurora.store.data.model.Request
|
|
||||||
import com.aurora.store.data.network.HttpClient
|
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.room.download.Download
|
import com.aurora.store.data.room.download.Download
|
||||||
@@ -125,7 +124,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Purchase the app (free apps needs to be purchased too)
|
// Purchase the app (free apps needs to be purchased too)
|
||||||
val requestList = mutableListOf<Request>()
|
val requestList = mutableListOf<GPlayFile>()
|
||||||
if (download.sharedLibs.isNotEmpty()) {
|
if (download.sharedLibs.isNotEmpty()) {
|
||||||
download.sharedLibs.forEach {
|
download.sharedLibs.forEach {
|
||||||
PathUtil.getLibDownloadDir(
|
PathUtil.getLibDownloadDir(
|
||||||
@@ -135,10 +134,10 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
it.packageName
|
it.packageName
|
||||||
).mkdirs()
|
).mkdirs()
|
||||||
it.fileList = it.fileList.ifEmpty { purchase(it.packageName, it.versionCode, 0) }
|
it.fileList = it.fileList.ifEmpty { purchase(it.packageName, it.versionCode, 0) }
|
||||||
requestList.addAll(getDownloadRequest(it.fileList, it.packageName))
|
requestList.addAll(it.fileList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
requestList.addAll(getDownloadRequest(download.fileList, null))
|
requestList.addAll(download.fileList)
|
||||||
|
|
||||||
// Update data for notification
|
// Update data for notification
|
||||||
download.totalFiles = requestList.size
|
download.totalFiles = requestList.size
|
||||||
@@ -169,8 +168,13 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!requestList.all { it.file.exists() }) {
|
try {
|
||||||
Log.e(TAG, "Downloaded files are missing")
|
// Verify all downloaded files
|
||||||
|
Log.i(TAG, "Verifying downloaded files")
|
||||||
|
notifyStatus(DownloadStatus.VERIFYING)
|
||||||
|
requestList.forEach { require(verifyFile(it)) }
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Failed to verify downloaded files!", exception)
|
||||||
onFailure()
|
onFailure()
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
@@ -237,47 +241,27 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDownloadRequest(files: List<GPlayFile>, libPackageName: String?): List<Request> {
|
|
||||||
val downloadList = mutableListOf<Request>()
|
|
||||||
files.filter { it.url.isNotBlank() }.forEach {
|
|
||||||
val file = when (it.type) {
|
|
||||||
GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT -> {
|
|
||||||
PathUtil.getApkDownloadFile(
|
|
||||||
appContext, download.packageName, download.versionCode, it, libPackageName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
GPlayFile.FileType.OBB, GPlayFile.FileType.PATCH -> {
|
|
||||||
PathUtil.getObbDownloadFile(download.packageName, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
downloadList.add(Request(it.url, file, it.size, it.sha1, it.sha256))
|
|
||||||
}
|
|
||||||
return downloadList
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads the file from the given request.
|
* Downloads the file from the given request.
|
||||||
* Failed downloads aren't removed and persists as long as [CacheWorker] doesn't cleans them.
|
* Failed downloads aren't removed and persists as long as [CacheWorker] doesn't cleans them.
|
||||||
* @param request A [Request] to download
|
* @param gFile A [GPlayFile] to download
|
||||||
* @return A [Result] indicating whether the file was downloaded or not.
|
* @return A [Result] indicating whether the file was downloaded or not.
|
||||||
*/
|
*/
|
||||||
private suspend fun downloadFile(request: Request): Result {
|
private suspend fun downloadFile(gFile: GPlayFile): Result {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val algorithm = if (request.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256
|
val file = PathUtil.getLocalFile(appContext, gFile, download)
|
||||||
val expectedSha = if (algorithm == Algorithm.SHA1) request.sha1 else request.sha256
|
|
||||||
|
|
||||||
// If file exists and sha matches the request, no need to download again
|
// If file exists and has integrity intact, no need to download again
|
||||||
if (request.file.exists() && validSha(request.file, expectedSha, algorithm)) {
|
if (file.exists() && verifyFile(gFile)) {
|
||||||
Log.i(TAG, "${request.file} is already downloaded!")
|
Log.i(TAG, "$file is already downloaded!")
|
||||||
downloadedBytes += request.file.length()
|
downloadedBytes += file.length()
|
||||||
return@withContext Result.success()
|
return@withContext Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Download as a temporary file to avoid installing corrupted files
|
// Download as a temporary file to avoid installing corrupted files
|
||||||
val tmpFileSuffix = ".tmp"
|
val tmpFileSuffix = ".tmp"
|
||||||
val tmpFile = File(request.file.absolutePath + tmpFileSuffix)
|
val tmpFile = File(file.absolutePath + tmpFileSuffix)
|
||||||
val isNewFile = tmpFile.createNewFile()
|
val isNewFile = tmpFile.createNewFile()
|
||||||
|
|
||||||
val okHttpClient = httpClient as HttpClient
|
val okHttpClient = httpClient as HttpClient
|
||||||
@@ -289,24 +273,19 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
headers["Range"] = "bytes=${tmpFile.length()}-"
|
headers["Range"] = "bytes=${tmpFile.length()}-"
|
||||||
}
|
}
|
||||||
|
|
||||||
okHttpClient.call(request.url, headers).body?.byteStream()?.use { input ->
|
okHttpClient.call(gFile.url, headers).body?.byteStream()?.use { input ->
|
||||||
FileOutputStream(tmpFile, !isNewFile).use {
|
FileOutputStream(tmpFile, !isNewFile).use {
|
||||||
input.copyTo(it, request.size).collect { p -> onProgress(p) }
|
input.copyTo(it, gFile.size).collect { p -> onProgress(p) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure downloaded file matches expected sha
|
if (!tmpFile.renameTo(file)) {
|
||||||
if (!validSha(tmpFile, expectedSha, algorithm)) {
|
|
||||||
throw Exception("Incorrect hash for $tmpFile")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tmpFile.renameTo(request.file)) {
|
|
||||||
throw Exception("Failed to remove .tmp extension from $tmpFile")
|
throw Exception("Failed to remove .tmp extension from $tmpFile")
|
||||||
}
|
}
|
||||||
|
|
||||||
return@withContext Result.success()
|
return@withContext Result.success()
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.e(TAG, "Failed to download ${request.file}!", exception)
|
Log.e(TAG, "Failed to download $file!", exception)
|
||||||
notifyStatus(DownloadStatus.FAILED)
|
notifyStatus(DownloadStatus.FAILED)
|
||||||
return@withContext Result.failure()
|
return@withContext Result.failure()
|
||||||
}
|
}
|
||||||
@@ -371,6 +350,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
downloadDao.updateStatus(download.packageName, status)
|
downloadDao.updateStatus(download.packageName, status)
|
||||||
|
|
||||||
when (status) {
|
when (status) {
|
||||||
|
DownloadStatus.VERIFYING,
|
||||||
DownloadStatus.CANCELLED -> return
|
DownloadStatus.CANCELLED -> return
|
||||||
DownloadStatus.COMPLETED -> {
|
DownloadStatus.COMPLETED -> {
|
||||||
// Mark progress as 100 manually to avoid race conditions
|
// Mark progress as 100 manually to avoid race conditions
|
||||||
@@ -387,14 +367,15 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates whether given file has the expected SHA hash sum.
|
* Verifies integrity of a downloaded [GPlayFile].
|
||||||
* @param file [File] to check
|
* @param gFile [GPlayFile] to verify
|
||||||
* @param expectedSha Expected SHA hash sum
|
|
||||||
* @param algorithm [Algorithm] of the SHA
|
|
||||||
* @return A boolean whether the given file has the expected SHA or not.
|
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
private suspend fun validSha(file: File, expectedSha: String, algorithm: Algorithm): Boolean {
|
private suspend fun verifyFile(gFile: GPlayFile): Boolean {
|
||||||
|
val file = PathUtil.getLocalFile(appContext, gFile, download)
|
||||||
|
val algorithm = if (gFile.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256
|
||||||
|
val expectedSha = if (algorithm == Algorithm.SHA1) gFile.sha1 else gFile.sha256
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val messageDigest = MessageDigest.getInstance(algorithm.value)
|
val messageDigest = MessageDigest.getInstance(algorithm.value)
|
||||||
DigestInputStream(file.inputStream(), messageDigest).use { input ->
|
DigestInputStream(file.inputStream(), messageDigest).use { input ->
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import com.aurora.extensions.isHuawei
|
|||||||
import com.aurora.extensions.isOAndAbove
|
import com.aurora.extensions.isOAndAbove
|
||||||
import com.aurora.extensions.isPAndAbove
|
import com.aurora.extensions.isPAndAbove
|
||||||
import com.aurora.extensions.isTAndAbove
|
import com.aurora.extensions.isTAndAbove
|
||||||
|
import com.aurora.extensions.isVAndAbove
|
||||||
import com.aurora.extensions.isValidApp
|
import com.aurora.extensions.isValidApp
|
||||||
import com.aurora.store.BuildConfig
|
import com.aurora.store.BuildConfig
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
@@ -95,6 +96,14 @@ object PackageUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isArchived(context: Context, packageName: String): Boolean {
|
||||||
|
return try {
|
||||||
|
isVAndAbove && context.packageManager.getArchivedPackage(packageName) != null
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun isSharedLibrary(context: Context, packageName: String): Boolean {
|
fun isSharedLibrary(context: Context, packageName: String): Boolean {
|
||||||
return if (isOAndAbove) {
|
return if (isOAndAbove) {
|
||||||
getAllSharedLibraries(context).any { it.name == packageName }
|
getAllSharedLibraries(context).any { it.name == packageName }
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.aurora.store.util
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import com.aurora.store.data.room.download.Download
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import com.aurora.gplayapi.data.models.File as GPlayFile
|
import com.aurora.gplayapi.data.models.File as GPlayFile
|
||||||
@@ -62,19 +63,33 @@ object PathUtil {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApkDownloadFile(
|
/**
|
||||||
context: Context,
|
* Returns an instance of java's [File] class for the given [GPlayFile]
|
||||||
packageName: String,
|
* @param context [Context]
|
||||||
versionCode: Int,
|
* @param gFile [GPlayFile] to download
|
||||||
file: GPlayFile,
|
* @param download An instance of [Download]
|
||||||
sharedLibPackageName: String? = null
|
*/
|
||||||
): File {
|
fun getLocalFile(context: Context, gFile: GPlayFile, download: Download): File {
|
||||||
val downloadDir = if (!sharedLibPackageName.isNullOrBlank()) {
|
val isSharedLib = download.sharedLibs.any { it.fileList.contains(gFile) }
|
||||||
getLibDownloadDir(context, packageName, versionCode, sharedLibPackageName)
|
return when (gFile.type) {
|
||||||
} else {
|
GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT -> {
|
||||||
getAppDownloadDir(context, packageName, versionCode)
|
val downloadDir = if (isSharedLib) {
|
||||||
|
getLibDownloadDir(
|
||||||
|
context,
|
||||||
|
download.packageName,
|
||||||
|
download.versionCode,
|
||||||
|
download.packageName
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
getAppDownloadDir(context, download.packageName, download.versionCode)
|
||||||
|
}
|
||||||
|
return File(downloadDir, gFile.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
GPlayFile.FileType.OBB, GPlayFile.FileType.PATCH -> {
|
||||||
|
File(getObbDownloadDir(download.packageName), gFile.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return File(downloadDir, file.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getZipFile(context: Context, packageName: String, versionCode: Int): File {
|
fun getZipFile(context: Context, packageName: String, versionCode: Int): File {
|
||||||
@@ -91,10 +106,6 @@ object PathUtil {
|
|||||||
return File(Environment.getExternalStorageDirectory(), "/Android/obb/$packageName")
|
return File(Environment.getExternalStorageDirectory(), "/Android/obb/$packageName")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getObbDownloadFile(packageName: String, file: GPlayFile): File {
|
|
||||||
return File(getObbDownloadDir(packageName), file.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSpoofDirectory(context: Context): File {
|
fun getSpoofDirectory(context: Context): File {
|
||||||
return File(context.filesDir, SPOOF)
|
return File(context.filesDir, SPOOF)
|
||||||
}
|
}
|
||||||
@@ -105,15 +116,5 @@ object PathUtil {
|
|||||||
file.createNewFile()
|
file.createNewFile()
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canReadWriteOBB(context: Context): Boolean {
|
|
||||||
val obbDir = context.obbDir.parentFile
|
|
||||||
|
|
||||||
if (obbDir != null) {
|
|
||||||
return obbDir.exists() && obbDir.canRead() && obbDir.canWrite()
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ class UpdateButton : RelativeLayout {
|
|||||||
fun updateState(downloadStatus: DownloadStatus) {
|
fun updateState(downloadStatus: DownloadStatus) {
|
||||||
val displayChild = when (downloadStatus) {
|
val displayChild = when (downloadStatus) {
|
||||||
DownloadStatus.QUEUED,
|
DownloadStatus.QUEUED,
|
||||||
DownloadStatus.DOWNLOADING -> 1
|
DownloadStatus.DOWNLOADING,
|
||||||
|
DownloadStatus.VERIFYING -> 1
|
||||||
|
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +66,9 @@ class UpdateButton : RelativeLayout {
|
|||||||
binding.viewFlipper.displayedChild = displayChild
|
binding.viewFlipper.displayedChild = displayChild
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not allowed to cancel installation at this point
|
||||||
|
binding.btnNegative.isEnabled = downloadStatus != DownloadStatus.VERIFYING
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addPositiveOnClickListener(onClickListener: OnClickListener?) {
|
fun addPositiveOnClickListener(onClickListener: OnClickListener?) {
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class AppUpdateView @JvmOverloads constructor(
|
|||||||
if (download != null) {
|
if (download != null) {
|
||||||
binding.btnAction.updateState(download.downloadStatus)
|
binding.btnAction.updateState(download.downloadStatus)
|
||||||
when (download.downloadStatus) {
|
when (download.downloadStatus) {
|
||||||
|
DownloadStatus.VERIFYING,
|
||||||
DownloadStatus.QUEUED -> {
|
DownloadStatus.QUEUED -> {
|
||||||
binding.progressDownload.isIndeterminate = true
|
binding.progressDownload.isIndeterminate = true
|
||||||
animateImageView(scaleFactor = 0.75f)
|
animateImageView(scaleFactor = 0.75f)
|
||||||
|
|||||||
@@ -228,11 +228,26 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
|
|||||||
setOnClickListener { openApp() }
|
setOnClickListener { openApp() }
|
||||||
}
|
}
|
||||||
binding.layoutDetailsApp.btnSecondaryAction.apply {
|
binding.layoutDetailsApp.btnSecondaryAction.apply {
|
||||||
|
isEnabled = true
|
||||||
text = getString(R.string.action_cancel)
|
text = getString(R.string.action_cancel)
|
||||||
setOnClickListener { viewModel.cancelDownload(app) }
|
setOnClickListener { viewModel.cancelDownload(app) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DownloadStatus.VERIFYING -> {
|
||||||
|
transformIcon(true)
|
||||||
|
binding.layoutDetailsApp.btnPrimaryAction.apply {
|
||||||
|
isEnabled = false
|
||||||
|
text = getString(R.string.action_open)
|
||||||
|
setOnClickListener(null)
|
||||||
|
}
|
||||||
|
binding.layoutDetailsApp.btnSecondaryAction.apply {
|
||||||
|
isEnabled = false
|
||||||
|
text = getString(R.string.action_cancel)
|
||||||
|
setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> checkAndSetupInstall()
|
else -> checkAndSetupInstall()
|
||||||
}
|
}
|
||||||
}.launchIn(viewLifecycleOwner.lifecycleScope)
|
}.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
@@ -572,7 +587,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
|
|||||||
|
|
||||||
// Setup primary and secondary action buttons
|
// Setup primary and secondary action buttons
|
||||||
binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true
|
binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true
|
||||||
binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true
|
binding.layoutDetailsApp.btnSecondaryAction.isEnabled = true
|
||||||
|
|
||||||
if (app.isInstalled) {
|
if (app.isInstalled) {
|
||||||
val isUpdatable = PackageUtil.isUpdatable(requireContext(), app.packageName, app.versionCode.toLong())
|
val isUpdatable = PackageUtil.isUpdatable(requireContext(), app.packageName, app.versionCode.toLong())
|
||||||
@@ -609,10 +624,16 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (app.isFree) {
|
if (PackageUtil.isArchived(requireContext(), app.packageName)) {
|
||||||
binding.layoutDetailsApp.btnPrimaryAction.setText(R.string.action_install)
|
binding.layoutDetailsApp.btnPrimaryAction.text =
|
||||||
|
getString(R.string.action_unarchive)
|
||||||
} else {
|
} else {
|
||||||
binding.layoutDetailsApp.btnPrimaryAction.text = app.price
|
if (app.isFree) {
|
||||||
|
binding.layoutDetailsApp.btnPrimaryAction.text =
|
||||||
|
getString(R.string.action_install)
|
||||||
|
} else {
|
||||||
|
binding.layoutDetailsApp.btnPrimaryAction.text = app.price
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener {
|
binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener {
|
||||||
|
|||||||
@@ -471,6 +471,7 @@
|
|||||||
<string name="status_completed">Completed</string>
|
<string name="status_completed">Completed</string>
|
||||||
<string name="status_queued">Queued</string>
|
<string name="status_queued">Queued</string>
|
||||||
<string name="status_unavailable">Unavailable</string>
|
<string name="status_unavailable">Unavailable</string>
|
||||||
|
<string name="status_verifying">Verifying</string>
|
||||||
|
|
||||||
<!-- UnarchivePackageReceiver -->
|
<!-- UnarchivePackageReceiver -->
|
||||||
<string name="authentication_required_title">Authentication required</string>
|
<string name="authentication_required_title">Authentication required</string>
|
||||||
@@ -479,4 +480,7 @@
|
|||||||
<!-- SpoofFragment -->
|
<!-- SpoofFragment -->
|
||||||
<string name="default_spoof">Default</string>
|
<string name="default_spoof">Default</string>
|
||||||
<string name="available_spoof">Available</string>
|
<string name="available_spoof">Available</string>
|
||||||
|
|
||||||
|
<!-- AppDetailsFragment -->
|
||||||
|
<string name="action_unarchive">Unarchive</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user