Merge pull request 'add check all updates page' (#15) from feat-updates into main

Reviewed-on: https://codeberg.org/mi6e4ka/openstore/pulls/15
This commit is contained in:
mi6e4ka 2026-01-31 19:45:29 +01:00
commit 0cc49e1ad0
14 changed files with 746 additions and 1 deletions

View file

@ -29,6 +29,7 @@ import dev.mi6e4ka.openstore.ui.screen.results.ResultsScreen
import dev.mi6e4ka.openstore.ui.screen.reviews.ReviewsScreen
import dev.mi6e4ka.openstore.ui.screen.search.SearchScreen
import dev.mi6e4ka.openstore.ui.screen.settings.SettingsScreen
import dev.mi6e4ka.openstore.ui.screen.updates.UpdatesScreen
import dev.mi6e4ka.openstore.ui.theme.AppTheme
class MainActivity : ComponentActivity() {
@ -116,6 +117,9 @@ class MainActivity : ComponentActivity() {
val packageName = it.arguments?.getString("packageName") ?: ""
ReviewsScreen(packageName = packageName, navController = navController)
}
composable("updates") {
UpdatesScreen(navController = navController)
}
composable("settings") {
SettingsScreen(navController = navController)
}

View file

@ -2,10 +2,14 @@ package dev.mi6e4ka.openstore.data.api
import dev.mi6e4ka.openstore.data.model.AppCommentsResponse
import dev.mi6e4ka.openstore.data.model.AppRatingResponse
import dev.mi6e4ka.openstore.data.model.AppUpdateRequest
import dev.mi6e4ka.openstore.data.model.AppUpdateResponse
import dev.mi6e4ka.openstore.data.model.BatchItemDetailsRequest
import dev.mi6e4ka.openstore.data.model.DownloadLinkRequest
import dev.mi6e4ka.openstore.data.model.DownloadLinkResponse
import dev.mi6e4ka.openstore.data.model.ItemDetailsResponse
import dev.mi6e4ka.openstore.data.model.SearchResponse
import dev.mi6e4ka.openstore.data.model.ShortItemDetails
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@ -45,4 +49,14 @@ interface ApiService {
@Query("pageSize") pageSize: Int,
@Query("sortBy") sortBy: String,
) : AppCommentsResponse
@POST("applicationData/newApps")
suspend fun getBatchUpdates(
@Body request: AppUpdateRequest
) : AppUpdateResponse
@POST("v2/showcase/store-app")
suspend fun getBatchItemDetails(
@Body request: BatchItemDetailsRequest
) : List<ShortItemDetails>
}

View file

@ -0,0 +1,31 @@
package dev.mi6e4ka.openstore.data.model
import com.google.gson.annotations.SerializedName
data class AppUpdateRequestEntry(
@SerializedName("packageName") val packageName: String,
@Transient val appName: String,
@SerializedName("versionCode") val versionCode: Long,
@Transient val versionName: String,
@Transient val installSource: String
)
data class AppUpdateRequest(
@SerializedName("content") val content: List<AppUpdateRequestEntry>
)
data class AppUpdateResponse(
@SerializedName("body") val body: AppUpdateResponseBody
)
data class AppUpdateResponseBody(
@SerializedName("content") val content: List<AppUpdateResponseEntry>
)
data class AppUpdateResponseEntry(
@SerializedName("appId") val appId: Int,
@SerializedName("packageName") val packageName: String,
@SerializedName("appName") val appName: String,
@SerializedName("updatedAt") val updatedAt: String,
@SerializedName("versionCode") val versionCode: Long
)

View file

@ -23,6 +23,18 @@ data class ItemDetails(
@SerializedName("appVerUpdatedAt") val appVerUpdatedAt: String,
)
data class ShortItemDetails(
@SerializedName("appId") val appId: Int,
@SerializedName("appName") val appName: String,
@SerializedName("packageName") val packageName: String,
@SerializedName("iconUrl") val iconUrl: String,
@SerializedName("versionCode") val versionCode: Long,
)
data class BatchItemDetailsRequest(
@SerializedName("packageNames") val packageNames: List<String>,
)
data class Files(
@SerializedName("fileUrl") val fileUrl: String,
@SerializedName("ordinal") val ordinal: Int,

View file

@ -5,10 +5,16 @@ import androidx.paging.PagingConfig
import dev.mi6e4ka.openstore.data.api.ApiClient
import dev.mi6e4ka.openstore.data.model.AppCommentsComments
import dev.mi6e4ka.openstore.data.model.AppRatingResponseBody
import dev.mi6e4ka.openstore.data.model.AppUpdateRequest
import dev.mi6e4ka.openstore.data.model.AppUpdateRequestEntry
import dev.mi6e4ka.openstore.data.model.AppUpdateResponse
import dev.mi6e4ka.openstore.data.model.AppUpdateResponseEntry
import dev.mi6e4ka.openstore.data.model.BatchItemDetailsRequest
import dev.mi6e4ka.openstore.data.model.DownloadLinkRequest
import dev.mi6e4ka.openstore.data.model.DownloadLinkResponseItem
import dev.mi6e4ka.openstore.data.model.ItemDetails
import dev.mi6e4ka.openstore.data.model.SearchResult
import dev.mi6e4ka.openstore.data.model.ShortItemDetails
import dev.mi6e4ka.openstore.data.paging.SearchPagingSource
class SearchRepository {
@ -48,4 +54,12 @@ class SearchRepository {
suspend fun getAppComments(packageName: String, pageSize: Int, sortBy: String): List<AppCommentsComments> {
return apiService.getAppComments(packageName, pageSize = pageSize, sortBy = sortBy).body.content
}
suspend fun getBatchUpdates(apps: List<AppUpdateRequestEntry>): List<AppUpdateResponseEntry> {
return apiService.getBatchUpdates(AppUpdateRequest(content = apps)).body.content
}
suspend fun getBatchItemDetails(packageNames: List<String>) : List<ShortItemDetails> {
return apiService.getBatchItemDetails(request = BatchItemDetailsRequest(packageNames = packageNames))
}
}

View file

@ -88,6 +88,12 @@ fun SearchScreen(navController: NavController = rememberNavController()) {
topBar = { TopAppBar(
title = {Text(stringResource(R.string.app_name))},
actions = {
IconButton(onClick = { navController.navigate("updates") }) {
Icon(
painter = painterResource(R.drawable.ic_update_24px),
contentDescription = stringResource(R.string.updates_title)
)
}
IconButton({navController.navigate("settings")}) {
Icon(painterResource(R.drawable.ic_settings_24px), "settings")
}

View file

@ -105,6 +105,10 @@ fun SettingsScreen(
SettingsSectionSelect(
title = "Google Play",
value = "com.android.vending"
),
SettingsSectionSelect(
title = "Manual Installed",
value = "com.android.packageinstaller"
)
)
),

View file

@ -0,0 +1,311 @@
package dev.mi6e4ka.openstore.ui.screen.updates
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import dev.mi6e4ka.openstore.R
import dev.mi6e4ka.openstore.internal.installer.ApkInstaller
import dev.mi6e4ka.openstore.ui.components.AppIcon
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UpdatesScreen(navController: NavController) {
val context = LocalContext.current
val apkInstaller = remember { ApkInstaller(context) }
val viewModel: UpdatesViewModel = viewModel(
factory = UpdatesViewModelFactory(apkInstaller)
)
val state by viewModel.state.collectAsState()
LaunchedEffect(Unit) {
viewModel.initPreferences(context)
viewModel.loadAndCheckUpdates(context)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.updates_title)) },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(
painter = painterResource(R.drawable.ic_arrow_back_24px),
contentDescription = "Back"
)
}
},
actions = {
IconButton(
onClick = { viewModel.checkForUpdates(context) },
enabled = !state.isCheckingUpdates && !state.isLoading
) {
Icon(
painter = painterResource(R.drawable.ic_refresh_24px),
contentDescription = stringResource(R.string.check_updates)
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
if (state.isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (state.isCheckingUpdates) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.size(16.dp))
Text(stringResource(R.string.checking_updates))
}
} else if (state.appsWithUpdates.isEmpty()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
UpdatesBanner(navController, state.installedApps.size)
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(R.drawable.ic_check_circle_24px),
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.size(16.dp))
Text(
stringResource(R.string.all_apps_updated),
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
Spacer(modifier = Modifier.weight(1f))
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
UpdatesBanner(navController, state.installedApps.size)
}
item {
Text(
stringResource(R.string.available_updates, state.appsWithUpdates.size),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 8.dp)
)
}
items(state.appsWithUpdates, key = { it.packageName }) { app ->
UpdateAppCard(
app = app,
isDownloading = state.downloadingPackages.contains(app.packageName),
downloadProgress = state.downloadProgress[app.packageName] ?: 0,
onUpdateClick = { viewModel.downloadAndUpdateApp(context, app) },
onCancelClick = { viewModel.cancelDownload(app.packageName) },
onCardClick = { navController.navigate("app/${app.packageName}?platform=mobile") }
)
}
}
}
}
}
}
@Composable
fun UpdatesBanner(navController: NavController, checkAppsCount: Int) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp)
.height(80.dp)
.clip(RoundedCornerShape(12.dp))
.clickable(onClick = {
navController.navigate("settings")
}),
//colors = CardDefaults.cardColors(containerColor = Color(109, 255, 30, 40))
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(horizontal = 24.dp)
) {
Icon(
painterResource(R.drawable.ic_settings_24px),
contentDescription = null,
modifier = Modifier.size(32.dp),
)
Column {
Text(stringResource(R.string.updates_app_count, checkAppsCount))
Text(stringResource(R.string.updates_change_settings))
}
}
}
}
@Composable
fun UpdateAppCard(
app: AppWithUpdates,
isDownloading: Boolean,
downloadProgress: Int,
onUpdateClick: () -> Unit,
onCancelClick: () -> Unit,
onCardClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.clickable(onClick = onCardClick)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
if (app.iconUrl != null) {
AppIcon(
app.iconUrl,
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(12.dp))
)
} else {
Box(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(R.drawable.ic_android_24px),
contentDescription = null,
modifier = Modifier.size(32.dp)
)
}
}
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp)
) {
Text(
text = app.appName,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "${app.installedVersionCode}${app.latestVersionCode}",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "from ${app.installSource}",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (isDownloading) {
Spacer(modifier = Modifier.size(4.dp))
LinearProgressIndicator(
progress = { downloadProgress / 100f },
modifier = Modifier.fillMaxWidth()
)
}
}
if (isDownloading) {
FilledTonalIconButton(onClick = onCancelClick) {
Icon(
painter = painterResource(R.drawable.ic_close_24px),
contentDescription = null
)
}
} else {
FilledTonalButton(onClick = onUpdateClick) {
Text(stringResource(R.string.update))
}
}
}
}
}

