mirror of
https://codeberg.org/mi6e4ka/openstore.git
synced 2026-07-09 15:59:50 +00:00
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:
commit
0cc49e1ad0
14 changed files with 746 additions and 1 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ fun SettingsScreen(
|
|||
SettingsSectionSelect(
|
||||
title = "Google Play",
|
||||
value = "com.android.vending"
|
||||
),
|
||||
SettingsSectionSelect(
|
||||
title = "Manual Installed",
|
||||
value = "com.android.packageinstaller"
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
10
app/src/main/res/drawable/ic_check_circle_24px.xml
Normal file
10
app/src/main/res/drawable/ic_check_circle_24px.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/ic_refresh_24px.xml
Normal file
10
app/src/main/res/drawable/ic_refresh_24px.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue