mirror of
https://codeberg.org/mi6e4ka/openstore.git
synced 2026-07-09 15:59:50 +00:00
fix/chore: some fixes and move installion logic out from ViewModels
This commit is contained in:
parent
47384b8af8
commit
6708c57959
8 changed files with 194 additions and 179 deletions
|
|
@ -4,9 +4,14 @@ import android.app.PendingIntent
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import dev.mi6e4ka.openstore.R
|
||||
import dev.mi6e4ka.openstore.data.model.DownloadLinkResponseItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.io.File
|
||||
|
|
@ -18,8 +23,12 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.concurrent.atomics.AtomicLong
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
|
@ -123,11 +132,6 @@ class ApkInstaller(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun isApkAlreadyDownloaded(fileName: String): Boolean {
|
||||
val apkFile = getApkFile(fileName)
|
||||
return apkFile.exists() && apkFile.length() > 0
|
||||
}
|
||||
|
||||
fun getApkFile(fileName: String): File {
|
||||
return File(context.externalCacheDir ?: context.cacheDir, fileName)
|
||||
}
|
||||
|
|
@ -140,33 +144,59 @@ class ApkInstaller(private val context: Context) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkFileSize(fileName: String): Long {
|
||||
return getApkFile(fileName).length()
|
||||
}
|
||||
|
||||
private val installer = context.packageManager.packageInstaller
|
||||
private val flags = PendingIntent.FLAG_MUTABLE
|
||||
private val intent = Intent(context, PackageInstallerStatusReceiver::class.java)
|
||||
private val sessionParams =
|
||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||
|
||||
fun installApp(
|
||||
packageName: String,
|
||||
apkFiles: List<File>,
|
||||
onSessionFinished: (Boolean) -> Unit = {},
|
||||
onError: (Exception) -> Unit = {}
|
||||
) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val installerType = prefs.getString("installer_type", "session")
|
||||
|
||||
when (installerType) {
|
||||
"session" -> {
|
||||
installViaSession(apkFiles, onSessionFinished, onError)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
setRequestUpdateOwnership(true)
|
||||
"intent" -> {
|
||||
installViaIntent(packageName, apkFiles, onError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun installSplitApk(context: Context, apkFiles: List<File>, installerCallback: PackageInstaller.SessionCallback, onError: (e: Exception) -> Unit) {
|
||||
val sessionId = installer.createSession(sessionParams)
|
||||
installer.registerSessionCallback(
|
||||
installerCallback,
|
||||
Handler(Looper.getMainLooper())
|
||||
)
|
||||
|
||||
val session = installer.openSession(sessionId)
|
||||
private fun installViaSession(
|
||||
apkFiles: List<File>,
|
||||
onSessionFinished: (Boolean) -> Unit,
|
||||
onError: (Exception) -> Unit
|
||||
) {
|
||||
try {
|
||||
val sessionParams =
|
||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
setRequestUpdateOwnership(true)
|
||||
}
|
||||
}
|
||||
val sessionId = installer.createSession(sessionParams)
|
||||
installer.registerSessionCallback(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(id: Int, success: Boolean) {
|
||||
if (id == sessionId) {
|
||||
onSessionFinished(success)
|
||||
installer.unregisterSessionCallback(this)
|
||||
}
|
||||
}
|
||||
}, Handler(Looper.getMainLooper()))
|
||||
|
||||
val session = installer.openSession(sessionId)
|
||||
session.use { activeSession ->
|
||||
apkFiles.forEach { file ->
|
||||
val sizeBytes = file.length()
|
||||
|
|
@ -181,9 +211,59 @@ class ApkInstaller(private val context: Context) {
|
|||
activeSession.commit(pendingIntent.intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
installer.abandonSession(sessionId)
|
||||
Log.e("ApkInstaller", "Error during session install", e)
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private fun installViaIntent(
|
||||
packageName: String,
|
||||
apkFiles: List<File>,
|
||||
onError: (Exception) -> Unit
|
||||
) {
|
||||
try {
|
||||
val apkUri: Uri
|
||||
if (apkFiles.size == 1) {
|
||||
apkUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.provider",
|
||||
apkFiles[0]
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.intent_installer_split_apk_build, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
val apksFile = File(context.externalCacheDir ?: context.cacheDir, "$packageName.apks")
|
||||
buildSplitApk(apkFiles, apksFile)
|
||||
apkUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.provider",
|
||||
apksFile
|
||||
)
|
||||
}
|
||||
Toast.makeText(context, R.string.intent_installer_warning, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ApkInstaller", "Error during intent install", e)
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSplitApk(apkFiles: List<File>, outputZip: File) {
|
||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZip))).use { zos ->
|
||||
apkFiles.forEach { file ->
|
||||
FileInputStream(file).use { fis ->
|
||||
val entry = ZipEntry(file.name)
|
||||
zos.putNextEntry(entry)
|
||||
fis.copyTo(zos)
|
||||
zos.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,14 @@ class PackageInstallerStatusReceiver : BroadcastReceiver() {
|
|||
}
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(context, if (isUninstall) R.string.failed_uninstall else R.string.failed_install, Toast.LENGTH_SHORT).show()
|
||||
val errorMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
|
||||
if (errorMessage?.contains("INSUFFICIENT_STORAGE", ignoreCase = true) == true) {
|
||||
Toast.makeText(context, R.string.insufficient_memory, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, if (isUninstall) R.string.failed_uninstall else R.string.failed_install, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
Log.e("app manager", errorMessage ?: "no error message")
|
||||
}
|
||||
}
|
||||
|
|
@ -44,4 +50,4 @@ class PackageInstallerStatusReceiver : BroadcastReceiver() {
|
|||
companion object {
|
||||
const val ACTION_UNINSTALL = "action_uninstall"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,11 @@ import android.content.ClipboardManager
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
|
@ -32,11 +29,6 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import androidx.core.net.toUri
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
data class DetailsState(
|
||||
val isLoading: Boolean = true,
|
||||
|
|
@ -153,99 +145,40 @@ class DetailsViewModel(private val appPlatform: String, private val apkInstaller
|
|||
cancelDownload = null
|
||||
}
|
||||
|
||||
fun buildSplitApk(apkFiles: List<File>, outputZip: File) {
|
||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZip))).use { zos ->
|
||||
apkFiles.forEach { file ->
|
||||
FileInputStream(file).use { fis ->
|
||||
val entry = ZipEntry(file.name)
|
||||
zos.putNextEntry(entry)
|
||||
fis.copyTo(zos)
|
||||
zos.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun installApp(context: Context, apkFiles: List<File>) {
|
||||
viewModelScope.launch {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val installerType = prefs.getString("installer_type", "session")
|
||||
when(installerType) {
|
||||
"session" ->
|
||||
{
|
||||
_state.update { it.copy(installStatus = InstallStatus.Pending) }
|
||||
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(installStatus = InstallStatus.Success, isDownloading = false)}
|
||||
if (success) {
|
||||
_state.update { it.copy(isInstalled = true, isNeedUpgrade = false) }
|
||||
apkInstaller.cleanApkFiles(apkFiles.map {it.name})
|
||||
}
|
||||
}
|
||||
}
|
||||
apkInstaller.installSplitApk(
|
||||
context,
|
||||
apkFiles,
|
||||
installerCallback,
|
||||
onError = { error ->
|
||||
if (error.message?.contains("Failed to allocate") == true) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.insufficient_memory,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
println("apk installer $error")
|
||||
Toast.makeText(context, R.string.installing_error, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
_state.update {
|
||||
it.copy(
|
||||
installStatus = InstallStatus.Idle,
|
||||
isDownloading = false
|
||||
)
|
||||
}
|
||||
})
|
||||
val packageName = state.value.item?.packageName ?: "tmp"
|
||||
_state.update { it.copy(installStatus = InstallStatus.Pending) }
|
||||
|
||||
apkInstaller.installApp(
|
||||
packageName = packageName,
|
||||
apkFiles = apkFiles,
|
||||
onSessionFinished = { success ->
|
||||
_state.update { it.copy(installStatus = InstallStatus.Success, isDownloading = false)}
|
||||
if (success) {
|
||||
_state.update { it.copy(isInstalled = true, isNeedUpgrade = false) }
|
||||
apkInstaller.cleanApkFiles(apkFiles.map {it.name})
|
||||
}
|
||||
"intent" -> {
|
||||
_state.update { it.copy(installStatus = InstallStatus.Idle, isDownloading = false) }
|
||||
val apkUri : Uri
|
||||
if (apkFiles.size == 1) {
|
||||
apkUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.provider",
|
||||
apkFiles[0]
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.intent_installer_split_apk_build, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
val apksFile = File(context.externalCacheDir ?: context.cacheDir, "${state.value.item?.packageName ?: "tmp"}.apks")
|
||||
buildSplitApk(apkFiles, apksFile)
|
||||
apkUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.provider",
|
||||
apksFile
|
||||
)
|
||||
}
|
||||
Toast.makeText(context, R.string.intent_installer_warning, Toast.LENGTH_SHORT)
|
||||
},
|
||||
onError = { error ->
|
||||
if (error.message?.contains("Failed to allocate") == true) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.insufficient_memory,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
println("apk installer $error")
|
||||
Toast.makeText(context, R.string.installing_error, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(
|
||||
apkUri,
|
||||
"application/vnd.android.package-archive"
|
||||
)
|
||||
flags =
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
_state.update {
|
||||
it.copy(
|
||||
installStatus = InstallStatus.Idle,
|
||||
isDownloading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPackageInfo(context: Context, packageName: String): PackageInfo? {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -96,7 +97,7 @@ fun SettingsScreen(
|
|||
select = listOf(
|
||||
SettingsSectionSelect(
|
||||
title = "OpenStore",
|
||||
value = "dev.mi6e4ka.dev"
|
||||
value = "dev.mi6e4ka.openstore"
|
||||
),
|
||||
SettingsSectionSelect(
|
||||
title = "RuStore",
|
||||
|
|
@ -162,7 +163,7 @@ fun SettingsCard(
|
|||
if (item.type == ParamType.SWITCH) prefs.getBoolean(item.id, false) else false
|
||||
)}
|
||||
|
||||
ElevatedCard (
|
||||
Card (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ fun UpdatesScreen(navController: NavController) {
|
|||
Icon(
|
||||
painter = painterResource(R.drawable.ic_check_circle_24px),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
|
@ -278,13 +278,13 @@ fun UpdateAppCard(
|
|||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = "from ${app.installSource}",
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ 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 android.widget.Toast
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.mi6e4ka.openstore.R
|
||||
import dev.mi6e4ka.openstore.data.model.AppUpdateRequestEntry
|
||||
import dev.mi6e4ka.openstore.di.AppModule
|
||||
import dev.mi6e4ka.openstore.internal.installer.ApkInstaller
|
||||
|
|
@ -172,13 +173,8 @@ class UpdatesViewModel(private val apkInstaller: ApkInstaller) : ViewModel() {
|
|||
onSuccess = { apkFiles ->
|
||||
installApp(context, app.packageName, apkFiles)
|
||||
},
|
||||
onError = { _ ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
downloadingPackages = it.downloadingPackages - app.packageName,
|
||||
downloadProgress = it.downloadProgress - app.packageName
|
||||
)
|
||||
}
|
||||
onError = { error ->
|
||||
handleError(context, app.packageName, error)
|
||||
},
|
||||
onCancel = {
|
||||
_state.update {
|
||||
|
|
@ -190,47 +186,46 @@ class UpdatesViewModel(private val apkInstaller: ApkInstaller) : ViewModel() {
|
|||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
downloadingPackages = it.downloadingPackages - app.packageName,
|
||||
downloadProgress = it.downloadProgress - app.packageName
|
||||
)
|
||||
}
|
||||
handleError(context, app.packageName, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = { _ ->
|
||||
apkInstaller.installApp(
|
||||
packageName = packageName,
|
||||
apkFiles = apkFiles,
|
||||
onSessionFinished = { success ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
downloadingPackages = it.downloadingPackages - packageName,
|
||||
downloadProgress = it.downloadProgress - 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 })
|
||||
}
|
||||
},
|
||||
onError = { error ->
|
||||
handleError(context, packageName, error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleError(context: Context, packageName: String, error: Throwable) {
|
||||
if (error.message?.contains("Failed to allocate") == true) {
|
||||
Toast.makeText(context, R.string.insufficient_memory, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, R.string.loading_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
_state.update {
|
||||
it.copy(
|
||||
downloadingPackages = it.downloadingPackages - packageName,
|
||||
downloadProgress = it.downloadProgress - packageName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
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"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M424,664L706,382L650,326L424,552L310,438L254,494L424,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,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,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>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
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"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,800Q346,800 253,707Q160,614 160,480Q160,346 253,253Q346,160 480,160Q549,160 612,188.5Q675,217 720,270L720,160L800,160L800,440L520,440L520,360L688,360Q656,304 600.5,272Q545,240 480,240Q380,240 310,310Q240,380 240,480Q240,580 310,650Q380,720 480,720Q557,720 619,676Q681,632 706,560L790,560Q762,666 676,733Q590,800 480,800Z"/>
|
||||
</vector>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue