AppDetailsFragment: Drop dependency upon kovenant

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-07-21 00:02:30 +05:30
parent c7d3cd73c1
commit 1156f584d2
3 changed files with 287 additions and 206 deletions

View File

@@ -35,11 +35,12 @@ import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.Toast import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.epoxy.EpoxyRecyclerView
@@ -53,15 +54,12 @@ import com.aurora.extensions.runOnUiThread
import com.aurora.extensions.share import com.aurora.extensions.share
import com.aurora.extensions.show import com.aurora.extensions.show
import com.aurora.extensions.showDialog import com.aurora.extensions.showDialog
import com.aurora.extensions.stackTraceToString
import com.aurora.extensions.toast import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.State import com.aurora.store.State
import com.aurora.store.data.ViewState import com.aurora.store.data.ViewState
@@ -70,9 +68,6 @@ import com.aurora.store.data.downloader.getGroupId
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.Report
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.AppMetadataStatusListener import com.aurora.store.data.service.AppMetadataStatusListener
import com.aurora.store.data.service.UpdateService import com.aurora.store.data.service.UpdateService
@@ -81,7 +76,6 @@ import com.aurora.store.databinding.LayoutDetailsBetaBinding
import com.aurora.store.databinding.LayoutDetailsDescriptionBinding import com.aurora.store.databinding.LayoutDetailsDescriptionBinding
import com.aurora.store.databinding.LayoutDetailsDevBinding import com.aurora.store.databinding.LayoutDetailsDevBinding
import com.aurora.store.databinding.LayoutDetailsPermissionsBinding import com.aurora.store.databinding.LayoutDetailsPermissionsBinding
import com.aurora.store.databinding.LayoutDetailsPrivacyBinding
import com.aurora.store.databinding.LayoutDetailsReviewBinding import com.aurora.store.databinding.LayoutDetailsReviewBinding
import com.aurora.store.util.CommonUtil import com.aurora.store.util.CommonUtil
import com.aurora.store.util.Log import com.aurora.store.util.Log
@@ -99,6 +93,7 @@ import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.view.ui.sheets.InstallErrorDialogSheet import com.aurora.store.view.ui.sheets.InstallErrorDialogSheet
import com.aurora.store.view.ui.sheets.ManualDownloadSheet import com.aurora.store.view.ui.sheets.ManualDownloadSheet
import com.aurora.store.view.ui.sheets.PermissionBottomSheet import com.aurora.store.view.ui.sheets.PermissionBottomSheet
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsClusterViewModel import com.aurora.store.viewmodel.details.DetailsClusterViewModel
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -111,15 +106,12 @@ import com.tonyodev.fetch2.FetchGroup
import com.tonyodev.fetch2.FetchGroupListener import com.tonyodev.fetch2.FetchGroupListener
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2core.DownloadBlock import com.tonyodev.fetch2core.DownloadBlock
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.json.JSONObject
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import kotlinx.coroutines.launch
class AppDetailsFragment : BaseFragment(R.layout.fragment_details) { class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
@@ -127,13 +119,12 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
private val binding: FragmentDetailsBinding private val binding: FragmentDetailsBinding
get() = _binding!! get() = _binding!!
private val viewModel: AppDetailsViewModel by viewModels()
private val args: AppDetailsFragmentArgs by navArgs() private val args: AppDetailsFragmentArgs by navArgs()
private lateinit var bottomSheetBehavior: BottomSheetBehavior<LinearLayout> private lateinit var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>
private val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/api/search/"
private val exodusApiKey = "Token bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae"
private val startForStorageManagerResult = private val startForStorageManagerResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (isRAndAbove() && Environment.isExternalStorageManager()) { if (isRAndAbove() && Environment.isExternalStorageManager()) {
@@ -254,16 +245,119 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
_binding = FragmentDetailsBinding.bind(view) _binding = FragmentDetailsBinding.bind(view)
// TODO: Move to viewModel
authData = AuthProvider.with(view.context).getAuthData()
if (args.app != null) { if (args.app != null) {
app = args.app!! app = args.app!!
isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
inflatePartialApp() inflatePartialApp()
fetchCompleteApp()
} else { } else {
isExternal = true isExternal = true
app = App(args.packageName) app = App(args.packageName)
fetchCompleteApp() }
// App Details
viewModel.fetchAppDetails(view.context, app.packageName)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.app.collect {
if (app.packageName.isNotBlank()) {
if (isExternal) {
app = it
inflatePartialApp()
}
inflateExtraDetails(it)
viewModel.fetchAppReviews(view.context, app.packageName)
} else {
toast("Failed to fetch app details")
}
}
}
// Reviews
viewLifecycleOwner.lifecycleScope.launch {
viewModel.reviews.collect {
binding.layoutDetailsReview.epoxyRecycler.withModels {
it.take(4).forEach { add(ReviewViewModel_().id(it.timeStamp).review(it)) }
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.userReview.collect {
if (it.timeStamp == 0L) {
binding.layoutDetailsReview.userStars.rating = it.rating.toFloat()
Toast.makeText(
requireContext(),
getString(R.string.toast_rated_success),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
requireContext(),
getString(R.string.toast_rated_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
// Report (Exodus Privacy)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.report.collect { report ->
if (report == null) {
binding.layoutDetailsPrivacy.txtStatus.text =
getString(R.string.failed_to_fetch_report)
return@collect
}
if (report.trackers.isNotEmpty()) {
binding.layoutDetailsPrivacy.txtStatus.apply {
setTextColor(
ContextCompat.getColor(
requireContext(),
if (report.trackers.size > 4)
R.color.colorRed
else
R.color.colorOrange
)
)
text = "${report.trackers.size} ${getString(R.string.exodus_substring)} ${report.version}"
}
binding.layoutDetailsPrivacy.headerPrivacy.addClickListener {
findNavController().navigate(AppDetailsFragmentDirections
.actionAppDetailsFragmentToDetailsExodusFragment(report)
)
}
} else {
binding.layoutDetailsPrivacy.txtStatus.apply {
setTextColor(ContextCompat.getColor(requireContext(), R.color.colorGreen))
text = getString(R.string.exodus_no_tracker)
}
}
}
}
// Beta program
viewLifecycleOwner.lifecycleScope.launch {
viewModel.testingProgramStatus.collect {
if (it != null) {
binding.layoutDetailsBeta.btnBetaAction.isEnabled = true
if (it.subscribed) {
updateBetaActions(binding.layoutDetailsBeta, true)
}
if (it.unsubscribed) {
updateBetaActions(binding.layoutDetailsBeta, false)
}
} else {
app.testingProgram?.let { testingProgram ->
updateBetaActions(binding.layoutDetailsBeta, testingProgram.isSubscribed)
toast(getString(R.string.details_beta_delay))
}
}
}
} }
// Toolbar // Toolbar
@@ -367,16 +461,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
if (apkFiles.size > 1 && preferredInstaller == 1) { if (apkFiles.size > 1 && preferredInstaller == 1) {
showDialog(R.string.title_installer, R.string.dialog_desc_native_split) showDialog(R.string.title_installer, R.string.dialog_desc_native_split)
} else { } else {
task { viewModel.install(requireContext(), app.packageName, apkFiles.map { it.file })
AppInstaller.getInstance(requireContext())
.getPreferredInstaller()
.install(
app.packageName,
apkFiles.map { it.file }
)
} fail {
Log.e(it.stackTraceToString())
}
runOnUiThread { runOnUiThread {
binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
@@ -386,34 +471,14 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
@Synchronized @Synchronized
private fun uninstallApp() { private fun uninstallApp() {
task { AppInstaller.getInstance(requireContext()).getPreferredInstaller()
AppInstaller.getInstance(requireContext()) .uninstall(app.packageName)
.getPreferredInstaller()
.uninstall(app.packageName)
}
} }
private fun attachWhiteListStatus() { private fun attachWhiteListStatus() {
} }
private fun fetchCompleteApp() {
task {
authData = AuthProvider.with(requireContext()).getAuthData()
return@task AppDetailsHelper(authData)
.using(HttpClient.getPreferredClient())
.getAppByPackageName(app.packageName)
} successUi {
if (isExternal) {
app = it
inflatePartialApp()
}
inflateExtraDetails(it)
} failUi {
toast("Failed to fetch app details")
}
}
private fun inflatePartialApp() { private fun inflatePartialApp() {
if (::app.isInitialized) { if (::app.isInitialized) {
attachWhiteListStatus() attachWhiteListStatus()
@@ -467,7 +532,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
inflateAppDescription(binding.layoutDetailDescription, app) inflateAppDescription(binding.layoutDetailDescription, app)
inflateAppRatingAndReviews(binding.layoutDetailsReview, app) inflateAppRatingAndReviews(binding.layoutDetailsReview, app)
inflateAppDevInfo(binding.layoutDetailsDev, app) inflateAppDevInfo(binding.layoutDetailsDev, app)
inflateAppPrivacy(binding.layoutDetailsPrivacy, app) inflateAppPrivacy(app)
inflateAppPermission(binding.layoutDetailsPermissions, app) inflateAppPermission(binding.layoutDetailsPermissions, app)
if (!authData.isAnonymous) { if (!authData.isAnonymous) {
@@ -957,7 +1022,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
if (authData.isAnonymous) { if (authData.isAnonymous) {
toast(R.string.toast_anonymous_restriction) toast(R.string.toast_anonymous_restriction)
} else { } else {
addOrUpdateReview(B, app, Review().apply { addOrUpdateReview(app, Review().apply {
title = authData.userProfile!!.name title = authData.userProfile!!.name
rating = B.userStars.rating.toInt() rating = B.userStars.rating.toInt()
comment = B.inputReview.text.toString() comment = B.inputReview.text.toString()
@@ -973,66 +1038,10 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
) )
) )
} }
task {
fetchReviewSummary(app)
} successUi {
B.epoxyRecycler.withModels {
it.take(4)
.forEach {
add(
ReviewViewModel_()
.id(it.timeStamp)
.review(it)
)
}
}
} failUi {
}
} }
private fun inflateAppPrivacy(B: LayoutDetailsPrivacyBinding, app: App) { private fun inflateAppPrivacy(app: App) {
viewModel.fetchAppReport(app.packageName)
task {
fetchReport(app.packageName)
} successUi { report ->
if (report.trackers.isNotEmpty()) {
B.txtStatus.apply {
setTextColor(
ContextCompat.getColor(
requireContext(),
if (report.trackers.size > 4)
R.color.colorRed
else
R.color.colorOrange
)
)
text =
("${report.trackers.size} ${getString(R.string.exodus_substring)} ${report.version}")
}
B.headerPrivacy.addClickListener {
findNavController().navigate(
AppDetailsFragmentDirections
.actionAppDetailsFragmentToDetailsExodusFragment(report)
)
}
} else {
B.txtStatus.apply {
setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.colorGreen
)
)
text = getString(R.string.exodus_no_tracker)
}
}
} failUi {
B.txtStatus.text = it.message
}
} }
private fun inflateAppDevInfo(B: LayoutDetailsDevBinding, app: App) { private fun inflateAppDevInfo(B: LayoutDetailsDevBinding, app: App) {
@@ -1079,26 +1088,13 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
} }
B.btnBetaAction.setOnClickListener { B.btnBetaAction.setOnClickListener {
val authData = AuthProvider.with(requireContext()).getAuthData() B.btnBetaAction.text = getString(R.string.action_pending)
task { B.btnBetaAction.isEnabled = false
B.btnBetaAction.text = getString(R.string.action_pending) viewModel.fetchTestingProgramStatus(
B.btnBetaAction.isEnabled = false requireContext(),
AppDetailsHelper(authData).testingProgram( app.packageName,
app.packageName, !betaProgram.isSubscribed
!betaProgram.isSubscribed )
)
} successUi {
B.btnBetaAction.isEnabled = true
if (it.subscribed) {
updateBetaActions(B, true)
}
if (it.unsubscribed) {
updateBetaActions(B, false)
}
} failUi {
updateBetaActions(B, betaProgram.isSubscribed)
toast(getString(R.string.details_beta_delay))
}
} }
} else { } else {
B.root.hide() B.root.hide()
@@ -1190,82 +1186,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
return RatingView(requireContext(), number, max.toInt(), rating.toInt()) return RatingView(requireContext(), number, max.toInt(), rating.toInt())
} }
private fun addOrUpdateReview( private fun addOrUpdateReview(app: App, review: Review, isBeta: Boolean = false) {
B: LayoutDetailsReviewBinding, viewModel.postAppReview(requireContext(), app.packageName, review, isBeta)
app: App,
review: Review,
isBeta: Boolean = false
) {
task {
val authData = AuthProvider.with(requireContext()).getAuthData()
ReviewsHelper(authData)
.using(HttpClient.getPreferredClient())
.addOrEditReview(
app.packageName,
review.title,
review.comment,
review.rating,
isBeta
)
}.successUi {
it?.let {
B.userStars.rating = it.rating.toFloat()
Toast.makeText(
requireContext(),
getString(R.string.toast_rated_success),
Toast.LENGTH_SHORT
).show()
}
}.failUi {
Toast.makeText(
requireContext(),
getString(R.string.toast_rated_failed),
Toast.LENGTH_SHORT
).show()
}
}
private fun fetchReviewSummary(app: App): List<Review> {
val authData = AuthProvider
.with(requireContext())
.getAuthData()
val reviewsHelper = ReviewsHelper(authData)
.using(HttpClient.getPreferredClient())
return reviewsHelper.getReviewSummary(app.packageName)
}
/* App Privacy Helpers */
private fun parseResponse(response: String, packageName: String): List<Report> {
try {
val jsonObject = JSONObject(response)
val exodusObject = jsonObject.getJSONObject(packageName)
val exodusReport: ExodusReport = gson.fromJson(
exodusObject.toString(),
ExodusReport::class.java
)
return exodusReport.reports
} catch (e: Exception) {
throw Exception("No reports found")
}
}
private fun fetchReport(packageName: String): Report {
val headers: MutableMap<String, String> = mutableMapOf()
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
headers["Authorization"] = exodusApiKey
val url = exodusBaseUrl + packageName
val playResponse = HttpClient
.getPreferredClient()
.get(url, headers)
if (playResponse.isSuccessful) {
return parseResponse(String(playResponse.responseBytes), packageName)[0]
} else {
throw Exception("Failed to fetch report")
}
} }
} }

View File

@@ -0,0 +1,159 @@
package com.aurora.store.viewmodel.details
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.details.TestingProgramStatus
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.Report
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.google.gson.GsonBuilder
import java.lang.reflect.Modifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import org.json.JSONObject
class AppDetailsViewModel : ViewModel() {
private val TAG = AppDetailsViewModel::class.java.simpleName
private val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/api/search/"
private val exodusApiKey = "Token bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae"
private val _app = MutableSharedFlow<App>()
val app = _app.asSharedFlow()
private val _reviews = MutableSharedFlow<List<Review>>()
val reviews = _reviews.asSharedFlow()
private val _userReview = MutableSharedFlow<Review>()
val userReview = _userReview.asSharedFlow()
private val _report = MutableSharedFlow<Report?>()
val report = _report.asSharedFlow()
private val _testingProgramStatus = MutableSharedFlow<TestingProgramStatus?>()
val testingProgramStatus = _testingProgramStatus.asSharedFlow()
fun fetchAppDetails(context: Context, packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val authData = AuthProvider.with(context).getAuthData()
_app.emit(
AppDetailsHelper(authData).using(HttpClient.getPreferredClient())
.getAppByPackageName(packageName)
)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch app details", exception)
_app.emit(App(""))
}
}
}
fun fetchAppReviews(context: Context, packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val authData = AuthProvider.with(context).getAuthData()
_reviews.emit(ReviewsHelper(authData).using(HttpClient.getPreferredClient())
.getReviewSummary(packageName))
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch app reviews", exception)
_reviews.emit(emptyList())
}
}
}
fun postAppReview(context: Context, packageName: String, review: Review, isBeta: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
val authData = AuthProvider.with(context).getAuthData()
_userReview.emit(ReviewsHelper(authData)
.using(HttpClient.getPreferredClient())
.addOrEditReview(
packageName,
review.title,
review.comment,
review.rating,
isBeta
)!!)
} catch (exception: Exception) {
Log.e(TAG, "Failed to post review", exception)
_userReview.emit(Review())
}
}
}
fun fetchAppReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val headers: MutableMap<String, String> = mutableMapOf()
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
headers["Authorization"] = exodusApiKey
val url = exodusBaseUrl + packageName
val playResponse = HttpClient.getPreferredClient().get(url, headers)
_report.emit(parseResponse(String(playResponse.responseBytes), packageName)[0])
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch privacy report", exception)
_report.emit(null)
}
}
}
fun fetchTestingProgramStatus(context: Context, packageName: String, subscribe: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
val authData = AuthProvider.with(context).getAuthData()
_testingProgramStatus.emit(
AppDetailsHelper(authData).testingProgram(
packageName,
subscribe
)
)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch testing program status", exception)
_testingProgramStatus.emit(null)
}
}
}
@Synchronized
fun install(context: Context, packageName: String, files: List<Any>) {
try {
viewModelScope.launch(Dispatchers.IO) {
AppInstaller.getInstance(context).getPreferredInstaller()
.install(packageName, files)
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to install app", exception)
}
}
private fun parseResponse(response: String, packageName: String): List<Report> {
try {
val gson = GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC)
.create()
val jsonObject = JSONObject(response)
val exodusObject = jsonObject.getJSONObject(packageName)
val exodusReport: ExodusReport = gson.fromJson(
exodusObject.toString(), ExodusReport::class.java
)
return exodusReport.reports
} catch (e: Exception) {
throw Exception("No reports found")
}
}
}

View File

@@ -365,4 +365,5 @@
<string name="checking_for_updates">Checking for updates</string> <string name="checking_for_updates">Checking for updates</string>
<string name="pref_updates_check">Automated updates check</string> <string name="pref_updates_check">Automated updates check</string>
<string name="pref_updates_check_desc">Automatically check &amp; notify for new app updates periodically</string> <string name="pref_updates_check_desc">Automatically check &amp; notify for new app updates periodically</string>
<string name="failed_to_fetch_report">Failed to fetch privacy report</string>
</resources> </resources>