View file

@ -0,0 +1,293 @@
package dev.mi6e4ka.openstore.ui.screen.updates
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.mi6e4ka.openstore.data.model.AppUpdateRequestEntry
import dev.mi6e4ka.openstore.di.AppModule
import dev.mi6e4ka.openstore.internal.installer.ApkInstaller
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
data class AppWithUpdates (
val packageName: String,
val appName: String,
val installedVersionCode: Long,
val installedVersionName: String,
val iconUrl: String? = null,
val latestVersionCode: Long? = null,
val latestVersionName: String? = null,
val appId: Int? = null,
val installSource: String
)
data class UpdatesState(
val isLoading: Boolean = false,
val isCheckingUpdates: Boolean = false,
val installedApps: List<AppUpdateRequestEntry> = emptyList(),
val appsWithUpdates: List<AppWithUpdates> = emptyList(),
val error: String? = null,
val downloadingPackages: Set<String> = emptySet(),
val downloadProgress: Map<String, Int> = emptyMap(),
val excludeGooglePlay: Boolean = true
)
class UpdatesViewModel(private val apkInstaller: ApkInstaller) : ViewModel() {
private val _state = MutableStateFlow(UpdatesState())
val state: StateFlow<UpdatesState> = _state.asStateFlow()
private val cancelDownloads = mutableMapOf<String, () -> Unit>()
companion object {
private const val PREFS_NAME = "updates_prefs"
private const val KEY_EXCLUDE_GOOGLE_PLAY = "exclude_google_play"
}
fun initPreferences(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val excludeGooglePlay = prefs.getBoolean(KEY_EXCLUDE_GOOGLE_PLAY, true)
_state.update { it.copy(excludeGooglePlay = excludeGooglePlay) }
}
fun toggleExcludeGooglePlay(context: Context) {
val newValue = !_state.value.excludeGooglePlay
_state.update { it.copy(excludeGooglePlay = newValue) }
// Сохраняем в SharedPreferences
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_EXCLUDE_GOOGLE_PLAY, newValue)
.apply()
loadAndCheckUpdates(context)
}
fun loadAndCheckUpdates(context: Context) {
viewModelScope.launch {
// Загружаем список приложений
_state.update { it.copy(isLoading = true, error = null) }
val installedApps = try {
withContext(Dispatchers.IO) {
getInstalledApps(context)
}
} catch (e: Exception) {
_state.update { it.copy(error = e.message, isLoading = false) }
return@launch
}
_state.update { it.copy(installedApps = installedApps, isLoading = false) }
// Сразу запускаем проверку
checkForUpdatesInternal(installedApps)
}
}
fun checkForUpdates(context: Context) {
loadAndCheckUpdates(context)
}
private suspend fun checkForUpdatesInternal(installedApps: List<AppUpdateRequestEntry>) {
_state.update { it.copy(isCheckingUpdates = true, appsWithUpdates = emptyList()) }
val rawUpdates = AppModule.searchRepository.getBatchUpdates(installedApps)
if (rawUpdates.isEmpty()) {
_state.update {
it.copy(
isCheckingUpdates = false,
appsWithUpdates = emptyList()
)
}
return
}
val detailsUpdates = AppModule.searchRepository.getBatchItemDetails(rawUpdates.map {it.packageName} )
val detailsByPackageName = detailsUpdates.associateBy {it.packageName}
val installedAppsByPackageName = installedApps.associateBy {it.packageName}
val appsWithUpdates = rawUpdates.mapNotNull {
val details = detailsByPackageName[it.packageName] ?: return@mapNotNull null
val installedApp = installedAppsByPackageName[it.packageName] ?: return@mapNotNull null
AppWithUpdates(
packageName = it.packageName,
appName = it.appName,
installedVersionCode = installedApp.versionCode,
installedVersionName = installedApp.versionName,
iconUrl = details.iconUrl,
latestVersionCode = details.versionCode,
latestVersionName = "?",
appId = it.appId,
installSource = installedApp.installSource
)
}
_state.update {
it.copy(
isCheckingUpdates = false,
appsWithUpdates = appsWithUpdates
)
}
}
fun downloadAndUpdateApp(context: Context, app: AppWithUpdates) {
if (app.appId == null) return
viewModelScope.launch {
_state.update {
it.copy(
downloadingPackages = it.downloadingPackages + app.packageName,
downloadProgress = it.downloadProgress + (app.packageName to 0)
)
}
try {
val appFiles = withContext(Dispatchers.IO) {
AppModule.searchRepository.getAppFiles(
app.appId,
firstInstall = false,
supportedAbis = Build.SUPPORTED_ABIS.toList(),
withoutSplits = false,
deviceType = "mobile"
)
}
cancelDownloads[app.packageName] = apkInstaller.downloadFilesWithProgress(
appFiles,
onProgress = { percent, _, _ ->
_state.update {
it.copy(downloadProgress = it.downloadProgress + (app.packageName to percent))
}
},
onSuccess = { apkFiles ->
installApp(context, app.packageName, apkFiles)
},
onError = { _ ->
_state.update {
it.copy(
downloadingPackages = it.downloadingPackages - app.packageName,
downloadProgress = it.downloadProgress - app.packageName
)
}
},
onCancel = {
_state.update {
it.copy(
downloadingPackages = it.downloadingPackages - app.packageName,
downloadProgress = it.downloadProgress - app.packageName
)
}
}
)
} catch (e: Exception) {
_state.update {
it.copy(
downloadingPackages = it.downloadingPackages - app.packageName,
downloadProgress = it.downloadProgress - app.packageName
)
}
}
}
}
private fun installApp(context: Context, packageName: String, apkFiles: List<File>) {
viewModelScope.launch {
val installerCallback = object : PackageInstaller.SessionCallback() {
override fun onCreated(sessionId: Int) {}
override fun onBadgingChanged(sessionId: Int) {}
override fun onActiveChanged(sessionId: Int, active: Boolean) {}
override fun onProgressChanged(sessionId: Int, progress: Float) {}
override fun onFinished(sessionId: Int, success: Boolean) {
_state.update {
it.copy(
downloadingPackages = it.downloadingPackages - packageName,
downloadProgress = it.downloadProgress - packageName,
appsWithUpdates = if (success) {
it.appsWithUpdates.filter { app -> app.packageName != packageName }
} else it.appsWithUpdates
)
}
if (success) {
apkInstaller.cleanApkFiles(apkFiles.map { it.name })
}
}
}
apkInstaller.installSplitApk(context, apkFiles, installerCallback, onError = { _ ->
_state.update {
it.copy(
downloadingPackages = it.downloadingPackages - packageName,
downloadProgress = it.downloadProgress - packageName
)
}
})
}
}
fun cancelDownload(packageName: String) {
cancelDownloads[packageName]?.invoke()
cancelDownloads.remove(packageName)
}
private fun getInstalledApps(context: Context): List<AppUpdateRequestEntry> {
val pm = context.packageManager
val packages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getInstalledPackages(PackageManager.PackageInfoFlags.of(0))
} else {
@Suppress("DEPRECATION")
pm.getInstalledPackages(0)
}
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val allowedSources = prefs.getString("updates_sources", "")?.split(";") ?: listOf()
val allowAllSources = prefs.getBoolean("updates_check_all", false)
return packages
.filter { !isSystemApp(it) && (allowAllSources || allowedSources.contains(getInstallSource(pm, it.packageName))) }
.map { packageInfo ->
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
@Suppress("DEPRECATION")
packageInfo.versionCode.toLong()
}
println("package "+packageInfo.packageName+" from "+getInstallSource(pm, packageInfo.packageName))
AppUpdateRequestEntry(
packageName = packageInfo.packageName,
appName = pm.getApplicationLabel(packageInfo.applicationInfo!!).toString(),
versionCode = versionCode,
versionName = packageInfo.versionName ?: "Unknown",
installSource = getInstallSource(pm, packageInfo.packageName)
)
}
}
private fun isSystemApp(packageInfo: PackageInfo): Boolean {
return (packageInfo.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) ?: 0) != 0
}
private fun getInstallSource(pm: PackageManager, packageName: String): String {
val installer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
pm.getInstallSourceInfo(packageName).installingPackageName
} catch (e: Exception) {
null
}
} else {
@Suppress("DEPRECATION")
pm.getInstallerPackageName(packageName)
}
return installer ?: "null"
}
}

View file

@ -0,0 +1,18 @@
package dev.mi6e4ka.openstore.ui.screen.updates
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dev.mi6e4ka.openstore.internal.installer.ApkInstaller
class UpdatesViewModelFactory(
private val apkInstaller: ApkInstaller
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UpdatesViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return UpdatesViewModel(apkInstaller) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M424,664L696,392L654,350L424,580L306,462L264,504L424,664ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,840Q630,840 735,735Q840,630 840,480Q840,330 735,225Q630,120 480,120Q330,120 225,225Q120,330 120,480Q120,630 225,735Q330,840 480,840ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,800Q347,800 253.5,706.5Q160,613 160,480L200,480Q200,596 281,678Q362,760 480,760Q598,760 679,678Q760,596 760,480Q760,364 679,282Q598,200 480,200Q411,200 353,231Q295,262 254,314L360,314L360,354L174,354L174,168L214,168L214,270Q262,210 330.5,175Q399,140 480,140Q547,140 607,166.5Q667,193 712.5,238.5Q758,284 784.5,344Q811,404 811,472Q811,540 784.5,600Q758,660 712.5,705.5Q667,751 607,777.5Q547,804 480,800Z"/>
</vector>

View file

@ -41,4 +41,13 @@
<string name="settings_updates_check_all_title">Check for updates for all non-system applications</string>
<string name="settings_updates_check_all_description">!!! RuStore gets the full list of installed applications, it is not safe</string>
<string name="settings_save_button">Save</string>
<string name="updates_title">Updates</string>
<string name="check_updates">Check for updates</string>
<string name="checking_updates">Checking for updates…</string>
<string name="all_apps_updated">All apps are up to date</string>
<string name="available_updates">Available updates: %1$d</string>
<string name="check_updates_hint">Tap to check for updates</string>
<string name="exclude_google_play">Exclude Google Play</string>
<string name="updates_change_settings">Open settings</string>
<string name="updates_app_count">Applications checked: %1$d</string>
</resources>

View file

@ -1,5 +1,5 @@
<resources>
<string name="app_name" translatable="false">OpenStore</string>
<string name="app_name">OpenStore</string>
<string name="safety_card_text">О безопасности приложений</string>
<string name="error_message">Ошибка: %1$s</string>
<string name="provided_by">Предоставлено %1$s</string>
@ -42,4 +42,13 @@
<string name="settings_updates_check_all_title">Проверять обновления для всех не системных приложений</string>
<string name="settings_updates_check_all_description">!!! RuStore получает полный список установленных приложений, не безопасно</string>
<string name="settings_save_button">Сохранить</string>
<string name="updates_title">Обновления</string>
<string name="check_updates">Проверить обновления</string>
<string name="checking_updates">Проверка обновлений…</string>
<string name="all_apps_updated">Все приложения обновлены</string>
<string name="available_updates">Доступно обновлений: %1$d</string>
<string name="check_updates_hint">Нажмите для проверки обновлений</string>
<string name="exclude_google_play">Исключить Google Play</string>
<string name="updates_change_settings">Открыть настройки</string>
<string name="updates_app_count">Проверяется приложений: %1$d</string>
</resources>