commit 1a488425b2e11163acc606ca1c054a7834749ad6
parent 45f4c65ab336ccb78f55bdc9b5856e722f7d4e10
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Mon, 30 Oct 2023 00:13:56 +0100

feat(manager): snapchat patch section (wip)

Diffstat:
Mgradle/libs.versions.toml | 2++
Mmanager/build.gradle.kts | 1+
Mmanager/src/main/AndroidManifest.xml | 2+-
Amanager/src/main/kotlin/me/rhunk/snapenhance/manager/data/APKMirror.kt | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmanager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/LSPatch.kt | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mmanager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Navigation.kt | 22+++++++++++++++-------
Mmanager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Tab.kt | 7+++++++
Amanager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/components/Dialogs.kt | 38++++++++++++++++++++++++++++++++++++++
Mmanager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/HomeTab.kt | 4+++-
Amanager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/SnapchatPatchTab.kt | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amanager/src/main/res/drawable/sclogo.xml | 15+++++++++++++++
Amanager/src/main/res/values/themes.xml | 9+++++++++
12 files changed, 476 insertions(+), 25 deletions(-)

diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ agp = "8.1.2" apksig = "8.0.2" guava = "32.1.3-jre" +jsoup = "1.16.2" kotlin = "1.9.0" kotlinx-coroutines-android = "1.7.3" @@ -44,6 +45,7 @@ dexlib2 = { group = "org.smali", name = "dexlib2", version.ref = "dexlib2" } ffmpeg-kit = { group = "com.arthenica", name = "ffmpeg-kit-full-gpl", version.ref = "ffmpeg-kit" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } guava = { module = "com.google.guava:guava", version.ref = "guava" } +jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit = { module = "junit:junit", version.ref = "junit" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid-android" } diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts @@ -75,6 +75,7 @@ dependencies { implementation(libs.guava) implementation(libs.apksig) implementation(libs.gson) + implementation(libs.jsoup) implementation(libs.okhttp) implementation(libs.androidx.material3) implementation(libs.androidx.activity.ktx) diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ tools:targetApi="34" android:enableOnBackInvokedCallback="true" android:icon="@android:drawable/ic_input_add"> - <activity android:name=".ui.MainActivity" android:exported="true"> + <activity android:name=".ui.MainActivity" android:exported="true" android:theme="@style/AppTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/data/APKMirror.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/data/APKMirror.kt @@ -0,0 +1,76 @@ +package me.rhunk.snapenhance.manager.data + +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.Jsoup +import kotlin.math.absoluteValue + +data class DownloadItem( + val title: String, + val releaseDate: String, + val downloadPage: String +) { + val shortTitle = title.substringBefore("(").trim() + val hash = (title + releaseDate + downloadPage).hashCode().absoluteValue.toString(16) + val isBeta = title.contains("Beta", ignoreCase = true) +} + +class APKMirror { + val okhttpClient = OkHttpClient.Builder().addInterceptor { + it.proceed( + it.request().newBuilder() + .addHeader("User-Agent", System.getProperty("http.agent")!!) + .build() + ) + }.build() + + companion object { + private const val BASE_URL = "https://www.apkmirror.com" + private const val FETCH_BUILD_URL = "$BASE_URL/apk/snap-inc/snapchat/variant-%7B%22arches_slug%22%3A%5B%22arm64-v8a%22%2C%22armeabi-v7a%22%5D%2C%22dpis_slug%22%3A%5B%22nodpi%22%5D%7D/page/{page}/" + } + + fun fetchDownloadLink(downloadPageUri: String): String? { + okhttpClient.newCall( + Request.Builder() + .url("$BASE_URL$downloadPageUri") + .build() + ).execute().use { response -> + if (!response.isSuccessful) return null + val finalDownloadPageUri = Jsoup.parse(response.body.string()).getElementsByClass("downloadButton").first()?.attr("href") + + okhttpClient.newCall( + Request.Builder() + .url("$BASE_URL$finalDownloadPageUri") + .build() + ).execute().use { response2 -> + if (!response2.isSuccessful) return null + val document = Jsoup.parse(response2.body.string()) + val downloadForm = document.getElementById("filedownload") ?: return null + val arguments = downloadForm.childNodes().mapNotNull { + (it.attr("name") to it.attr("value")).takeIf { pair -> pair.second.isNotEmpty() } + } + return BASE_URL + downloadForm.attr("action") + "?" + arguments.joinToString("&") { "${it.first}=${it.second}" } + } + } + } + + fun fetchSnapchatVersions(page: Int = 1): List<DownloadItem>? { + val versions = mutableListOf<DownloadItem>() + okhttpClient.newCall( + Request.Builder() + .url(FETCH_BUILD_URL.replace("{page}", page.toString())) + .build() + ).execute().use { response -> + if (!response.isSuccessful) return null + val document = Jsoup.parse(response.body.string()) + document.getElementById("primary")?.getElementsByClass("appRow")?.forEach { app -> + val title = app.getElementsByTag("h5").first()?.attr("title") ?: return@forEach + val releaseDate = app.getElementsByClass("dateyear_utc").attr("data-utcdate") ?: return@forEach + val downloadPage = app.getElementsByClass("downloadLink").first()?.attr("href") ?: return@forEach + + versions.add(DownloadItem(title, releaseDate, downloadPage)) + } + } + return versions + } +}+ \ No newline at end of file diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/LSPatch.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/LSPatch.kt @@ -52,6 +52,44 @@ class LSPatch( }.toByteArray() } + private fun provideSigningExtension(): SigningExtension { + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + keyStore.load(context.assets.open("lspatch/keystore.jks"), "android".toCharArray()) + val key = keyStore.getEntry("androiddebugkey", KeyStore.PasswordProtection("android".toCharArray())) as KeyStore.PrivateKeyEntry + val certificates = key.certificateChain.mapNotNull { it as? X509Certificate }.toTypedArray() + + return SigningExtension( + SigningOptions.builder().apply { + setMinSdkVersion(28) + setV2SigningEnabled(true) + setCertificates(*certificates) + setKey(key.privateKey) + }.build() + ) + } + + private fun resignApk(inputApkFile: File, outputFile: File) { + printLog("Resigning ${inputApkFile.absolutePath} to ${outputFile.absolutePath}") + val dstZFile = ZFile.openReadWrite(outputFile, Z_FILE_OPTIONS) + val inZFile = ZFile.openReadOnly(inputApkFile) + + inZFile.entries().forEach { entry -> + dstZFile.add(entry.centralDirectoryHeader.name, entry.open()) + } + + // sign apk + runCatching { + provideSigningExtension().register(dstZFile) + }.onFailure { + throw Exception("Failed to sign apk", it) + } + + dstZFile.realign() + dstZFile.close() + inZFile.close() + printLog("Done") + } + @Suppress("UNCHECKED_CAST") @OptIn(ExperimentalEncodingApi::class) private fun patchApk(inputApkFile: File, outputFile: File) { @@ -70,19 +108,7 @@ class LSPatch( // sign apk runCatching { - val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) - keyStore.load(context.assets.open("lspatch/keystore.jks"), "android".toCharArray()) - val key = keyStore.getEntry("androiddebugkey", KeyStore.PasswordProtection("android".toCharArray())) as KeyStore.PrivateKeyEntry - SigningExtension( - SigningOptions.builder().apply { - setMinSdkVersion(28) - setV2SigningEnabled(true) - setCertificates(*(key.certificateChain as Array<X509Certificate>)) - setKey(key.privateKey) - }.build() - ).apply { - register(dstZFile) - } + provideSigningExtension().register(dstZFile) }.onFailure { throw Exception("Failed to sign apk", it) } @@ -99,7 +125,6 @@ class LSPatch( printLog("Adding config") dstZFile.add("assets/lspatch/config.json", ByteArrayInputStream(patchConfig.toByteArray())) - // add loader dex printLog("Adding dex files") dstZFile.add("classes.dex", context.assets.open("lspatch/dexes/metaloader.dex")) @@ -117,7 +142,6 @@ class LSPatch( dstZFile.add("assets/lspatch/modules/$packageName.apk", module.inputStream()) } - // link apk entries printLog("Linking apk entries") @@ -138,7 +162,22 @@ class LSPatch( printLog("Done") } - fun patch(input: File, outputFile: File) { + fun patchSplits(inputs: List<File>): List<File> { + val outputs = mutableListOf<File>() + inputs.forEach { input -> + val outputFile = File.createTempFile("patched", ".apk", context.cacheDir) + if (input.name.contains("split")) { + resignApk(input, outputFile) + outputs.add(outputFile) + return@forEach + } + patch(input, outputFile) + outputs.add(outputFile) + } + return outputs + } + + private fun patch(input: File, outputFile: File) { //check if input apk is already patched var isAlreadyPatched = false var inputFile = input diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Navigation.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Navigation.kt @@ -8,10 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -31,15 +28,26 @@ class Navigation( private val tabs: List<Tab>, private val defaultTab: KClass<out Tab> ) { - + @OptIn(ExperimentalMaterial3Api::class) @Composable fun TopBar() { - + val navBackStackEntry by navHostController.currentBackStackEntryAsState() + val currentTab = tabs.firstOrNull { it.route == navBackStackEntry?.destination?.route } + TopAppBar(title = { + Text(text = currentTab?.route ?: "") + }, navigationIcon = { + currentTab?.icon?.let { + Icon(imageVector = it, contentDescription = null) + } + }, actions = { + currentTab?.TopBar() + }) } @Composable fun FloatingActionButtons() { - + val navBackStackEntry by navHostController.currentBackStackEntryAsState() + tabs.firstOrNull { it.route == navBackStackEntry?.destination?.route }?.FloatingActionButtons() } fun navigateTo(tab: KClass<out Tab>) { diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Tab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Tab.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.manager.ui import android.os.Bundle +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector @@ -46,4 +47,10 @@ open class Tab( @Composable open fun Content() {} + + fun toast(message: String) { + activity.runOnUiThread { + Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() + } + } } \ No newline at end of file diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/components/Dialogs.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/components/Dialogs.kt @@ -0,0 +1,37 @@ +package me.rhunk.snapenhance.manager.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + + +@Composable +fun ConfirmationDialog(title: String, onDismiss: () -> Unit, onConfirm: () -> Unit) { + Card { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text(title) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Button(onClick = { onDismiss() }) { + Text("Cancel") + } + Button(onClick = { onConfirm() }) { + Text("Yes") + } + } + } + } +}+ \ No newline at end of file diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/HomeTab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/HomeTab.kt @@ -29,11 +29,13 @@ import me.rhunk.snapenhance.manager.BuildConfig import me.rhunk.snapenhance.manager.lspatch.config.Constants import me.rhunk.snapenhance.manager.ui.Tab import me.rhunk.snapenhance.manager.ui.tab.download.SEDownloadTab +import me.rhunk.snapenhance.manager.ui.tab.download.SnapchatPatchTab class HomeTab : Tab("home", true, icon = Icons.Default.Home) { override fun init(activity: ComponentActivity) { super.init(activity) registerNestedTab(SEDownloadTab::class) + registerNestedTab(SnapchatPatchTab::class) } @Composable @@ -73,7 +75,7 @@ class HomeTab : Tab("home", true, icon = Icons.Default.Home) { Text(text = "Patched", fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurface) } OutlinedButton(onClick = { - + navigation.navigateTo(SnapchatPatchTab::class) }) { Text(text = if (isLSPatched) "Repatch" else "Patch") } diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/SnapchatPatchTab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/SnapchatPatchTab.kt @@ -0,0 +1,252 @@ +package me.rhunk.snapenhance.manager.ui.tab.download + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material.icons.filled.Download +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.rhunk.snapenhance.manager.R +import me.rhunk.snapenhance.manager.data.APKMirror +import me.rhunk.snapenhance.manager.data.DownloadItem +import me.rhunk.snapenhance.manager.ui.Tab +import me.rhunk.snapenhance.manager.ui.components.ConfirmationDialog + +@OptIn(ExperimentalMaterial3Api::class) +class SnapchatPatchTab : Tab("snapchat_download_tab") { + private val apkCache by lazy { + activity.cacheDir.resolve("snapchat_apk_cache").also { + if (!it.exists()) it.mkdirs() + } + } + private val apkMirror = APKMirror() + private val cachedDownloadItems = mutableListOf<DownloadItem>() + private var currentPage by mutableIntStateOf(1) + + private fun isDownloaded(hash: String): Boolean { + return apkCache.resolve("${hash}.apk").exists() + } + + private fun deleteDownload(hash: String) { + runCatching { + apkCache.resolve("${hash}.apk").delete() + }.onFailure { + it.printStackTrace() + toast("Failed to delete download. It may have already been deleted.") + } + } + + private suspend fun downloadSnapchatVersion(downloadItem: DownloadItem, onProgress: (Float) -> Unit, onSuccess: suspend () -> Unit) { + withContext(Dispatchers.IO) { + toast("Downloading ${downloadItem.title}...") + val downloadLink = apkMirror.fetchDownloadLink(downloadItem.downloadPage) ?: run { + toast("Failed to fetch download link") + return@withContext + } + + val downloadResponse = apkMirror.okhttpClient.newCall( + okhttp3.Request.Builder() + .url(downloadLink) + .build() + ).execute() + + if (!downloadResponse.isSuccessful) { + toast("Failed to download") + return@withContext + } + + val apkFile = apkCache.resolve("${downloadItem.hash}.apk") + apkFile.outputStream().use { outputStream -> + runCatching { + downloadResponse.body.byteStream().use { inputStream -> + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var read: Int + var totalRead = 0L + val totalSize = downloadResponse.body.contentLength() + while (inputStream.read(buffer).also { read = it } != -1) { + outputStream.write(buffer, 0, read) + totalRead += read + onProgress(totalRead.toFloat() / totalSize.toFloat()) + } + } + }.onFailure { + it.printStackTrace() + toast("Failed to save apk") + return@withContext + } + } + + withContext(Dispatchers.Main) { + onSuccess() + } + } + } + + @Composable + override fun TopBar() { + var deleteAllDialog by remember { mutableStateOf(false) } + IconButton(onClick = { deleteAllDialog = true }) { + Icon(imageVector = Icons.Default.DeleteForever, contentDescription = null) + } + + if (deleteAllDialog) { + AlertDialog(onDismissRequest = { deleteAllDialog = false }) { + ConfirmationDialog(title = "Are you sure you want to delete all downloads?", onDismiss = { deleteAllDialog = false }) { + deleteAllDialog = false + runCatching { + apkCache.listFiles()?.forEach { it.deleteRecursively() } + }.onFailure { + toast("Failed to delete downloads") + it.printStackTrace() + } + toast("Done!") + } + } + } + } + + @Composable + private fun DownloadItemRow(coroutineScope: CoroutineScope, item: DownloadItem) { + var isDownloading by remember { mutableStateOf(false) } + var downloadProgress by remember { mutableFloatStateOf(-1f) } + var isDownloaded by remember { mutableStateOf(isDownloaded(item.hash)) } + + var showDeleteCurrentDownloadDialog by remember { mutableStateOf(false) } + + if (showDeleteCurrentDownloadDialog) { + AlertDialog(onDismissRequest = { showDeleteCurrentDownloadDialog = false }) { + ConfirmationDialog(title = "Are you sure you want to delete this download?", onDismiss = { showDeleteCurrentDownloadDialog = false }) { + coroutineScope.launch { + deleteDownload(item.hash) + isDownloaded = false + } + showDeleteCurrentDownloadDialog = false + } + } + } + + ElevatedCard( + modifier = Modifier.padding(10.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.padding(10.dp), + ) { + Icon(painter = painterResource(R.drawable.sclogo), contentDescription = null, tint = MaterialTheme.colorScheme.onSurface, modifier = Modifier.size(40.dp)) + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Text(item.shortTitle) + Text(item.releaseDate) + if (!item.isBeta) { + Text("Recommended", color = MaterialTheme.colorScheme.tertiary) + } + } + Row( + modifier = Modifier.padding(5.dp), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + if (!isDownloaded) { + if (isDownloading) { + if (downloadProgress != -1f) { + CircularProgressIndicator(progress = downloadProgress, modifier = Modifier.size(40.dp)) + } else { + CircularProgressIndicator(modifier = Modifier.size(40.dp)) + } + } else { + FilledIconButton(onClick = { + coroutineScope.launch { + isDownloading = true + downloadProgress = -1f + downloadSnapchatVersion(item, onProgress = { + downloadProgress = it + }, onSuccess = { isDownloaded = true }) + isDownloading = false + } + }) { + Icon(imageVector = Icons.Default.Download, contentDescription = null) + } + } + } else { + Button(onClick = { /*TODO*/ }) { + Text("Patch") + } + IconButton(onClick = { showDeleteCurrentDownloadDialog = true }) { + Icon(imageVector = Icons.Default.Delete, contentDescription = null) + } + } + } + } + } + } + + @Composable + override fun Content() { + val coroutineScope = rememberCoroutineScope() + var isFetching by remember { mutableStateOf(false) } + val downloadItems = remember { cachedDownloadItems.toMutableStateList() } + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text("Select a version to download and patch") + LazyColumn { + items(downloadItems, key = { it.hash }) { item -> + DownloadItemRow(coroutineScope, item) + } + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(modifier = Modifier.alpha(if (isFetching) 1f else 0f)) + } + SideEffect { + if (isFetching) return@SideEffect + isFetching = true + coroutineScope.launch { + runCatching { + withContext(Dispatchers.IO) { + apkMirror.fetchSnapchatVersions(currentPage)?.let { + withContext(Dispatchers.Main) { + cachedDownloadItems.addAll(it) + downloadItems.addAll(it) + } + } + } + }.onFailure { + it.printStackTrace() + } + ++currentPage + isFetching = false + } + } + } + } + } + } +}+ \ No newline at end of file diff --git a/manager/src/main/res/drawable/sclogo.xml b/manager/src/main/res/drawable/sclogo.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:width="200dp" + android:height="200dp" + android:viewportWidth="1024" + android:viewportHeight="1009"> + <path + android:fillColor="#FF000000" + android:pathData="M992.5,748.4c-4.2,-13.9 -24.3,-23.7 -24.3,-23.7 -1.9,-1 -3.6,-1.9 -5,-2.6 -33.5,-16.2 -63.2,-35.7 -88.2,-57.8 -20.1,-17.8 -37.3,-37.4 -51.1,-58.2 -16.9,-25.4 -24.8,-46.6 -28.2,-58.1 -1.9,-7.5 -1.6,-10.5 0,-14.4 1.3,-3.3 5.2,-6.4 7,-7.9 11.3,-8 29.5,-19.8 40.7,-27 9.7,-6.3 18,-11.7 22.9,-15.1 15.7,-11 26.5,-22.2 32.8,-34.3 8.2,-15.6 9.2,-32.8 2.8,-49.7 -8.6,-22.8 -29.9,-36.4 -57,-36.4 -6,0 -12.2,0.7 -18.4,2 -15.5,3.4 -30.2,8.9 -42.5,13.7 -0.9,0.4 -1.9,-0.3 -1.8,-1.3 1.3,-30.5 2.8,-71.5 -0.6,-110.4 -3,-35.2 -10.3,-64.9 -22.1,-90.8 -11.9,-26 -27.4,-45.2 -39.5,-59.1 -11.5,-13.2 -31.8,-32.7 -62.4,-50.2 -43,-24.6 -92,-37.1 -145.6,-37.1 -53.5,0 -102.4,12.5 -145.5,37.1 -32.4,18.5 -53.1,39.4 -62.5,50.2 -12.1,13.9 -27.6,33.1 -39.5,59.1 -11.9,25.9 -19.1,55.5 -22.1,90.8 -3.4,39.1 -2,76.8 -0.6,110.4 0,1 -0.9,1.7 -1.9,1.3 -12.3,-4.8 -27,-10.3 -42.5,-13.7 -6.1,-1.3 -12.3,-2 -18.4,-2 -27,0 -48.3,13.6 -57,36.4 -6.4,16.9 -5.4,34.1 2.8,49.7 6.4,12.1 17.1,23.3 32.8,34.3 4.8,3.4 13.2,8.8 22.9,15.1 10.9,7.1 28.6,18.6 40,26.5 1.4,1 6.2,4.6 7.7,8.4 1.6,4 1.9,7 -0.2,15 -3.5,11.6 -11.4,32.6 -28,57.5 -13.8,20.9 -31,40.4 -51.1,58.2 -25,22.1 -54.7,41.6 -88.2,57.8 -1.6,0.8 -3.5,1.7 -5.5,2.9 0,0 -20,10.2 -23.8,23.4 -5.6,19.5 9.3,37.8 24.4,47.6 24.8,16 55,24.6 72.5,29.3 4.9,1.3 9.3,2.5 13.3,3.7 2.5,0.8 8.8,3.2 11.5,6.7 3.4,4.4 3.8,9.8 5,15.9 1.9,10.3 6.2,23 18.9,31.8 14,9.6 31.7,10.3 54.2,11.2 23.5,0.9 52.7,2 86.2,13.1 15.5,5.1 29.6,13.8 45.8,23.8 34,20.9 76.3,46.9 148.5,46.9 72.3,0 114.9,-26.1 149.1,-47.1 16.2,-9.9 30.1,-18.5 45.3,-23.5 33.5,-11.1 62.7,-12.2 86.2,-13.1 22.5,-0.9 40.2,-1.5 54.2,-11.2 13.6,-9.4 17.5,-23.4 19.3,-33.9 1,-5.2 1.6,-9.9 4.6,-13.7 2.6,-3.3 8.4,-5.6 11.1,-6.5 4.1,-1.3 8.7,-2.5 13.8,-3.9 17.5,-4.7 39.5,-10.2 66.2,-25.3 32.2,-18.3 34.4,-40.7 31,-51.8z" + tools:ignore="VectorPath" /> + <path + android:fillColor="#FF000000" + android:pathData="M1020.3,737.6c-7.1,-19.4 -20.7,-29.7 -36.1,-38.3 -2.9,-1.7 -5.6,-3.1 -7.8,-4.1 -4.6,-2.4 -9.3,-4.7 -14,-7.1 -48.1,-25.5 -85.7,-57.7 -111.7,-95.8 -8.8,-12.9 -14.9,-24.5 -19.2,-34 -2.2,-6.4 -2.1,-10 -0.5,-13.3 1.2,-2.5 4.4,-5.1 6.2,-6.4 8.3,-5.5 16.8,-11 22.6,-14.7 10.3,-6.7 18.5,-12 23.7,-15.6 19.8,-13.8 33.6,-28.5 42.2,-44.9 12.2,-23.1 13.7,-49.5 4.3,-74.3 -13,-34.4 -45.6,-55.8 -85,-55.8 -8.2,0 -16.5,0.9 -24.7,2.7 -2.2,0.5 -4.3,1 -6.4,1.5 0.4,-23.4 -0.2,-48.4 -2.3,-72.8 -7.4,-86 -37.5,-131.1 -68.9,-167 -13.1,-15 -35.9,-36.9 -70.1,-56.5 -47.7,-27.4 -101.7,-41.2 -160.6,-41.2 -58.7,0 -112.7,13.8 -160.4,41.1 -34.4,19.6 -57.2,41.6 -70.2,56.5 -31.4,35.9 -61.5,81 -68.9,167 -2.1,24.4 -2.6,49.4 -2.3,72.8 -2.1,-0.5 -4.3,-1 -6.4,-1.5 -8.2,-1.8 -16.6,-2.7 -24.7,-2.7 -39.4,0 -72,21.4 -85,55.8 -9.4,24.8 -7.9,51.2 4.3,74.3 8.6,16.4 22.5,31.1 42.2,44.9 5.3,3.7 13.4,9 23.7,15.6 5.6,3.6 13.7,8.9 21.7,14.2 1.2,0.8 5.5,4 7,7 1.7,3.4 1.7,7.1 -0.8,13.9 -4.2,9.3 -10.3,20.7 -18.9,33.3 -25.5,37.3 -62,68.9 -108.5,94.1 -24.7,13.1 -50.3,21.8 -61.1,51.2 -8.2,22.2 -2.8,47.5 17.9,68.8 6.8,7.3 15.4,13.8 26.2,19.8 25.4,14 47,20.9 64,25.6 3,0.9 9.9,3.1 12.9,5.8 7.6,6.6 6.5,16.6 16.6,31.2 6.1,9.1 13.1,15.3 18.9,19.3 21.1,14.6 44.9,15.5 70.1,16.5 22.7,0.9 48.5,1.9 77.9,11.6 12.2,4 24.9,11.8 39.5,20.8 35.2,21.7 83.5,51.3 164.2,51.3 80.8,0 129.3,-29.8 164.8,-51.5 14.6,-8.9 27.2,-16.7 39,-20.6 29.4,-9.7 55.2,-10.7 77.9,-11.6 25.2,-1 48.9,-1.9 70.1,-16.5 6.6,-4.6 15,-12.1 21.6,-23.5 7.2,-12.3 7.1,-21 13.9,-26.9 2.8,-2.4 8.9,-4.5 12.2,-5.5 17.1,-4.7 39,-11.6 64.9,-25.9 11.5,-6.3 20.4,-13.2 27.5,-21.1l0.3,-0.3c19.3,-21 24.2,-45.5 16.2,-67.2zM948.6,776.1c-43.8,24.2 -72.9,21.6 -95.5,36.1 -19.2,12.4 -7.9,39.1 -21.8,48.7 -17.2,11.9 -67.9,-0.8 -133.4,20.8 -54,17.9 -88.5,69.2 -185.8,69.2 -97.5,0 -131,-51.1 -185.8,-69.2 -65.5,-21.6 -116.3,-8.9 -133.4,-20.8 -13.9,-9.6 -2.6,-36.3 -21.8,-48.7 -22.6,-14.6 -51.7,-12 -95.5,-36.1 -27.9,-15.4 -12.1,-24.9 -2.8,-29.4 158.6,-76.7 183.8,-195.3 185,-204.2 1.4,-10.6 2.9,-19 -8.8,-29.9 -11.3,-10.5 -61.6,-41.6 -75.5,-51.3 -23.1,-16.1 -33.2,-32.2 -25.7,-52 5.2,-13.7 18,-18.8 31.5,-18.8 4.2,0 8.5,0.5 12.6,1.4 25.3,5.5 49.9,18.2 64.1,21.6 2,0.5 3.7,0.7 5.2,0.7 7.6,0 10.2,-3.8 9.7,-12.5 -1.6,-27.7 -5.6,-81.7 -1.2,-132.2 6,-69.4 28.4,-103.8 55,-134.3 12.8,-14.6 72.8,-78 187.5,-78 115,0 174.7,63.4 187.5,78 26.6,30.4 49,64.8 55,134.3 4.4,50.5 0.6,104.5 -1.2,132.2 -0.6,9.1 2.2,12.5 9.7,12.5 1.5,0 3.3,-0.2 5.2,-0.7 14.2,-3.4 38.8,-16.1 64.1,-21.6 4.1,-0.9 8.4,-1.4 12.6,-1.4 13.5,0 26.3,5.2 31.5,18.8 7.5,19.8 -2.7,35.9 -25.7,52 -13.9,9.7 -64.2,40.8 -75.5,51.3 -11.7,10.8 -10.2,19.2 -8.8,29.9 1.1,8.9 26.4,127.5 185,204.2 9,4.5 24.9,14 -3,29.4z" + tools:ignore="VectorPath" /> +</vector> diff --git a/manager/src/main/res/values/themes.xml b/manager/src/main/res/values/themes.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<resources> + <style name="AppTheme"> + <item name="android:windowNoTitle">true</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:alertDialogTheme">@android:style/Theme.DeviceDefault.Dialog.Alert</item> + </style> +</resources>+ \ No newline at end of file