├── app ├── .gitignore ├── gradle.properties ├── src │ └── main │ │ ├── res │ │ ├── play_store_512.png │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ ├── values │ │ │ ├── themes.xml │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ ├── drawable │ │ │ └── launch_background.xml │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ ├── xml │ │ │ ├── backup_rules.xml │ │ │ └── data_extraction_rules.xml │ │ ├── values-night │ │ │ ├── styles.xml │ │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ │ └── strings.xml │ │ ├── values-fa │ │ │ └── strings.xml │ │ └── values-zh-rCN │ │ │ └── strings.xml │ │ ├── java │ │ ├── io │ │ │ └── github │ │ │ │ └── samolego │ │ │ │ └── canta │ │ │ │ ├── ui │ │ │ │ ├── navigation │ │ │ │ │ └── Screen.kt │ │ │ │ ├── theme │ │ │ │ │ ├── Color.kt │ │ │ │ │ ├── Type.kt │ │ │ │ │ └── Theme.kt │ │ │ │ ├── component │ │ │ │ │ ├── IconClickButton.kt │ │ │ │ │ ├── fab │ │ │ │ │ │ ├── PresetEditFAB.kt │ │ │ │ │ │ └── ExpandableFAB.kt │ │ │ │ │ ├── ScreenTopBar.kt │ │ │ │ │ ├── AppBadge.kt │ │ │ │ │ ├── Dropdown.kt │ │ │ │ │ └── SettingsItem.kt │ │ │ │ ├── menu │ │ │ │ │ ├── MoreOptionsMenu.kt │ │ │ │ │ └── FiltersMenu.kt │ │ │ │ ├── dialog │ │ │ │ │ ├── ExplainBadgesDialog.kt │ │ │ │ │ ├── UninstallDialog.kt │ │ │ │ │ ├── NoWarrantyDialog.kt │ │ │ │ │ ├── SuccessDialog.kt │ │ │ │ │ └── preset │ │ │ │ │ │ └── PresetEditDialog.kt │ │ │ │ ├── screen │ │ │ │ │ └── LogsPage.kt │ │ │ │ └── viewmodel │ │ │ │ │ └── PresetsViewModel.kt │ │ │ │ ├── CantaApplication.kt │ │ │ │ ├── util │ │ │ │ ├── CantaPresetData.kt │ │ │ │ ├── LogUtils.kt │ │ │ │ ├── UninstallLock.kt │ │ │ │ ├── apps │ │ │ │ │ ├── Filter.kt │ │ │ │ │ └── AppInfo.kt │ │ │ │ ├── shizuku │ │ │ │ │ ├── ShizukuPermission.kt │ │ │ │ │ └── ShizukuPackageInstallerUtils.kt │ │ │ │ ├── CustomTextSelectionCallback.kt │ │ │ │ └── BloatUtils.kt │ │ │ │ ├── extension │ │ │ │ ├── ToastExtension.kt │ │ │ │ ├── MutableStateSet.kt │ │ │ │ └── PackageManagerExt.kt │ │ │ │ └── data │ │ │ │ ├── AppSettingsSerializer.kt │ │ │ │ └── SettingsStore.kt │ │ └── android │ │ │ └── content │ │ │ └── pm │ │ │ ├── IPackageInstaller.java │ │ │ └── IPackageManager.java │ │ ├── proto │ │ ├── presets.proto │ │ └── settings.proto │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── docs ├── public │ ├── images │ └── robots.txt ├── .gitignore ├── .vitepress │ ├── theme │ │ ├── index.ts │ │ └── custom.css │ └── config.mts ├── deno.json ├── package.json ├── install.md ├── usage.md ├── index.md ├── features.md ├── presets.md ├── download.md └── settings.md ├── metadata └── en-US │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── screenshot-main.png │ │ ├── screenshot-search.png │ │ ├── screenshot-settings.png │ │ ├── screenshot-app-description.png │ │ ├── screenshot-create-preset.png │ │ ├── screenshot-import-preset.png │ │ ├── screenshot-preset-screen.png │ │ ├── screenshot-shizuku-dialog.png │ │ ├── screenshot-preset-edit-apps.png │ │ ├── screenshot-uninstall-dialog.png │ │ └── screenshot-uninstalled-list.png │ ├── short_description.txt │ └── full_description.txt ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── crowdin.yml ├── .gitignore ├── settings.gradle.kts ├── .github ├── workflows │ ├── pull_request_build.yml │ ├── docs.yml │ ├── release.yml │ └── build.yml ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── xyz-doesn-t-work.md ├── gradle.properties ├── privacy_policy.md ├── gradlew.bat ├── gradlew └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /docs/public/images: -------------------------------------------------------------------------------- 1 | ../../metadata/en-US/images -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vitepress/cache 3 | .vitepress/dist 4 | -------------------------------------------------------------------------------- /app/gradle.properties: -------------------------------------------------------------------------------- 1 | shizuku_version=13.1.4 2 | version_code=223 3 | version_name=3.1.2 4 | -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Sitemap: https://samolego.github.io/Canta/sitemap.xml 4 | -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Canta Debloater lets you uninstall any(*) app without root (updated version). 2 | -------------------------------------------------------------------------------- /app/src/main/res/play_store_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/play_store_512.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import "./custom.css"; 3 | 4 | export default DefaultTheme; 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /app/src/main/res/values/strings.xml 3 | translation: /app/src/main/res/values-%android_code%/%original_file_name% 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-main.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-search.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-settings.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-app-description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-app-description.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-create-preset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-create-preset.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-import-preset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-import-preset.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-preset-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-preset-screen.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-shizuku-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-shizuku-dialog.png -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep public class android.content.pm.** { *; } 2 | 3 | # ProGuard rules for protobuf generated classes 4 | -keep class io.github.samolego.canta.data.proto.** { *; } 5 | -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-preset-edit-apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-preset-edit-apps.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-uninstall-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-uninstall-dialog.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-uninstalled-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samolego/Canta/HEAD/metadata/en-US/images/phoneScreenshots/screenshot-uninstalled-list.png -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/data/AppSettingsSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.data 2 | 3 | import androidx.datastore.core.Serializer 4 | import com.google.protobuf.InvalidProtocolBufferException 5 | import io.github.samolego.canta.data.proto.AppSettings 6 | import io.github.samolego.canta.util.LogUtils 7 | import java.io.InputStream 8 | import java.io.OutputStream 9 | 10 | object AppSettingsSerializer : Serializer { 11 | override val defaultValue: AppSettings = AppSettings 12 | .newBuilder() 13 | .setAutoUpdateBloatList(true) 14 | .setConfirmBeforeUninstall(true) 15 | .build() 16 | 17 | override suspend fun readFrom(input: InputStream): AppSettings { 18 | try { 19 | return AppSettings.parseFrom(input) 20 | } catch (exception: InvalidProtocolBufferException) { 21 | LogUtils.e("AppSettingsSerializer", "Cannot read proto.", exception) 22 | } 23 | return defaultValue 24 | } 25 | 26 | override suspend fun writeTo(t: AppSettings, output: OutputStream) = t.writeTo(output) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/xyz-doesn-t-work.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: XYZ doesn't work 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: samolego 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Version info** 24 | - Android version: 25 | - Canta version: 26 | - Shizuku version: 27 | 28 | **Logcat** 29 | 30 | 31 | 32 | 33 |
34 | View logcat 35 | ```logcat 36 | paste logcat here 37 | ``` 38 |
39 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - "docs/**" 8 | # Allow manual trigger 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow only one concurrent deployment 17 | concurrency: 18 | group: pages 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup Deno 29 | uses: denoland/setup-deno@v2 30 | with: 31 | deno-version: v2.x 32 | 33 | - name: Setup Pages 34 | uses: actions/configure-pages@v4 35 | 36 | - name: Build 37 | run: | 38 | cd docs 39 | deno install 40 | deno task docs:build 41 | 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: docs/.vitepress/dist 46 | 47 | deploy: 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | needs: build 52 | runs-on: ubuntu-latest 53 | name: Deploy 54 | steps: 55 | - name: Deploy to GitHub Pages 56 | id: deployment 57 | uses: actions/deploy-pages@v4 58 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/component/fab/PresetEditFAB.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.component.fab 2 | 3 | import androidx.compose.foundation.layout.navigationBarsPadding 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.Save 8 | import androidx.compose.material3.ExtendedFloatingActionButton 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.unit.dp 16 | import io.github.samolego.canta.R 17 | 18 | @Composable 19 | fun PresetEditFAB( 20 | onPresetEditFinish: () -> Unit, 21 | ) { 22 | ExtendedFloatingActionButton( 23 | containerColor = MaterialTheme.colorScheme.tertiaryContainer, 24 | shape = RoundedCornerShape(16.dp), 25 | modifier = Modifier.padding(16.dp).navigationBarsPadding(), 26 | icon = { 27 | Icon( 28 | Icons.Default.Save, 29 | contentDescription = stringResource(R.string.save), 30 | ) 31 | }, 32 | text = { 33 | Text(stringResource(R.string.save)) 34 | }, 35 | onClick = onPresetEditFinish, 36 | ) 37 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.application.generateLocaleConfig=true -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/component/ScreenTopBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.component 2 | 3 | import androidx.compose.foundation.layout.RowScope 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.TopAppBar 9 | import androidx.compose.material3.TopAppBarDefaults 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.res.stringResource 12 | import io.github.samolego.canta.R 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Composable 16 | fun ScreenTopBar( 17 | onNavigateBack: () -> Unit, 18 | title: @Composable () -> Unit, 19 | actions: @Composable RowScope.() -> Unit = {}, 20 | ) { 21 | TopAppBar( 22 | colors = 23 | TopAppBarDefaults.topAppBarColors( 24 | containerColor = MaterialTheme.colorScheme.primaryContainer 25 | ), 26 | title = title, 27 | navigationIcon = { 28 | IconClickButton( 29 | onClick = onNavigateBack, 30 | icon = Icons.AutoMirrored.Filled.ArrowBack, 31 | contentDescription = stringResource(R.string.back), 32 | ) 33 | }, 34 | actions = actions, 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Canta - Usage 3 | description: Usage instructions for Canta. 4 | --- 5 | # Usage 6 | 7 | ## Uninstalling Apps 8 | 9 | ### 1. Launch Canta 10 |
11 | Main screen 12 |
13 | Home screen of Canta showing list of installed apps 14 |
15 |
16 | 17 | ### 2. Select Apps to Uninstall 18 |
19 |
20 | Search screen 21 |
22 | Search or filter apps you want to uninstall 23 |
24 |
25 |
26 | App description 27 |
28 | Check app descriptions and badges 29 |
30 |
31 |
32 | 33 | ### 3. Click the Trash Button 34 | 35 | ::: info 36 | You will need to grant Shizuku access for Canta upon first uninstallation. 37 | ::: 38 | 39 | ### 4. Confirm Uninstallation 40 |
41 | Uninstall confirmation 42 |
43 | Confirm that you want to uninstall selected apps 44 |
45 |
46 | 47 | ## Reinstalling Apps 48 | 49 | Navigate to the uninstalled apps tab, select the apps and click the reinstall button. 50 | 51 |
52 | Uninstalled apps 53 |
54 | List of previously uninstalled apps that can be reinstalled 55 |
56 |
57 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: Canta - Debloat Android without root 4 | description: A powerful Android app that lets you uninstall any app without requiring root access, powered by Shizuku 5 | 6 | hero: 7 | name: "Canta" 8 | text: "Uninstall any app without root!" 9 | tagline: Powered by Shizuku - safe and easy app removal 10 | image: 11 | src: https://raw.githubusercontent.com/samolego/Canta/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 12 | alt: Canta 13 | actions: 14 | - theme: brand 15 | text: Get Started 16 | link: /install 17 | - theme: alt 18 | text: View on GitHub 19 | link: https://github.com/samolego/Canta 20 | 21 | features: 22 | - icon: 🛡️ 23 | title: Safe Uninstallation 24 | details: No risk of bricking your device - APKs stay on device. In case of bootloop you will only need to perform a factory reset. 25 | 26 | - icon: 📝 27 | title: Badges & Descriptions 28 | details: Provides community-powered recommendations for apps to uninstall. 29 | link: https://github.com/Universal-Debloater-Alliance/universal-android-preinstalled-lists 30 | 31 | - icon: 📦 32 | title: Presets System 33 | details: Create, share, and apply app removal configurations across devices. Perfect for consistent debloating. 34 | link: /presets 35 | 36 | - icon: 📱 37 | title: Android 9.0+ 38 | details: Supports Android 9.0 (SDK 28) and above devices. 39 | 40 | - icon: 🌍 41 | title: Translations 42 | details: Help translate Canta into your language on Crowdin.

Crowdin translation status 43 | link: https://crowdin.com/project/canta 44 | 45 | - icon: 🪙 46 | title: Donate 47 | details: Enjoying Canta? It's built in my free time with privacy in mind - no ads, no tracking. If you find it useful, consider supporting its development! 48 | link: https://www.paypal.com/donate/?hosted_button_id=FD4R46ZZ5EWME 49 | 50 | --- 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 📦🚀 Build & deploy Android app for an environment 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: 7 | - published 8 | jobs: 9 | deployAndroid: 10 | permissions: write-all 11 | name: 🤖📦🚀 Build & deploy Android release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: ⬇️ Checkout repository 15 | uses: actions/checkout@v3 16 | - name: ⚙️ Setup Java 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: "21.x" 20 | cache: "gradle" 21 | distribution: "adopt" 22 | id: java 23 | - name: 🔐 Retrieve base64 keystore and decode it to a file 24 | id: write_file 25 | uses: timheuer/base64-to-file@v1.2 26 | with: 27 | fileName: "android-keystore.jks" 28 | fileDir: "${{ github.workspace }}/" 29 | encodedString: ${{ secrets.KEYSTORE_FILE_BASE64 }} 30 | - name: 📝🔐 Create keystore.properties file 31 | env: 32 | KEYSTORE_PROPERTIES_PATH: ${{ github.workspace }}/key.properties 33 | run: | 34 | echo "storeFile=${{ github.workspace }}/android-keystore.jks" > $KEYSTORE_PROPERTIES_PATH 35 | echo "keyAlias=${{ secrets.KEYSTORE_KEY_ALIAS }}" >> $KEYSTORE_PROPERTIES_PATH 36 | echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" >> $KEYSTORE_PROPERTIES_PATH 37 | echo "keyPassword=${{ secrets.KEYSTORE_KEY_PASSWORD }}" >> $KEYSTORE_PROPERTIES_PATH 38 | - name: 🤖📦 Create Android release 39 | run: | 40 | ./gradlew app:assembleRelease 41 | - name: 📝 Generate SHA-256 42 | run: | 43 | cd app/build/outputs/apk/release/ 44 | sha256sum *.apk > SHA256SUMS.txt 45 | - name: "Echo SHA-256 sums" 46 | run: cat app/build/outputs/apk/release/SHA256SUMS.txt 47 | - name: 🤖🚀 Upload to GitHub release 48 | uses: AButler/upload-release-assets@v2.0 49 | with: 50 | files: "app/build/outputs/apk/release/*.apk" 51 | repo-token: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/menu/MoreOptionsMenu.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.menu 2 | 3 | import androidx.compose.foundation.layout.width 4 | import androidx.compose.material3.DropdownMenu 5 | import androidx.compose.material3.DropdownMenuItem 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.compose.ui.unit.dp 11 | import io.github.samolego.canta.R 12 | import io.github.samolego.canta.ui.navigation.Screen 13 | 14 | @Composable 15 | fun MoreOptionsMenu( 16 | showMenu: Boolean, 17 | showBadgeInfoDialog: () -> Unit, 18 | navigateToPage: (route: String) -> Unit, 19 | onDismiss: () -> Unit, 20 | ) { 21 | 22 | DropdownMenu( 23 | expanded = showMenu, 24 | onDismissRequest = onDismiss, 25 | modifier = Modifier.width(200.dp) 26 | ) { 27 | // Badge info dialog 28 | DropdownMenuItem( 29 | text = { Text(stringResource(R.string.badge_info)) }, 30 | onClick = { 31 | showBadgeInfoDialog() 32 | onDismiss() 33 | } 34 | ) 35 | 36 | // Logs page 37 | DropdownMenuItem( 38 | text = { Text(stringResource(R.string.logs)) }, 39 | onClick = { 40 | navigateToPage(Screen.Logs.route) 41 | onDismiss() 42 | } 43 | ) 44 | 45 | // Settings page 46 | DropdownMenuItem( 47 | text = { Text(stringResource(R.string.settings)) }, 48 | onClick = { 49 | navigateToPage(Screen.Settings.route) 50 | onDismiss() 51 | } 52 | ) 53 | 54 | DropdownMenuItem( 55 | text = { Text(stringResource(R.string.presets)) }, 56 | onClick = { 57 | navigateToPage(Screen.Presets.route) 58 | onDismiss() 59 | } 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/util/LogUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.util 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.mutableStateListOf 5 | import androidx.compose.ui.graphics.Color 6 | import kotlinx.coroutines.DelicateCoroutinesApi 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | import java.text.SimpleDateFormat 11 | import java.util.Date 12 | import java.util.Locale 13 | 14 | object LogUtils { 15 | private val logs = mutableStateListOf() 16 | private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.US) 17 | 18 | fun d(tag: String, message: String) { 19 | Log.d(tag, message) 20 | addLog(LogLevel.DEBUG, tag, message) 21 | } 22 | 23 | fun i(tag: String, message: String) { 24 | Log.i(tag, message) 25 | addLog(LogLevel.INFO, tag, message) 26 | } 27 | 28 | fun w(tag: String, message: String) { 29 | Log.w(tag, message) 30 | addLog(LogLevel.WARNING, tag, message) 31 | } 32 | 33 | fun e(tag: String, message: String, throwable: Throwable? = null) { 34 | Log.e(tag, message, throwable) 35 | addLog(LogLevel.ERROR, tag, message + (throwable?.message?.let { "\n$it" } ?: "")) 36 | } 37 | 38 | @OptIn(DelicateCoroutinesApi::class) 39 | private fun addLog(level: LogLevel, tag: String, message: String) { 40 | // We need to run in main thread due to the logs being 41 | // a not thread-safe list 42 | GlobalScope.launch(Dispatchers.Main) { 43 | logs.add(LogEntry(level, tag, message, System.currentTimeMillis())) 44 | } 45 | } 46 | 47 | fun getLogs(): List = logs 48 | 49 | data class LogEntry( 50 | val level: LogLevel, 51 | val tag: String, 52 | val message: String, 53 | val timestamp: Long 54 | ) { 55 | fun getFormattedTime(): String = dateFormat.format(Date(timestamp)) 56 | } 57 | 58 | enum class LogLevel(val color: Color) { 59 | DEBUG(Color.Gray), 60 | INFO(Color.Green), 61 | WARNING(Color.Yellow), 62 | ERROR(Color.Red) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /privacy_policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy for Canta 2 | 3 | ## Introduction 4 | This Privacy Policy describes how Canta ("we", "our", or "app") handles information. 5 | Canta is a FOSS (Free and Open Source Software) debloater app powered by Shizuku that allows users to uninstall applications without root access. 6 | 7 | ## Information Collection and Use 8 | Canta does not collect, store, transmit, or share any personal information. 9 | The app functions entirely on your device and does not connect to any servers or third-party services for data processing. 10 | 11 | ## App Permissions 12 | Canta requires the following permissions: 13 | 14 | - **Shizuku API access**: Required to perform app uninstallation functions without root access. The Shizuku API allows Canta to execute commands with elevated privileges. 15 | - **Package visibility**: Required to see and interact with other installed applications on your device. 16 | 17 | These permissions are used solely for the core functionality of the app and not for data collection. 18 | 19 | ## Third-Party Libraries & Data Sources 20 | Canta uses the following third-party libraries: 21 | 22 | - **Shizuku**: An API that enables apps to perform operations that typically require elevated permissions. We do not share any data with Shizuku developers. For more information about Shizuku's privacy practices, please visit [https://shizuku.rikka.app/](https://shizuku.rikka.app/). 23 | 24 | Canta also uses **Universal Android Debloater**'s information about apps (app descriptions and badges). 25 | 26 | ## Data Storage 27 | Canta may store information on your device, e.g. app descriptions, versions etc. 28 | This information **never leaves your device** and is **not** accessible to us or any third parties. 29 | 30 | ## Changes to This Privacy Policy 31 | If needed, we may update our Privacy Policy from time to time. 32 | Thus, you are advised to review this page periodically for any changes. 33 | We will notify you of any changes by posting the new Privacy Policy on this page. 34 | 35 | ## Contact Us 36 | If you have any questions or suggestions about this Privacy Policy, do not hesitate to contact us through our GitHub repository at [https://github.com/samolego/Canta](https://github.com/samolego/Canta). 37 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/util/UninstallLock.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.util 2 | 3 | import android.content.Context 4 | import androidx.biometric.BiometricManager 5 | import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG 6 | import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL 7 | import androidx.biometric.BiometricPrompt 8 | import androidx.core.content.ContextCompat 9 | import androidx.core.content.ContextCompat.getString 10 | import androidx.fragment.app.FragmentActivity 11 | import io.github.samolego.canta.R 12 | 13 | private const val TAG = "UninstallLock" 14 | 15 | fun showBiometricPrompt( 16 | context: Context, 17 | onSuccess: () -> Unit 18 | ) { 19 | val executor = ContextCompat.getMainExecutor(context) 20 | 21 | val promptInfo = BiometricPrompt.PromptInfo.Builder() 22 | .setTitle(getString(context, R.string.auth_required)) 23 | .setSubtitle(getString(context, R.string.auth_required_description)) 24 | .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) 25 | .build() 26 | 27 | 28 | val biometricManager = BiometricManager.from(context) 29 | val authStatus = biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) 30 | if (authStatus != BiometricManager.BIOMETRIC_SUCCESS) { 31 | LogUtils.e(TAG, "Device cannot use biometrics. Auth status: $authStatus") 32 | 33 | } 34 | 35 | val biometricPrompt = BiometricPrompt(context as FragmentActivity, executor, 36 | object : BiometricPrompt.AuthenticationCallback() { 37 | override fun onAuthenticationSucceeded( 38 | result: BiometricPrompt.AuthenticationResult 39 | ) { 40 | onSuccess() 41 | } 42 | 43 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { 44 | LogUtils.e(TAG, "An error occured while trying to authenticate. Code: $errorCode, message: $errString") 45 | } 46 | 47 | override fun onAuthenticationFailed() { 48 | LogUtils.w(TAG, "Authentication failed!") 49 | } 50 | }) 51 | 52 | biometricPrompt.authenticate(promptInfo) 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/util/apps/Filter.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.util.apps 2 | 3 | import io.github.samolego.canta.util.RemovalRecommendation 4 | import java.util.Locale 5 | 6 | /** 7 | * Filter for the app list. 8 | * @param name Name of the filter. 9 | * @param shouldShow Function to determine if the app should be shown. 10 | */ 11 | class Filter( 12 | val name: String, 13 | val shouldShow: (AppInfo) -> Boolean, 14 | val removalRecommendation: RemovalRecommendation? = null 15 | ) { 16 | companion object { 17 | /** 18 | * Filter to show all apps. 19 | */ 20 | val any: Filter = Filter(name = "Any", shouldShow = { true }) 21 | 22 | /** 23 | * List of available filters. 24 | */ 25 | val availableFilters: List 26 | 27 | init { 28 | // Filters are generated from the RemovalRecommendation enum. 29 | val removalFilters = 30 | RemovalRecommendation.entries.filter { RemovalRecommendation.SYSTEM != it } 31 | .map { entry -> 32 | Filter( 33 | name = entry.toString().lowercase(Locale.ROOT) 34 | .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() }, 35 | shouldShow = { app -> app.removalInfo == entry }, 36 | removalRecommendation = entry 37 | ) 38 | }.toMutableList() 39 | removalFilters.add(0, any) 40 | 41 | // Apps that are not system apps. 42 | val user = Filter(name = "User", shouldShow = { app -> !app.isSystemApp }) 43 | removalFilters.add(1, user) 44 | 45 | val unclassified = 46 | Filter(name = "Unclassified", shouldShow = { app -> app.removalInfo == null }) 47 | removalFilters.add(2, unclassified) 48 | 49 | // Apps that are disabled 50 | val disabled = Filter(name = "Disabled", shouldShow = { app -> app.isDisabled }) 51 | removalFilters.add(3, disabled) 52 | 53 | availableFilters = removalFilters 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 🌙 Nightly Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - "app/**" 10 | 11 | jobs: 12 | buildAndroid: 13 | permissions: write-all 14 | name: 🤖📦 Build Android APK 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: ⬇️ Checkout repository 18 | uses: actions/checkout@v3 19 | 20 | - name: ⚙️ Setup Java 21 | uses: actions/setup-java@v3 22 | with: 23 | java-version: "21.x" 24 | cache: "gradle" 25 | distribution: "adopt" 26 | id: java 27 | 28 | - name: 🔐 Retrieve base64 keystore and decode it to a file 29 | id: write_file 30 | uses: timheuer/base64-to-file@v1.2 31 | with: 32 | fileName: "android-keystore.jks" 33 | fileDir: "${{ github.workspace }}/" 34 | encodedString: ${{ secrets.KEYSTORE_FILE_BASE64 }} 35 | 36 | - name: 📝🔐 Create keystore.properties file 37 | env: 38 | KEYSTORE_PROPERTIES_PATH: ${{ github.workspace }}/key.properties 39 | run: | 40 | echo "storeFile=${{ github.workspace }}/android-keystore.jks" > $KEYSTORE_PROPERTIES_PATH 41 | echo "keyAlias=${{ secrets.KEYSTORE_KEY_ALIAS }}" >> $KEYSTORE_PROPERTIES_PATH 42 | echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" >> $KEYSTORE_PROPERTIES_PATH 43 | echo "keyPassword=${{ secrets.KEYSTORE_KEY_PASSWORD }}" >> $KEYSTORE_PROPERTIES_PATH 44 | 45 | - name: 🤖📦 Create Android release 46 | run: | 47 | ./gradlew app:assembleRelease 48 | 49 | - name: 📝 Generate build date 50 | id: date 51 | run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT 52 | - name: 📝 Generate SHA-256 53 | run: | 54 | cd app/build/outputs/apk/release/ 55 | sha256sum *.apk > SHA256SUMS.txt 56 | - name: "Echo SHA-256 sums" 57 | run: cat app/build/outputs/apk/release/SHA256SUMS.txt 58 | - name: 📦 Upload Build Artifact 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: app-release 62 | # Include both the APK and the SHA-256 sums 63 | path: app/build/outputs/apk/release/ 64 | retention-days: 7 65 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.core.view.WindowCompat 16 | 17 | private val DarkColorScheme = darkColorScheme( 18 | primary = Purple80, 19 | secondary = PurpleGrey80, 20 | tertiary = Pink80 21 | ) 22 | 23 | private val LightColorScheme = lightColorScheme( 24 | primary = Purple40, 25 | secondary = PurpleGrey40, 26 | tertiary = Pink40 27 | 28 | /* Other default colors to override 29 | background = Color(0xFFFFFBFE), 30 | surface = Color(0xFFFFFBFE), 31 | onPrimary = Color.White, 32 | onSecondary = Color.White, 33 | onTertiary = Color.White, 34 | onBackground = Color(0xFF1C1B1F), 35 | onSurface = Color(0xFF1C1B1F), 36 | */ 37 | ) 38 | 39 | @Composable 40 | fun CantaTheme( 41 | darkTheme: Boolean = isSystemInDarkTheme(), 42 | // Dynamic color is available on Android 12+ 43 | dynamicColor: Boolean = true, 44 | content: @Composable () -> Unit 45 | ) { 46 | val colorScheme = when { 47 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 48 | val context = LocalContext.current 49 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 50 | } 51 | 52 | darkTheme -> DarkColorScheme 53 | else -> LightColorScheme 54 | } 55 | val view = LocalView.current 56 | if (!view.isInEditMode) { 57 | SideEffect { 58 | val window = (view.context as Activity).window 59 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme 60 | } 61 | } 62 | 63 | MaterialTheme( 64 | colorScheme = colorScheme, 65 | typography = Typography, 66 | content = content 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/util/apps/AppInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.util.apps 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.content.pm.PackageInfo 5 | import android.content.pm.PackageManager 6 | import android.os.Parcelable 7 | import io.github.samolego.canta.util.BloatData 8 | import io.github.samolego.canta.util.RemovalRecommendation 9 | import kotlinx.parcelize.Parcelize 10 | 11 | /** 12 | * Data class to hold information about an app. 13 | */ 14 | @Parcelize 15 | data class AppInfo( 16 | private val appName: String?, 17 | val packageName: String, 18 | val versionName: String, 19 | val versionCode: Long, 20 | val isSystemApp: Boolean, 21 | val isUninstalled: Boolean, 22 | val isDisabled: Boolean, 23 | val bloatData: BloatData? 24 | ) : Parcelable { 25 | 26 | val name: String 27 | get() = appName ?: packageName.substringAfterLast('.') 28 | val removalInfo: RemovalRecommendation? 29 | get() = bloatData?.removal 30 | val description: String? 31 | get() = bloatData?.description 32 | 33 | 34 | companion object { 35 | fun fromPackageInfo( 36 | packageInfo: PackageInfo, 37 | packageManager: PackageManager, 38 | isUninstalled: Boolean, 39 | bloatList: Map = emptyMap() 40 | ): AppInfo { 41 | val bloatData = bloatList[packageInfo.packageName] 42 | 43 | val isSystemApp = 44 | (packageInfo.applicationInfo!!.flags and ApplicationInfo.FLAG_SYSTEM) != 0 45 | 46 | val isDisabled = try { 47 | !packageManager.getApplicationInfo(packageInfo.packageName, 0).enabled 48 | } catch (e: PackageManager.NameNotFoundException) { 49 | false 50 | } 51 | 52 | val versionName = packageInfo.versionName ?: "unknown" 53 | 54 | return AppInfo( 55 | appName = packageInfo.applicationInfo!!.loadLabel(packageManager).toString(), 56 | packageName = packageInfo.packageName, 57 | versionName = versionName, 58 | versionCode = packageInfo.longVersionCode, 59 | isSystemApp = isSystemApp, 60 | isUninstalled = isUninstalled, 61 | isDisabled = isDisabled, 62 | bloatData = bloatData, 63 | ) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 22 | 24 | 25 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/extension/PackageManagerExt.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.extension 2 | 3 | import android.content.pm.PackageInfo 4 | import android.content.pm.PackageManager 5 | import android.content.pm.PackageManager.NameNotFoundException 6 | import android.os.Build 7 | import io.github.samolego.canta.util.LogUtils 8 | import io.github.samolego.canta.util.apps.AppInfo 9 | 10 | 11 | private fun PackageManager.getUninstalledPackages(installedPackages: List): List { 12 | val flags = PackageManager.MATCH_UNINSTALLED_PACKAGES 13 | 14 | // Get uninstalled packages + installed packages 15 | val uninstalledPackages = getPackages(flags).toSet() 16 | 17 | val installed = installedPackages.map { it.packageName } 18 | val minus = uninstalledPackages.filter { !installed.contains(it.packageName) } 19 | 20 | // Return only apps that have been uninstalled 21 | return minus.toList() 22 | } 23 | 24 | fun PackageManager.getAllPackagesInfo(): List { 25 | val installedPackages = getInstalledPackages() 26 | val uninstalledPackages = getUninstalledPackages(installedPackages) 27 | 28 | val all = uninstalledPackages.map { app -> 29 | AppInfo.fromPackageInfo(app, this, true) 30 | } + installedPackages.map { app -> 31 | AppInfo.fromPackageInfo(app, this, false) 32 | } 33 | 34 | return all 35 | } 36 | 37 | fun PackageManager.getInstalledPackages(): List { 38 | val flags = PackageManager.GET_META_DATA 39 | return getPackages(flags) 40 | } 41 | 42 | private fun PackageManager.getPackages(flags: Int): List { 43 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 44 | this.getInstalledPackages( 45 | PackageManager.PackageInfoFlags.of(flags.toLong()) 46 | ) 47 | } else { 48 | this.getInstalledPackages(flags) 49 | } 50 | } 51 | 52 | fun PackageManager.getInfoForPackage( 53 | packageName: String, 54 | ): PackageInfo? { 55 | return try { 56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 57 | this.getPackageInfo( 58 | packageName, 59 | PackageManager.PackageInfoFlags.of(PackageManager.GET_META_DATA.toLong()) 60 | ) 61 | } else { 62 | this.getPackageInfo( 63 | packageName, 64 | PackageManager.GET_META_DATA 65 | ) 66 | } 67 | } catch (e: NameNotFoundException) { 68 | LogUtils.e("PackageManagerExt", "Failed to get package info", e) 69 | null 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/util/shizuku/ShizukuPermission.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.util.shizuku 2 | 3 | import android.content.pm.PackageManager 4 | import io.github.samolego.canta.SHIZUKU_PACKAGE_NAME 5 | import io.github.samolego.canta.packageName 6 | import io.github.samolego.canta.util.LogUtils 7 | import rikka.shizuku.Shizuku 8 | import rikka.sui.Sui 9 | 10 | class ShizukuPermission { 11 | companion object { 12 | private const val SHIZUKU_CODE = 0xCA07A 13 | 14 | private val isSui: Boolean = Sui.init(packageName) 15 | private val TAG: String = ShizukuPermission::class.java.simpleName 16 | private var binderStatus = Shizuku.pingBinder() 17 | 18 | init { 19 | Shizuku.addBinderDeadListener { binderStatus = false } 20 | Shizuku.addBinderReceivedListener { binderStatus = true } 21 | } 22 | 23 | /** Checks if the shizuku permission is granted. Call from main thread only! */ 24 | fun requestShizukuPermission(onPermissionResult: (Int) -> Unit) { 25 | if (!checkRequirements()) { 26 | LogUtils.i( 27 | TAG, 28 | "Shizuku permission result: ping: ${Shizuku.pingBinder()}, preV11: ${Shizuku.isPreV11()}" 29 | ) 30 | onPermissionResult(PackageManager.PERMISSION_DENIED) 31 | } else if (isPermissionGranted()) { 32 | LogUtils.i( 33 | TAG, 34 | "Shizuku permission result: ${Shizuku.checkSelfPermission()}, sui status: $isSui" 35 | ) 36 | onPermissionResult(PackageManager.PERMISSION_GRANTED) 37 | } else { 38 | LogUtils.i(TAG, "Requesting shizuku permission") 39 | Shizuku.addRequestPermissionResultListener { requestCode, grantResult -> 40 | if (requestCode == SHIZUKU_CODE) { 41 | onPermissionResult(grantResult) 42 | } 43 | } 44 | Shizuku.requestPermission(SHIZUKU_CODE) 45 | } 46 | } 47 | 48 | private fun checkRequirements(): Boolean { 49 | return binderStatus && !Shizuku.isPreV11() && !Shizuku.shouldShowRequestPermissionRationale() 50 | } 51 | 52 | private fun isPermissionGranted(): Boolean { 53 | return isSui || Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED 54 | } 55 | 56 | fun isCantaAuthorized(): Boolean { 57 | return checkRequirements() && isPermissionGranted() 58 | } 59 | 60 | fun checkShizukuActive(packageManager: PackageManager): ShizukuStatus { 61 | if (isSui) { 62 | return ShizukuStatus.ACTIVE 63 | } 64 | try { 65 | packageManager.getPackageInfo(SHIZUKU_PACKAGE_NAME, 0) 66 | if (Shizuku.pingBinder() && !Shizuku.isPreV11()) { 67 | return ShizukuStatus.ACTIVE 68 | } 69 | return ShizukuStatus.NOT_ACTIVE 70 | } catch (e: PackageManager.NameNotFoundException) { 71 | return ShizukuStatus.NOT_INSTALLED 72 | } 73 | } 74 | } 75 | } 76 | 77 | /** Enum class to represent Shizuku status. */ 78 | enum class ShizukuStatus { 79 | ACTIVE, 80 | NOT_ACTIVE, 81 | NOT_INSTALLED, 82 | } 83 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | activityCompose = "1.11.0" 3 | api = "13.1.5" 4 | biometric = "1.4.0-alpha04" 5 | coilCompose = "3.1.0" 6 | composeBom = "2025.10.00" 7 | coreKtx = "1.17.0" 8 | espressoCore = "3.7.0" 9 | hiddenapibypass = "6.1" 10 | junit = "4.13.2" 11 | junitVersion = "1.3.0" 12 | lifecycleRuntimeKtx = "2.9.4" 13 | material3Android = "1.4.0" 14 | materialIconsExtended = "1.7.8" 15 | navigationRuntimeKtx = "2.9.5" 16 | navigationCompose = "2.9.5" 17 | protobuf = "4.32.0" 18 | protobufPlugin = "0.9.5" 19 | provider = "13.1.5" 20 | kotlin = "2.1.21" 21 | datastore = "1.1.7" 22 | 23 | [plugins] 24 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 25 | protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } 26 | 27 | [libraries] 28 | androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } 29 | androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } 30 | androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastore" } 31 | protobuf-javalite = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobuf" } 32 | protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } 33 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } 34 | androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } 35 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } 36 | androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } 37 | androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" } 38 | androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } 39 | androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" } 40 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } 41 | androidx-navigation-runtime-ktx = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" } 42 | androidx-material3-android = { module = "androidx.compose.material3:material3-android", version.ref = "material3Android" } 43 | androidx-ui = { module = "androidx.compose.ui:ui" } 44 | androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } 45 | androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } 46 | androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } 47 | androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } 48 | androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } 49 | api = { module = "dev.rikka.shizuku:api", version.ref = "api" } 50 | coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } 51 | hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" } 52 | junit = { module = "junit:junit", version.ref = "junit" } 53 | provider = { module = "dev.rikka.shizuku:provider", version.ref = "provider" } 54 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 55 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Canta - Features 3 | description: Features of Canta. 4 | --- 5 | # Features 6 | 7 | ## Core Features 8 | 9 | ### Safe Uninstallation 10 | No permanent device damage risk can be done when uninstalling apps with Canta. The uninstallation process happens for current user 11 | only, so APKs remain on device. In case you uninstall a critical system component, you can risk a bootloop. 12 | In that case, you will need to perform a factory reset and the device will not be bricked. 13 | 14 | ::: info 15 | Canta calls Android System APIs directly to uninstall apps, similar to how ADB does it. 16 | ::: 17 | 18 | ### System Requirements 19 | * Android 9.0 (SDK 28) or higher 20 | * No root required 21 | * Elevates permissions using [Shizuku](https://shizuku.rikka.app/) 22 | 23 | ## Cool Features 24 | 25 | ### App Management 26 | * Filter apps by type (system apps, user apps) or badge 27 | * Search functionality 28 | * Batch selection for multiple uninstalls 29 | * See app info and size details 30 | 31 | ### Presets 32 | * Create reusable app removal configurations 33 | * Import and export presets as JSON 34 | * Share presets across devices and with others 35 | 36 | ### App Badges 37 | * 🟢 **Safe to Remove** - Non-essential apps. Still review them, though. 38 | * 🟡 **Advanced** - May affect some functionality. 39 | * 🔴 **Expert** - Can break important functionality. 40 | * 🟣 **Unsafe** - Apps that can break vital system components. 41 | 42 | ### User Interface 43 | App uses a modern Material Design 3 interface that adapts to your device's theme preferences. 44 | The interface provides detailed information about each app while maintaining simplicity in its controls. 45 | Finding specific apps is effortless thanks to the quick search functionality and comprehensive filtering options. 46 | 47 |
48 | Main screen 49 |
50 | Canta Home Screen 51 |
52 |
53 | 54 | ## Advanced Features 55 | 56 | ### Presets System 57 | Canta's preset system allows you to create, manage, and share collections of apps for removal. This feature enables: 58 | 59 | * **Device Consistency**: Apply the same bloatware removal across multiple devices 60 | * **Community Sharing**: Share your carefully curated removal lists with others 61 | * **Quick Setup**: Rapidly configure new devices with proven app configurations 62 | * **Backup & Restore**: Save your uninstall preferences before major changes 63 | 64 | Presets are stored locally and can be exported as JSON for easy sharing. When importing presets, Canta automatically validates app availability on your device and filters out incompatible entries. 65 | 66 | [Learn more about Presets →](/presets) 67 | 68 | ## Privacy & Security 69 | 70 | ### Privacy-Focused 71 | Canta can operate without internet connection and collects no data whatsoever. 72 | There's no analytics or tracking built into the app. 73 | 74 | ::: warning NOTE 75 | Badges information and app descriptions are fetched from GitHub repository if connected to the internet. 76 | No data is uploaded whatsoever. 77 | ::: 78 | 79 | ### Open Source 80 | Canta is licensed under LGPL-3.0, with its complete source code available on [GitHub](https://github.com/samolego/Canta). This transparency ensures you can verify the app's functionality and contribute to its development if you wish. 81 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-brand: #ff4444 !important; 3 | --vp-c-brand-light: #ff6b6b !important; 4 | --vp-c-brand-lighter: #ffa0a0 !important; 5 | --vp-c-brand-dark: #cc0000 !important; 6 | --vp-c-brand-darker: #a60000 !important; 7 | 8 | --vp-home-hero-name-color: #ff4444 !important; 9 | --vp-c-brand-1: #ff4444 !important; 10 | --vp-c-brand-2: #ff6b6b !important; 11 | --vp-c-brand-3: #ff8c8c !important; 12 | 13 | --vp-button-brand-bg: #ff4444 !important; 14 | --vp-button-brand-hover-bg: #ff6b6b !important; 15 | --vp-button-brand-active-bg: #cc0000 !important; 16 | } 17 | 18 | .dark { 19 | --vp-c-brand: #ff6666 !important; 20 | --vp-c-brand-light: #ff8c8c !important; 21 | --vp-c-brand-lighter: #ffb3b3 !important; 22 | --vp-c-brand-dark: #ff4444 !important; 23 | --vp-c-brand-darker: #cc0000 !important; 24 | 25 | --vp-home-hero-name-color: #ff6666 !important; 26 | --vp-c-brand-1: #ff6666 !important; 27 | --vp-c-brand-2: #ff8c8c !important; 28 | --vp-c-brand-3: #ffb3b3 !important; 29 | } 30 | 31 | .VPHero .image-src { 32 | max-width: 192px; 33 | max-height: 192px; 34 | } 35 | 36 | .VPNavBarTitle .logo { 37 | height: 32px; 38 | width: 32px; 39 | } 40 | 41 | .VPFeature { 42 | position: relative; 43 | transition: 44 | transform 0.2s, 45 | box-shadow 0.2s; 46 | } 47 | 48 | .VPFeature[href] { 49 | cursor: pointer; 50 | } 51 | 52 | .VPFeature[href]:hover { 53 | transform: translateY(-2px); 54 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); 55 | } 56 | 57 | .VPFeature[href]::after { 58 | content: ""; 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | right: 0; 63 | bottom: 0; 64 | border-radius: 12px; 65 | border: 2px solid transparent; 66 | transition: border-color 0.2s; 67 | } 68 | 69 | .VPFeature[href]:hover::after { 70 | border-color: var(--vp-c-brand); 71 | } 72 | 73 | .dark .VPFeature[href]:hover { 74 | box-shadow: 0 2px 12px rgba(255, 255, 255, 0.1); 75 | } 76 | 77 | /* Screenshot containers and styling */ 78 | .screenshot-container { 79 | display: flex; 80 | flex-direction: column; 81 | align-items: center; 82 | margin: 2rem 0; 83 | text-align: center; 84 | } 85 | 86 | .screenshot-grid { 87 | display: grid; 88 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 89 | gap: 2rem; 90 | margin: 2rem 0; 91 | } 92 | 93 | .screenshot-item { 94 | display: flex; 95 | flex-direction: column; 96 | align-items: center; 97 | text-align: center; 98 | } 99 | 100 | .phone-screenshot { 101 | width: 256px; 102 | border-radius: 1rem; 103 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 104 | transition: 105 | transform 0.2s ease, 106 | box-shadow 0.2s ease; 107 | } 108 | 109 | .phone-screenshot:hover { 110 | transform: translateY(-4px); 111 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); 112 | } 113 | 114 | .screenshot-caption { 115 | margin-top: 1rem; 116 | color: var(--vp-c-text-2); 117 | font-size: 0.9rem; 118 | } 119 | 120 | /* Dark mode adjustments */ 121 | .dark .phone-screenshot { 122 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); 123 | } 124 | 125 | .dark .phone-screenshot:hover { 126 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4); 127 | } 128 | 129 | /* Responsive adjustments */ 130 | @media (max-width: 640px) { 131 | .screenshot-grid { 132 | grid-template-columns: 1fr; 133 | } 134 | 135 | .phone-screenshot { 136 | width: 200px; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/util/shizuku/ShizukuPackageInstallerUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.util.shizuku 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.pm.IPackageInstaller 6 | import android.content.pm.IPackageManager 7 | import android.content.pm.PackageInstaller 8 | import android.content.pm.PackageManager 9 | import android.os.Build 10 | import org.lsposed.hiddenapibypass.HiddenApiBypass 11 | import rikka.shizuku.ShizukuBinderWrapper 12 | import rikka.shizuku.SystemServiceHelper 13 | import java.lang.reflect.InvocationTargetException 14 | 15 | /** 16 | * Taken from FDroid Priv. 17 | */ 18 | object ShizukuPackageInstallerUtils { 19 | private val PACKAGE_MANAGER: IPackageManager by lazy { 20 | // This is needed to access hidden methods in IPackageManager 21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 22 | HiddenApiBypass.addHiddenApiExemptions( 23 | "Landroid/content/pm" 24 | ) 25 | } 26 | 27 | IPackageManager.Stub.asInterface( 28 | ShizukuBinderWrapper( 29 | SystemServiceHelper.getSystemService( 30 | "package" 31 | ) 32 | ) 33 | ) 34 | } 35 | 36 | fun getPrivilegedPackageInstaller(): IPackageInstaller { 37 | val packageInstaller: IPackageInstaller = PACKAGE_MANAGER.packageInstaller 38 | return IPackageInstaller.Stub.asInterface(ShizukuBinderWrapper(packageInstaller.asBinder())) 39 | } 40 | 41 | /** 42 | * Taken from https://github.com/RikkaApps/Shizuku-API/blob/01e08879d58a5cb11a333535c6ddce9f7b7c88ff/demo/src/main/java/rikka/shizuku/demo/util/PackageInstallerUtils.java#L15 43 | * @author RikkaW 44 | */ 45 | @Throws( 46 | NoSuchMethodException::class, 47 | IllegalAccessException::class, 48 | InvocationTargetException::class, 49 | InstantiationException::class, 50 | ) 51 | fun createPackageInstaller( 52 | installer: IPackageInstaller?, 53 | installerPackageName: String?, 54 | userId: Int, 55 | activity: Activity, 56 | ): PackageInstaller { 57 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) { 58 | return PackageInstaller::class.java.getConstructor( 59 | IPackageInstaller::class.java, 60 | String::class.java, 61 | String::class.java, 62 | Int::class.javaPrimitiveType 63 | ).newInstance(installer, installerPackageName, null, userId) 64 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 65 | return PackageInstaller::class.java.getConstructor( 66 | IPackageInstaller::class.java, String::class.java, Int::class.java 67 | ) 68 | .newInstance(installer, installerPackageName, userId) 69 | } else { 70 | return PackageInstaller::class.java.getConstructor( 71 | Context::class.java, 72 | PackageManager::class.java, 73 | IPackageInstaller::class.java, 74 | String::class.java, 75 | Int::class.javaPrimitiveType 76 | ) 77 | .newInstance( 78 | activity, 79 | activity.packageManager, 80 | installer, 81 | installerPackageName, 82 | userId 83 | ) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/data/SettingsStore.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.data 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import androidx.datastore.core.DataStore 6 | import androidx.datastore.dataStore 7 | import io.github.samolego.canta.data.proto.AppSettings 8 | import io.github.samolego.canta.util.DEFAULT_BLOAT_COMMITS_URL 9 | import io.github.samolego.canta.util.DEFAULT_BLOAT_URL 10 | import kotlinx.coroutines.flow.map 11 | 12 | // Proto DataStore instance 13 | private val Context.dataStore: DataStore by 14 | dataStore(fileName = "app_settings.pb", serializer = AppSettingsSerializer) 15 | 16 | class SettingsStore private constructor(context: Context) { 17 | 18 | private val dataStore = context.dataStore 19 | val autoUpdateBloatListFlow = dataStore.data.map { it.autoUpdateBloatList } 20 | val confirmBeforeUninstallFlow = dataStore.data.map { it.confirmBeforeUninstall } 21 | val disableRiskDialogFlow = dataStore.data.map { it.disableRiskDialog } 22 | val latestCommitHashFlow = dataStore.data.map { it.latestBloatCommitHash } 23 | val bloatListUrlFlow = dataStore.data.map { 24 | it.bloatListUrl.ifEmpty { DEFAULT_BLOAT_URL } 25 | } 26 | val commitsUrlFlow = dataStore.data.map { 27 | it.commitsUrl.ifEmpty { DEFAULT_BLOAT_COMMITS_URL } 28 | } 29 | val allowUnsafeUninstallsFlow = dataStore.data.map { it.allowUnsafeUninstalls } 30 | val hideSuccessDialogFlow = dataStore.data.map { it.hideSuccessDialog } 31 | 32 | val authEnabledFlow = dataStore.data.map { it.authEnabled } 33 | 34 | 35 | suspend fun setAutoUpdateBloatList(autoUpdate: Boolean) { 36 | dataStore.updateData { it.toBuilder().setAutoUpdateBloatList(autoUpdate).build() } 37 | } 38 | 39 | suspend fun setConfirmBeforeUninstall(needsConfirm: Boolean) { 40 | dataStore.updateData { it.toBuilder().setConfirmBeforeUninstall(needsConfirm).build() } 41 | } 42 | 43 | suspend fun setDisableRiskDialog(disable: Boolean) { 44 | dataStore.updateData { it.toBuilder().setDisableRiskDialog(disable).build() } 45 | } 46 | 47 | suspend fun setLatestCommitHash(hash: String) { 48 | dataStore.updateData { it.toBuilder().setLatestBloatCommitHash(hash).build() } 49 | } 50 | 51 | suspend fun setBloatListUrl(url: String) { 52 | dataStore.updateData { it.toBuilder().setBloatListUrl(url).build() } 53 | } 54 | 55 | suspend fun setCommitsUrl(url: String) { 56 | dataStore.updateData { it.toBuilder().setCommitsUrl(url).build() } 57 | } 58 | 59 | suspend fun setAllowUnsafeUninstalls(allow: Boolean) { 60 | dataStore.updateData { it.toBuilder().setAllowUnsafeUninstalls(allow).build() } 61 | } 62 | 63 | suspend fun setHideSuccessDialog(hide: Boolean) { 64 | dataStore.updateData { it.toBuilder().setHideSuccessDialog(hide).build() } 65 | } 66 | 67 | suspend fun setAuthEnabled(enabled: Boolean) { 68 | dataStore.updateData { it.toBuilder().setAuthEnabled(enabled).build() } 69 | } 70 | 71 | 72 | 73 | companion object { 74 | @SuppressLint("StaticFieldLeak") @Volatile private var INSTANCE: SettingsStore? = null 75 | 76 | fun initialize(appContext: Context) { 77 | synchronized(this) { 78 | if (INSTANCE == null) { 79 | INSTANCE = SettingsStore(appContext) 80 | } 81 | } 82 | } 83 | 84 | fun getInstance(): SettingsStore { 85 | return INSTANCE 86 | ?: throw IllegalStateException( 87 | "SettingsStore has not been initialized. Call initialize() in MyApplication.onCreate()." 88 | ) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/dialog/ExplainBadgesDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.dialog 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.lazy.grid.GridCells 12 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 13 | import androidx.compose.foundation.lazy.grid.items 14 | import androidx.compose.material3.BasicAlertDialog 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.TextButton 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.window.DialogProperties 25 | import io.github.samolego.canta.R 26 | import io.github.samolego.canta.ui.component.RemovalBadge 27 | import io.github.samolego.canta.util.RemovalRecommendation 28 | 29 | @OptIn(ExperimentalMaterial3Api::class) 30 | @Composable 31 | fun ExplainBadgesDialog( 32 | onDismissRequest: () -> Unit 33 | ) { 34 | BasicAlertDialog( 35 | modifier = Modifier 36 | .fillMaxWidth(0.8f) 37 | .background(MaterialTheme.colorScheme.surfaceContainer, MaterialTheme.shapes.large), 38 | properties = DialogProperties( 39 | decorFitsSystemWindows = true, 40 | usePlatformDefaultWidth = false, 41 | ), 42 | onDismissRequest = onDismissRequest, 43 | ) { 44 | Column( 45 | modifier = Modifier.fillMaxWidth(), 46 | horizontalAlignment = Alignment.CenterHorizontally, 47 | ) { 48 | LazyVerticalGrid( 49 | columns = GridCells.Fixed(1), 50 | modifier = Modifier.fillMaxWidth(), 51 | contentPadding = PaddingValues(8.dp), 52 | horizontalArrangement = Arrangement.SpaceEvenly, 53 | verticalArrangement = Arrangement.SpaceEvenly 54 | ) { 55 | items(RemovalRecommendation.entries) { removalRecommendation -> 56 | Row( 57 | modifier = Modifier 58 | .padding(8.dp) 59 | .fillMaxWidth(), 60 | horizontalArrangement = Arrangement.SpaceBetween, 61 | ) { 62 | Box( 63 | modifier = Modifier.weight(2f), 64 | ) { 65 | RemovalBadge( 66 | type = removalRecommendation 67 | ) 68 | } 69 | Text( 70 | removalRecommendation.description, 71 | style = MaterialTheme.typography.bodySmall, 72 | modifier = Modifier.weight(3f) 73 | ) 74 | } 75 | } 76 | } 77 | TextButton( 78 | modifier = Modifier 79 | .fillMaxWidth() 80 | .padding(8.dp), 81 | onClick = onDismissRequest, 82 | ) { 83 | Text(stringResource(R.string.got_it)) 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/util/CustomTextSelectionCallback.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.view.ActionMode 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import android.widget.TextView 10 | 11 | 12 | class CustomTextSelectionCallback( 13 | private val context: Context, 14 | private val textView: TextView 15 | ) : ActionMode.Callback { 16 | 17 | companion object { 18 | private const val MENU_ITEM_TRANSLATE_DEEPL = 100 19 | private const val MENU_ITEM_SEARCH_GOOGLE = 101 20 | } 21 | 22 | override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { 23 | menu.add(Menu.NONE, MENU_ITEM_TRANSLATE_DEEPL, 5, "Translate with DeepL") 24 | .setIcon(android.R.drawable.ic_menu_search) // Use a standard icon or your custom one 25 | 26 | menu.add(Menu.NONE, MENU_ITEM_SEARCH_GOOGLE, 6, "Search") 27 | .setIcon(android.R.drawable.ic_menu_search) 28 | 29 | return true 30 | } 31 | 32 | override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { 33 | return false 34 | } 35 | 36 | override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { 37 | val selectedText = getSelectedText() 38 | 39 | when (item.itemId) { 40 | MENU_ITEM_TRANSLATE_DEEPL -> { 41 | openDeepLTranslation(selectedText) 42 | mode.finish() 43 | return true 44 | } 45 | MENU_ITEM_SEARCH_GOOGLE -> { 46 | searchOnGoogle(selectedText) 47 | mode.finish() 48 | return true 49 | } 50 | } 51 | 52 | return false 53 | } 54 | 55 | override fun onDestroyActionMode(mode: ActionMode) { 56 | } 57 | 58 | private fun getSelectedText(): String { 59 | val selectionStart = textView.selectionStart 60 | val selectionEnd = textView.selectionEnd 61 | 62 | if (selectionStart != selectionEnd) { 63 | val min = selectionStart.coerceAtMost(selectionEnd) 64 | val max = selectionStart.coerceAtLeast(selectionEnd) 65 | if (min >= 0 && max <= textView.text.length) { 66 | return textView.text.substring(min, max) 67 | } 68 | } 69 | return "" 70 | } 71 | 72 | private fun openDeepLTranslation(text: String) { 73 | if (text.isEmpty()) return 74 | 75 | val encodedText = Uri.encode(text) 76 | 77 | val deepLAppIntent = Intent(Intent.ACTION_SEND).apply { 78 | type = "text/plain" 79 | putExtra(Intent.EXTRA_TEXT, text) 80 | setPackage("com.deepl.mobiletranslator") 81 | } 82 | 83 | val packageManager = context.packageManager 84 | val deepLAvailable = deepLAppIntent.resolveActivity(packageManager) != null 85 | 86 | if (deepLAvailable) { 87 | context.startActivity(deepLAppIntent) 88 | } else { 89 | val deepLUrl = "https://www.deepl.com/translator#auto/en/$encodedText" 90 | val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse(deepLUrl)) 91 | context.startActivity(webIntent) 92 | } 93 | } 94 | 95 | private fun searchOnGoogle(text: String) { 96 | if (text.isEmpty()) return 97 | 98 | val encodedText = Uri.encode(text) 99 | val searchUrl = "https://www.google.com/search?q=$encodedText" 100 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(searchUrl)) 101 | context.startActivity(intent) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/component/AppBadge.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.component 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.DisabledByDefault 13 | import androidx.compose.material.icons.filled.RestoreFromTrash 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.graphics.vector.ImageVector 22 | import androidx.compose.ui.text.TextStyle 23 | import androidx.compose.ui.tooling.preview.Preview 24 | import androidx.compose.ui.unit.dp 25 | import androidx.compose.ui.unit.sp 26 | import io.github.samolego.canta.util.RemovalRecommendation 27 | 28 | @Composable 29 | fun RemovalBadge(type: RemovalRecommendation) { 30 | AppBadge( 31 | label = type.name, 32 | icon = type.icon, 33 | color = type.badgeColor 34 | ) 35 | } 36 | 37 | @Composable 38 | fun SystemBadge() { 39 | RemovalBadge(type = RemovalRecommendation.SYSTEM) 40 | } 41 | 42 | @Composable 43 | fun DisabledBadge() { 44 | AppBadge( 45 | label = "DISABLED", 46 | icon = Icons.Default.DisabledByDefault, 47 | color = MaterialTheme.colorScheme.tertiary, 48 | ) 49 | } 50 | 51 | @Composable 52 | fun CantaBadge() { 53 | AppBadge( 54 | label = "CANTA", 55 | icon = Icons.Default.RestoreFromTrash, 56 | color = Color.Red.copy(alpha = 0.7f), 57 | ) 58 | } 59 | 60 | @Composable 61 | private fun AppBadge( 62 | label: String, 63 | icon: ImageVector, 64 | color: Color, 65 | ) { 66 | val contrastColor = color.getContrastColor() 67 | Row( 68 | modifier = Modifier 69 | .padding(all = 4.dp) 70 | .background( 71 | color, 72 | shape = RoundedCornerShape(16.dp) 73 | ) 74 | ) { 75 | Icon( 76 | icon, 77 | tint = contrastColor, 78 | modifier = Modifier 79 | .padding(start = 4.dp) 80 | .padding(vertical = 2.dp) 81 | .size(16.dp) 82 | .align(alignment = Alignment.CenterVertically), 83 | contentDescription = label, 84 | ) 85 | Spacer(modifier = Modifier.width(4.dp)) 86 | Text( 87 | text = label, 88 | modifier = Modifier 89 | .padding(end = 8.dp) 90 | .align(alignment = Alignment.CenterVertically), 91 | style = TextStyle( 92 | fontSize = 8.sp, 93 | color = contrastColor, 94 | ) 95 | ) 96 | } 97 | } 98 | 99 | private fun Color.getContrastColor(): Color { 100 | val luminance = (0.113 * red + 0.587 * green + 0.114 * blue) 101 | return if (luminance > 0.5) Color.Black else Color.White 102 | } 103 | 104 | @Preview 105 | @Composable 106 | fun BadgePreviews() { 107 | Column { 108 | for (removal in RemovalRecommendation.entries) { 109 | RemovalBadge(removal) 110 | } 111 | DisabledBadge() 112 | } 113 | } -------------------------------------------------------------------------------- /docs/presets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Canta - Presets 3 | description: Learn how to use presets to manage app uninstall lists across devices. 4 | --- 5 | 6 | # Presets 7 | 8 | Presets are a powerful feature in Canta that allows you to create, manage, and share collections of apps for uninstallation. This feature is particularly useful for applying the same set of app removals across multiple devices or sharing your bloatware removal configurations with others. 9 | 10 | ## What are presets? 11 | 12 | A preset is a saved collection of apps that you want to uninstall. Each preset contains: 13 | 14 | - **Name**: A descriptive name for your preset (e.g., "Samsung Bloatware") 15 | - **Description**: Optional details about what the preset removes 16 | - **App List**: The specific apps (package names) to be uninstalled 17 | - **Creation Date**: When the preset was created 18 | - **Version**: Format version for compatibility 19 | 20 | ## Creating presets 21 | 22 | ### From Uninstalled Apps 23 | 24 | The easiest way to create a preset is from apps you've already uninstalled: 25 | 26 | 1. **Uninstall the apps** you want to include in your preset 27 | 2. Navigate to the **Presets** tab in Canta 28 | 3. Tap the **Create Preset** button 29 | 4. Enter a **name** for your preset (required) 30 | 5. Add an optional **description** explaining what the preset removes 31 | 6. Tap **Save** 32 | 33 | ::: tip 34 | The preset will automatically capture all currently uninstalled apps on your device. Make sure you've uninstalled exactly the apps you want before creating the preset. 35 | ::: 36 | 37 | ## Managing Presets 38 | 39 | ### Viewing Your Presets 40 | 41 | When you navigate to the Presets tab, you'll see a comprehensive list of all your saved presets. Each preset entry displays the preset name and description, the creation date showing when it was originally made, the number of apps included in that preset, and an action menu that provides various management options for each individual preset. 42 | 43 | ### Applying Presets 44 | 45 | To use a preset on your current device: 46 | 47 | 1. Find the preset you want to apply 48 | 2. Tap the **Apply Preset** option 49 | 3. Canta will select all available apps from the preset 50 | 4. Review the selected apps in the main app list 51 | 5. Uninstall the selected apps as usual 52 | 53 | ::: warning Important 54 | When applying presets, only apps that exist on your current device will be selected. Apps that aren't installed or don't exist on your device will be skipped. 55 | ::: 56 | 57 | ## Import & Export 58 | 59 | 60 | Sharing and importing presets is simple and flexible in Canta. To **export** a preset, find it in your presets list and tap the **Share** option to copy it as JSON to your clipboard, which you can then paste and share with others. To **import** presets, tap **Import Preset** and choose either the **Clipboard** tab to import directly from copied JSON data, or the **Text** tab to manually paste preset data into the text field before tapping **Import**. 61 | 62 | ### JSON Format 63 | 64 | Presets are exported in JSON format for easy sharing. Here's what a preset JSON looks like: 65 | 66 | ```json 67 | { 68 | "name": "Samsung Bloatware", 69 | "description": "Removes Samsung's duplicate apps and unnecessary services", 70 | "createdDate": 1699123456789, 71 | "version": "1.0", 72 | "apps": [ 73 | { 74 | "packageName": "com.samsung.android.bixby.agent" 75 | }, 76 | { 77 | "packageName": "com.samsung.android.app.spage" 78 | } 79 | ] 80 | } 81 | ``` 82 | 83 | --- 84 | 85 | Presets make Canta even more powerful by allowing you to systematically manage app removal across devices. Whether you're setting up multiple devices, sharing your expertise, or simply want to backup your uninstall preferences, presets provide a robust solution for that case. 86 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/component/fab/ExpandableFAB.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.component.fab 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Add 9 | import androidx.compose.material.icons.filled.Download 10 | import androidx.compose.material3.FloatingActionButton 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.draw.clip 21 | import androidx.compose.ui.draw.rotate 22 | import androidx.compose.ui.unit.dp 23 | import io.github.samolego.canta.ui.component.IconClickButton 24 | 25 | @Composable 26 | fun ExpandableFAB( 27 | onBottomClick: () -> Unit, 28 | onTopClick: () -> Unit, 29 | modifier: Modifier = Modifier 30 | ) { 31 | var isExpanded by remember { mutableStateOf(false) } 32 | 33 | val rotation by 34 | animateFloatAsState( 35 | targetValue = if (isExpanded) 90f else 0f, 36 | animationSpec = tween(300, easing = FastOutSlowInEasing), 37 | label = "rotation" 38 | ) 39 | 40 | val spacingAnimation by 41 | animateDpAsState( 42 | targetValue = if (isExpanded) 8.dp else 0.dp, 43 | animationSpec = tween(300, easing = FastOutSlowInEasing), 44 | label = "spacing" 45 | ) 46 | 47 | val actionButtonScale by 48 | animateFloatAsState( 49 | targetValue = if (isExpanded) 1f else 0f, 50 | animationSpec = tween(300, easing = FastOutSlowInEasing), 51 | label = "actionButtonScale" 52 | ) 53 | 54 | Box( 55 | modifier = modifier.clip( 56 | shape = RoundedCornerShape(16.dp), 57 | ), 58 | contentAlignment = Alignment.BottomEnd, 59 | ) { 60 | Column( 61 | modifier = Modifier.background( 62 | color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f), 63 | ), 64 | horizontalAlignment = Alignment.CenterHorizontally, 65 | verticalArrangement = Arrangement.spacedBy(spacingAnimation) 66 | ) { 67 | // Import button 68 | if (isExpanded) { 69 | IconClickButton( 70 | onClick = { 71 | onTopClick() 72 | isExpanded = false 73 | }, 74 | icon = Icons.Default.Download, 75 | contentDescription = "Top click", 76 | scale = actionButtonScale 77 | ) 78 | } 79 | // Main FAB 80 | FloatingActionButton( 81 | modifier = Modifier.rotate(rotation), 82 | onClick = { 83 | if (isExpanded) { 84 | onBottomClick() 85 | } 86 | isExpanded = !isExpanded 87 | }, 88 | ) { 89 | Icon( 90 | Icons.Default.Add, 91 | contentDescription = if (isExpanded) "Bottom click" else "More actions", 92 | ) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docs/download.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Canta - Download 3 | description: Download information for Canta. 4 | --- 5 | # Download Canta 6 | 7 | Choose your preferred download source: 8 | 9 | ## F-Droid 10 | [![F-Droid](https://img.shields.io/f-droid/v/io.github.samolego.canta?include_prereleases&style=for-the-badge&logo=F-Droid&color=blue)](https://f-droid.org/en/packages/io.github.samolego.canta/) 11 | 12 | ## IzzyOnDroid 13 | [![IzzyOnDroid](https://img.shields.io/endpoint?&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAADAFBMVEUA0////wAA0v8A0v8A0////wD//wAFz/QA0/8A0/8A0/8A0/8A0v///wAA0/8A0/8A0/8A0/8A0//8/gEA0/8A0/8B0/4A0/8A0/8A0/+j5QGAwwIA0//C9yEA0/8A0/8A0/8A0/8A0/8A0/+n4SAA0/8A0/8A0/+o6gCw3lKt7QCv5SC+422b3wC19AC36zAA0/+d1yMA0/8A0/+W2gEA0/+w8ACz8gCKzgG7+QC+9CFLfwkA0/8A0////wAA0/8A0/8A0/8A0/+f2xym3iuHxCGq5BoA1P+m2joI0vONyiCz3mLO7oYA0/8M1Piq3Ei78CbB8EPe8LLj9Ly751G77zWQ1AC96UYC0fi37CL//wAA0/8A0////wD//wCp3jcA0/+j3SGj2i/I72Sx4zHE8FLB8zak1kYeycDI6nRl3qEA0/7V7psA0v6WzTa95mGi2RvB5XkPy9zH5YJ3uwGV1yxVihRLiwdxtQ1ZkAf//wD//wD//wD//wD//wCn5gf//wD//wD//wD//wD//wAA0/+h4A3R6p8A0/+X1w565OD6/ARg237n9csz2vPz+gNt37V/vifO8HW68B/L6ZOCwxXY8KRQsWRzhExAtG/E612a1Rd/pTBpmR9qjysduKVhmxF9mTY51aUozK+CsDSA52T//wD//wAA0////wD//wBJ1JRRxFWjzlxDyXRc0pGT1wCG0CWB3VGUzSTh8h6c0TSr5CCJ5FFxvl6s4H3m8xML0/DA5CvK51EX1N+Y2gSt4Dag3ChE3fax2ki68yO57NF10FRZnUPl88eJxhuCxgCz5EOLwEGf1DFutmahzGW98x0W1PGk3R154MHE6bOn69qv3gy92oG90o+Hn07B7rhCmiyMwECv1nO+0pQfwrCo57xF2daXsVhKrEdenQAduaee1Bsjr42z5D9RoCXy+QNovXpy2Z5MtWDO/TiSukaF3UtE1K6j3B4YwLc5wXlzpyIK0u5zy3uJqg4pu5RTpkZmpVKyAP8A0wBHcExHcEyBUSeEAAABAHRSTlP///9F9wjAAxD7FCEGzBjd08QyEL39abMd6///8P/ZWAnipIv/cC6B//7////////L/1Dz/0D///////86/vYnquY3/v///5T//v///17///////////////84S3QNB/8L/////////////7r/////NP////9l/////wPD4yis/x7Ym2lWSP+em////0n////////v///////////////////7//7pdGN3Urr6/+v/6aT////+//H/o2P/1v+7r7jp4PM/3p4g////g///K///481LxO///v////9w////8v/////9/p3J///a+P9v/5KR/+n///+p/xf//8P//wAAe7FyaAAABCZJREFUSMdj+E8iYKBUgwIHnwQ3N7cEHxcH+///VayoAE0Dh41qR7aBnCIQ8MsJKHH9/99czYYMWlA0cIkJGjMgAKfq//9RNYzIgLcBWYOTiCgDMhDn+B9bh6LebiWyH6L5UZQzONoAHWSHoqEpDkkDsyKqelv1//9rG1HUN9YihZK9AKp6BkG+/6xNqA5ajhSsCkrIipmYGGRa//9vQXVQXSySBnkWJOUMfn5Myuz/G3hR1NdEIUUchwiy+bkTsg4dbW/fu6W/e1c3XMMy5JiOZkFxUFZo74mgKTqaKXu0+2HqVwkja3BH9kFu361JwcHTfPJD4mdfe8ULAdVRyGlJAcVFfg+CQOozZ4XrJ85+JgwBsVXIGriQw5Tp4ZScezd8JiWnBupru30qwJZa+ZAjmWlC8fUZM4qB6kPnLNSPLMWqQQ5ZQ5aOzs1HmamBaQHzFs6y+qAmJCTE8f9/QgKSBg4DJPWc6zVDQkIC09JkZSPD38kukpExFpT4z67uYI/QwCOOCCK/izvu5CWl6AcEWMnKWml7LWbKZfH9/99UkknQHhGsynDz+65eWXv3/JmJrq5eXienVlRUfH/z8VvCf45soKQIH1yDEQsszrp6gwq9C73T87xcXadKl5TkFev4A/2tygmSBqYXqAYJmK+ZuoJydDR1vP09DA0NOy2kpdML81+U/heCpH1JU3jig7lJ5nKOT4i/t6ZHkqGzs4lJmIVHfrj+JR4HqLQSD0yDkCNEpGNn5ix9D03/eJdElTZdKV2TpNOhkwt8YUlNUgimgV0dLMBvf1gz1MolPd5FRcVNSkpDQ8owJeBCDyIhrIDnOD5QcuIU+3/2QKSs9laQ+noNLS0zLWdtqyP7mBAFAw88TwsJgMuJYweBGjYngtWbmeuZOW+bvNQToUFOAlFqOBk4Ov3/L7Z60/aN0p1tUhpa5nqWlub7C3p2I9QzyAghlUvczOz/1fhzPT3XSIfpSmmYAdVbmm1gV0dSz8DSilpUQsqCddIWIA3meuZaJqdMJZEzl6gRqgZIWZAxUdoizERXN8yi5MltcZTChzMaRQM3JNUWHS8rL/+yaPGvMmvr5ywoGoxtkDWwQ+Pb89ycBeWfGSJeL/la+RS1eOPnRtbQKgMRjZg+t8x6PkP273nWQAoFOPAgaeAThKXAmXMrK39Kmr5fsuBlBqoXfJGLe3VbmHjG9Mczi9T//3h7vygXtcDlQtJg44iQiIjIBRbGPO7gghPJy0ZIxT2HOLIUgwxQzsgYrUR350HSIMaJLidhgKY+mw+pflBDrX8E7OGBjPCAPc76gQFSTqAIiYrb/8dRP4CyosJ/rmwU5XIxHMilt4QBJwsSkBMClxOQULBlkRRwEONmR2kJcDGjADX2/+xO8r5iqjExqmLyrWpcPFRta1BfAwCtyN3XpuJ4RgAAAABJRU5ErkJggg==&url=https://apt.izzysoft.de/fdroid/api/v1/shield/io.github.samolego.canta&style=for-the-badge&label=IzzyOnDroid&color=blue)](https://apt.izzysoft.de/fdroid/index/apk/io.github.samolego.canta) 14 | 15 | ## GitHub 16 | [![GitHub](https://img.shields.io/github/v/release/samolego/canta?include_prereleases&style=for-the-badge&logo=GitHub&label=GitHub&color=blue)](https://github.com/samolego/Canta/releases/latest/) 17 | 18 | ## Verify Your Download 19 | 20 | You can verify the authenticity of downloaded APKs using this SHA-256 certificate fingerprint: 21 | ``` 22 | 0A:26:40:31:7C:43:27:21:88:C3:E1:31:94:C1:54:60:69:1F:12:C3:9E:A1:9B:BA:72:7D:D6:7F:B5:62:89:D4 23 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Canta - Settings 3 | description: Configure Canta settings for optimal use 4 | --- 5 | # Settings 6 | 7 | Canta offers several configuration options to customize your experience. This page explains each setting and how it affects app functionality. 8 | 9 | ## Available Settings 10 | 11 |
12 | Settings screen 13 |
14 | Canta settings screen 15 |
16 |
17 | 18 | ### Auto-update Bloat List 19 | 20 | **Description:** When enabled, Canta automatically checks for and downloads the latest app classification data from the [Universal Debloater Alliance](https://github.com/Universal-Debloater-Alliance/universal-android-preinstalled-lists) repository. 21 | 22 | - **Enabled (Default):** Ensures you have the most up-to-date information about apps, including newly identified bloatware and revised safety recommendations. 23 | - **Disabled:** Canta will use only the locally stored bloat list data without checking for updates. 24 | 25 | ::: tip 26 | Enable this setting to ensure you have the most accurate information about which apps are safe to remove. 27 | ::: 28 | 29 | ### Confirm Before Uninstall 30 | 31 | **Description:** Shows a confirmation dialog before uninstalling selected apps. 32 | 33 | - **Enabled (Default):** Displays a confirmation dialog showing the number of apps you're about to uninstall, helping prevent accidental removals. 34 | - **Disabled:** Immediately proceeds with uninstallation when you tap the trash icon, without asking for confirmation. 35 | 36 | ::: warning 37 | We recommend keeping this enabled to avoid accidentally uninstalling important apps. 38 | ::: 39 | 40 | ## Advanced Settings 41 | 42 | Canta includes advanced settings for power users who need more control over the app's behavior. 43 | 44 | ### Allow Unsafe Selections 45 | 46 | **Description:** Controls whether you can select apps that are marked with the 🟣 **Unsafe** badge for uninstallation. 47 | You can bypass this restriction by using presets, too. 48 | 49 | - **Disabled (Default):** Apps marked as unsafe cannot be selected for uninstallation, protecting you from accidentally removing critical system components. 50 | - **Enabled:** Allows selection of unsafe apps, giving experienced users full control over what they can uninstall. 51 | 52 | ::: danger IMPORTANT 53 | Enabling this setting allows you to select apps that could break vital system functionality. Only enable this if you understand the risks and know exactly what you're doing. There's a high chance you'll experience bootlooping! 54 | ::: 55 | 56 | ### Bloat List URL 57 | 58 | **Description:** Specifies the source URL where Canta downloads app classification data, badges, and descriptions. 59 | 60 | - **Default:** Points to the Universal Debloater Alliance repository 61 | - **Custom:** You can specify an alternative source that follows the same data format 62 | 63 | This setting allows organizations or advanced users to maintain their own app classification databases. 64 | 65 | ### Commits URL 66 | 67 | **Description:** Defines where Canta checks for updates to the bloat list data. 68 | 69 | - **Default:** Points to the commits API of the Universal Debloater Alliance repository 70 | - **Custom:** Can be changed to track updates from alternative sources 71 | 72 | This works in conjunction with the "Auto-update Bloat List" setting to determine when new data is available. 73 | 74 | ::: tip ADVANCED USAGE 75 | The Bloat List URL and Commits URL settings are primarily intended for developers testing custom app databases. 76 | ::: 77 | 78 | ## Hidden Features 79 | 80 | ### Select All 81 | 82 | For advanced users, Canta includes a hidden "Select All" feature that can be enabled by tapping the version number in Settings multiple times. This feature adds a "Select All" option when having "recommended" filter applied. 83 | 84 | ::: warning CAUTION 85 | Use this feature carefully, as mass uninstallation could affect device functionality. 86 | ::: 87 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/dialog/UninstallDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.dialog 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.BasicAlertDialog 11 | import androidx.compose.material3.Checkbox 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TextButton 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import io.github.samolego.canta.R 27 | 28 | @OptIn(ExperimentalMaterial3Api::class) 29 | @Composable 30 | fun UninstallAppsDialog( 31 | appCount: Int, 32 | canResetToFactory: Boolean = false, 33 | onDismiss: () -> Unit, 34 | onAgree: (resetToFactory: Boolean) -> Unit, 35 | ) { 36 | var resetToFactory by remember { mutableStateOf(false) } 37 | 38 | BasicAlertDialog( 39 | modifier = Modifier 40 | .background(MaterialTheme.colorScheme.surfaceContainer, MaterialTheme.shapes.large), 41 | onDismissRequest = onDismiss, 42 | ) { 43 | Column( 44 | modifier = Modifier.padding(16.dp), 45 | verticalArrangement = Arrangement.spacedBy(8.dp) 46 | ) { 47 | Text(stringResource(R.string.are_you_sure_to_uninstall_apps, appCount)) 48 | 49 | if (canResetToFactory) { 50 | Row( 51 | modifier = Modifier 52 | .fillMaxWidth() 53 | .clickable { 54 | resetToFactory = !resetToFactory 55 | } 56 | .padding(vertical = 8.dp), 57 | verticalAlignment = Alignment.CenterVertically 58 | ) { 59 | Checkbox( 60 | checked = resetToFactory, 61 | onCheckedChange = { resetToFactory = it } 62 | ) 63 | Text( 64 | text = stringResource(R.string.reset_to_factory_version), 65 | style = MaterialTheme.typography.bodyMedium, 66 | modifier = Modifier.padding(start = 8.dp) 67 | ) 68 | } 69 | } 70 | 71 | Row( 72 | modifier = Modifier.fillMaxWidth(), 73 | horizontalArrangement = Arrangement.End 74 | ) { 75 | TextButton( 76 | onClick = { onAgree(resetToFactory) } 77 | ) { 78 | Text(stringResource(R.string.ok)) 79 | } 80 | TextButton( 81 | onClick = onDismiss 82 | ) { 83 | Text(stringResource(R.string.cancel)) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | @Preview 91 | @Composable 92 | fun UninstallAppsDialogPreview() { 93 | UninstallAppsDialog( 94 | appCount = 5, 95 | canResetToFactory = true, 96 | onDismiss = {}, 97 | onAgree = {} 98 | ) 99 | } 100 | 101 | @Preview 102 | @Composable 103 | fun UninstallAppsDialogRegularAppPreview() { 104 | UninstallAppsDialog( 105 | appCount = 1, 106 | canResetToFactory = false, 107 | onDismiss = {}, 108 | onAgree = {} 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/component/Dropdown.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.component 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.expandVertically 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.animation.shrinkVertically 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.filled.ExpandLess 15 | import androidx.compose.material.icons.filled.ExpandMore 16 | import androidx.compose.material3.Card 17 | import androidx.compose.material3.CardDefaults 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.unit.dp 28 | 29 | @Composable 30 | fun Dropdown( 31 | header: @Composable (Boolean) -> Unit, 32 | content: @Composable () -> Unit, 33 | modifier: Modifier = Modifier, 34 | expanded: Boolean? = null, 35 | onExpandedChange: ((Boolean) -> Unit)? = null, 36 | headerBackgroundColor: Color = MaterialTheme.colorScheme.background, 37 | contentBackgroundColor: Color = MaterialTheme.colorScheme.background, 38 | ) { 39 | var internalExpanded by remember { mutableStateOf(false) } 40 | val isExpanded = expanded ?: internalExpanded 41 | val onExpansionChange = onExpandedChange ?: { internalExpanded = it } 42 | 43 | Column(modifier = modifier) { 44 | Card( 45 | modifier = Modifier.fillMaxWidth().clickable { onExpansionChange(!isExpanded) }, 46 | colors = CardDefaults.cardColors(containerColor = headerBackgroundColor) 47 | ) { header(isExpanded) } 48 | 49 | AnimatedVisibility( 50 | visible = isExpanded, 51 | enter = expandVertically() + fadeIn(), 52 | exit = shrinkVertically() + fadeOut() 53 | ) { 54 | Card( 55 | modifier = Modifier.fillMaxWidth().padding(top = 8.dp), 56 | colors = CardDefaults.cardColors(containerColor = contentBackgroundColor) 57 | ) { Column { content() } } 58 | } 59 | } 60 | } 61 | 62 | @Composable 63 | fun DropdownHeader( 64 | title: String, 65 | subtitle: String? = null, 66 | showExpandIcon: Boolean = true, 67 | expanded: Boolean = false, 68 | modifier: Modifier = Modifier, 69 | content: @Composable () -> Unit = {} 70 | ) { 71 | Row( 72 | modifier = modifier.fillMaxWidth().padding(16.dp), 73 | horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween, 74 | verticalAlignment = androidx.compose.ui.Alignment.CenterVertically 75 | ) { 76 | Column(modifier = Modifier.weight(1f)) { 77 | androidx.compose.material3.Text( 78 | text = title, 79 | style = MaterialTheme.typography.titleMedium 80 | ) 81 | if (subtitle != null) { 82 | androidx.compose.material3.Text( 83 | text = subtitle, 84 | style = MaterialTheme.typography.bodyMedium, 85 | color = MaterialTheme.colorScheme.onSurfaceVariant 86 | ) 87 | } 88 | } 89 | 90 | content() 91 | 92 | if (showExpandIcon) { 93 | Icon( 94 | imageVector = 95 | if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, 96 | contentDescription = if (expanded) "Collapse" else "Expand" 97 | ) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/dialog/NoWarrantyDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.dialog 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.rememberScrollState 11 | import androidx.compose.foundation.verticalScroll 12 | import androidx.compose.material3.BasicAlertDialog 13 | import androidx.compose.material3.Checkbox 14 | import androidx.compose.material3.ExperimentalMaterial3Api 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Surface 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.TextButton 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.saveable.rememberSaveable 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.res.stringResource 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.window.DialogProperties 31 | import io.github.samolego.canta.R 32 | 33 | @OptIn(ExperimentalMaterial3Api::class) 34 | @Composable 35 | fun NoWarrantyDialog( 36 | onProceed: (neverShowAgain: Boolean) -> Unit, 37 | onCancel: () -> Unit 38 | ) { 39 | var neverShowAgain by rememberSaveable { mutableStateOf(false) } 40 | 41 | BasicAlertDialog( 42 | onDismissRequest = onCancel, 43 | properties = DialogProperties( 44 | dismissOnBackPress = false, 45 | dismissOnClickOutside = false 46 | ) 47 | ) { 48 | Surface( 49 | shape = MaterialTheme.shapes.large, 50 | color = MaterialTheme.colorScheme.surfaceContainer 51 | ) { 52 | Column( 53 | modifier = Modifier 54 | .padding(24.dp) 55 | .verticalScroll(rememberScrollState()) 56 | ) { 57 | Text( 58 | text = stringResource(R.string.disclaimer), 59 | style = MaterialTheme.typography.headlineSmall, 60 | fontWeight = FontWeight.Bold 61 | ) 62 | 63 | Spacer(modifier = Modifier.height(16.dp)) 64 | 65 | Text( 66 | text = stringResource(R.string.no_warranty_content), 67 | style = MaterialTheme.typography.bodyMedium 68 | ) 69 | 70 | Spacer(modifier = Modifier.height(16.dp)) 71 | 72 | Text( 73 | text = stringResource(R.string.proceed_at_own_risk), 74 | style = MaterialTheme.typography.bodyMedium, 75 | fontWeight = FontWeight.Bold 76 | ) 77 | 78 | Spacer(modifier = Modifier.height(24.dp)) 79 | 80 | Row( 81 | verticalAlignment = Alignment.CenterVertically 82 | ) { 83 | Checkbox( 84 | checked = neverShowAgain, 85 | onCheckedChange = { 86 | neverShowAgain = it 87 | } 88 | ) 89 | Text( 90 | text = stringResource(R.string.never_show_again), 91 | style = MaterialTheme.typography.bodyMedium 92 | ) 93 | } 94 | 95 | Spacer(modifier = Modifier.height(8.dp)) 96 | 97 | Row( 98 | modifier = Modifier.fillMaxWidth(), 99 | horizontalArrangement = Arrangement.End 100 | ) { 101 | TextButton( 102 | onClick = onCancel 103 | ) { 104 | Text(stringResource(R.string.cancel)) 105 | } 106 | 107 | TextButton( 108 | onClick = { onProceed(neverShowAgain) } 109 | ) { 110 | Text(stringResource(R.string.proceed)) 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/menu/FiltersMenu.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.menu 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.ArrowDropDown 12 | import androidx.compose.material.icons.filled.ArrowDropUp 13 | import androidx.compose.material3.Checkbox 14 | import androidx.compose.material3.DropdownMenu 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Surface 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.unit.dp 28 | import io.github.samolego.canta.R 29 | import io.github.samolego.canta.ui.viewmodel.AppListViewModel 30 | import io.github.samolego.canta.util.apps.Filter 31 | 32 | @Composable 33 | fun FiltersMenu( 34 | showMenu: Boolean, 35 | onDismiss: () -> Unit, 36 | appListViewModel: AppListViewModel, 37 | ) { 38 | var filtersMenu by remember { mutableStateOf(false) } 39 | 40 | DropdownMenu( 41 | expanded = showMenu, 42 | onDismissRequest = onDismiss, 43 | modifier = Modifier.width(180.dp) 44 | ) { 45 | // System apps toggle 46 | FilterChip( 47 | text = stringResource(R.string.only_system), 48 | isSelected = appListViewModel.onlySystem, 49 | onClick = { appListViewModel.onlySystem = !appListViewModel.onlySystem }, 50 | trailingContent = { 51 | Checkbox( 52 | checked = appListViewModel.onlySystem, 53 | onCheckedChange = { appListViewModel.onlySystem = it } 54 | ) 55 | } 56 | ) 57 | 58 | // Filter submenu trigger 59 | FilterChip( 60 | text = appListViewModel.selectedFilter.name, 61 | isSelected = filtersMenu, 62 | onClick = { filtersMenu = !filtersMenu }, 63 | trailingContent = { 64 | Icon( 65 | if (filtersMenu) Icons.Default.ArrowDropUp 66 | else Icons.Default.ArrowDropDown, 67 | contentDescription = null 68 | ) 69 | } 70 | ) 71 | 72 | if (filtersMenu) { 73 | Filter.availableFilters.forEach { filter -> 74 | FilterChip( 75 | text = filter.name, 76 | isSelected = appListViewModel.selectedFilter == filter, 77 | onClick = { 78 | appListViewModel.selectedFilter = filter 79 | filtersMenu = false 80 | } 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | 87 | @Composable 88 | private fun FilterChip( 89 | text: String, 90 | isSelected: Boolean, 91 | onClick: () -> Unit, 92 | trailingContent: @Composable (() -> Unit)? = null 93 | ) { 94 | Surface( 95 | modifier = 96 | Modifier.fillMaxWidth() 97 | .padding(horizontal = 8.dp, vertical = 4.dp) 98 | .clickable(onClick = onClick), 99 | color = 100 | if (isSelected) MaterialTheme.colorScheme.primaryContainer 101 | else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f), 102 | shape = RoundedCornerShape(8.dp) 103 | ) { 104 | Row( 105 | modifier = Modifier.fillMaxWidth().padding(8.dp), 106 | horizontalArrangement = Arrangement.SpaceBetween, 107 | verticalAlignment = Alignment.CenterVertically 108 | ) { 109 | Text( 110 | text = text, 111 | style = MaterialTheme.typography.bodyMedium, 112 | modifier = Modifier.weight(1f) 113 | ) 114 | trailingContent?.invoke() 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | 3 | // Custom color theme 4 | const cantaRed = { 5 | primary: "#ff4444", 6 | secondary: "#ff6b6b", 7 | accent: "#ff0000", 8 | foreground: "#333333", 9 | background: "#ffffff", 10 | 11 | // Dark mode variants 12 | darkPrimary: "#ff6666", 13 | darkSecondary: "#ff8c8c", 14 | darkAccent: "#ff3333", 15 | darkForeground: "#ffffff", 16 | darkBackground: "#121212", 17 | }; 18 | 19 | export default defineConfig({ 20 | title: "Canta", 21 | base: "/Canta/", 22 | description: "Uninstall any app without root!", 23 | head: [ 24 | ["meta", { name: "author", content: "samo_lego" }], 25 | [ 26 | "meta", 27 | { 28 | name: "keywords", 29 | content: "canta, android, uninstall, debloat, shizuku, app", 30 | }, 31 | ], 32 | ["meta", { property: "og:type", content: "website" }], 33 | [ 34 | "meta", 35 | { 36 | property: "og:title", 37 | content: "Canta - Uninstall any app without root", 38 | }, 39 | ], 40 | [ 41 | "meta", 42 | { 43 | property: "og:image", 44 | content: 45 | "https://raw.githubusercontent.com/samolego/Canta/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png", 46 | }, 47 | ], 48 | [ 49 | "meta", 50 | { property: "og:url", content: "https://samolego.github.io/Canta" }, 51 | ], 52 | [ 53 | "meta", 54 | { 55 | property: "og:description", 56 | content: "Uninstall any Android app without root access using Shizuku", 57 | }, 58 | ], 59 | ["meta", { name: "twitter:card", content: "summary" }], 60 | // Favicon 61 | [ 62 | "link", 63 | { 64 | rel: "icon", 65 | href: "https://raw.githubusercontent.com/samolego/Canta/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png", 66 | }, 67 | ], 68 | ], 69 | sitemap: { 70 | hostname: "https://samolego.github.io/Canta", 71 | }, 72 | lastUpdated: true, 73 | // Theme customization 74 | themeConfig: { 75 | logo: "https://raw.githubusercontent.com/samolego/Canta/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png", 76 | nav: [ 77 | { text: "Home", link: "/" }, 78 | { text: "Install", link: "/install" }, 79 | { text: "Features", link: "/features" }, 80 | { text: "Presets", link: "/presets" }, 81 | { text: "Download", link: "/download" }, 82 | ], 83 | 84 | search: { 85 | provider: "local", 86 | }, 87 | 88 | editLink: { 89 | pattern: "https://github.com/samolego/Canta/edit/master/site/:path", 90 | text: "Edit this page on GitHub", 91 | }, 92 | 93 | sidebar: [ 94 | { 95 | text: "Getting Started", 96 | items: [ 97 | { text: "Setup", link: "/install" }, 98 | { text: "Usage", link: "/usage" }, 99 | { text: "Settings", link: "/settings" }, 100 | ], 101 | }, 102 | { 103 | text: "Advanced Features", 104 | items: [ 105 | { text: "Features", link: "/features" }, 106 | { text: "Presets", link: "/presets" }, 107 | ], 108 | }, 109 | ], 110 | 111 | socialLinks: [ 112 | { icon: "github", link: "https://github.com/samolego/Canta" }, 113 | ], 114 | 115 | footer: { 116 | message: "Released under the LGPL-3.0 License.", 117 | copyright: "Copyright © samo_lego", 118 | }, 119 | }, 120 | 121 | // CSS customization 122 | appearance: "dark", 123 | 124 | // Theme colors 125 | vite: { 126 | css: { 127 | preprocessorOptions: { 128 | scss: { 129 | additionalData: ` 130 | :root { 131 | --vp-c-brand: ${cantaRed.primary}; 132 | --vp-c-brand-light: ${cantaRed.secondary}; 133 | --vp-c-brand-lighter: ${cantaRed.accent}; 134 | --vp-c-brand-dark: ${cantaRed.darkPrimary}; 135 | --vp-c-brand-darker: ${cantaRed.darkSecondary}; 136 | 137 | --vp-home-hero-name-color: ${cantaRed.primary}; 138 | --vp-c-text-1: ${cantaRed.foreground}; 139 | --vp-c-text-2: ${cantaRed.foreground}; 140 | } 141 | 142 | .dark { 143 | --vp-c-brand: ${cantaRed.darkPrimary}; 144 | --vp-c-brand-light: ${cantaRed.darkSecondary}; 145 | --vp-c-brand-lighter: ${cantaRed.darkAccent}; 146 | --vp-c-brand-dark: ${cantaRed.primary}; 147 | --vp-c-brand-darker: ${cantaRed.secondary}; 148 | 149 | --vp-home-hero-name-color: ${cantaRed.darkPrimary}; 150 | --vp-c-text-1: ${cantaRed.darkForeground}; 151 | --vp-c-text-2: ${cantaRed.darkForeground}; 152 | } 153 | `, 154 | }, 155 | }, 156 | }, 157 | }, 158 | }); 159 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.FileInputStream 2 | import java.util.Properties 3 | 4 | plugins { 5 | id("com.android.application") 6 | id("org.jetbrains.kotlin.android") 7 | id("kotlin-parcelize") 8 | alias(libs.plugins.compose.compiler) 9 | alias(libs.plugins.protobuf) 10 | } 11 | 12 | val keystoreProperties = Properties() 13 | val keystorePropertiesFile: File = rootProject.file("key.properties") 14 | 15 | if (keystorePropertiesFile.exists()) { 16 | keystoreProperties.load(FileInputStream(keystorePropertiesFile)) 17 | } 18 | 19 | android { 20 | namespace = "io.github.samolego.canta" 21 | compileSdk = 36 22 | 23 | // For reproducible builds 24 | dependenciesInfo { 25 | // Disables dependency metadata when building APKs. 26 | includeInApk = false 27 | // Disables dependency metadata when building Android App Bundles. 28 | includeInBundle = false 29 | } 30 | 31 | defaultConfig { 32 | applicationId = namespace 33 | minSdk = 28 // todo - figure out a way to bypass hidden api methods on android < 9 34 | targetSdk = 35 35 | versionCode = project.property("version_code")?.toString()?.toInt() ?: 1 36 | versionName = project.property("version_name")?.toString() ?: "1.0.0" 37 | 38 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 39 | vectorDrawables { useSupportLibrary = true } 40 | 41 | buildConfigField("String", "APP_VERSION", "\"${project.property("version_name")}\"") 42 | buildConfigField("int", "VERSION_CODE", "${project.property("version_code")}") 43 | } 44 | 45 | signingConfigs { 46 | create("release") { 47 | keyAlias = keystoreProperties["keyAlias"].toString() 48 | keyPassword = keystoreProperties["keyPassword"].toString() 49 | storeFile = 50 | if (keystoreProperties["storeFile"] != null) 51 | keystoreProperties["storeFile"]?.let { file(it) } 52 | else null 53 | storePassword = keystoreProperties["storePassword"].toString() 54 | } 55 | } 56 | 57 | buildTypes { 58 | release { 59 | isMinifyEnabled = true 60 | isShrinkResources = true 61 | proguardFiles( 62 | getDefaultProguardFile("proguard-android-optimize.txt"), 63 | "proguard-rules.pro" 64 | ) 65 | signingConfig = signingConfigs.getByName("release") 66 | } 67 | } 68 | compileOptions { 69 | sourceCompatibility = JavaVersion.VERSION_1_8 70 | targetCompatibility = JavaVersion.VERSION_1_8 71 | } 72 | kotlinOptions { jvmTarget = "1.8" } 73 | buildFeatures { 74 | compose = true 75 | buildConfig = true 76 | } 77 | // composeOptions { 78 | // kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() 79 | // } 80 | packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } 81 | } 82 | 83 | dependencies { 84 | implementation(libs.kotlin.stdlib) 85 | implementation(libs.androidx.core.ktx) 86 | implementation(libs.androidx.datastore.preferences) 87 | implementation(libs.androidx.lifecycle.runtime.ktx) 88 | implementation(libs.androidx.activity.compose) 89 | implementation(platform(libs.androidx.compose.bom)) 90 | implementation(libs.androidx.ui) 91 | implementation(libs.androidx.ui.graphics) 92 | implementation(libs.androidx.ui.tooling.preview) 93 | implementation(libs.androidx.material3.android) 94 | implementation(libs.androidx.navigation.runtime.ktx) 95 | implementation(libs.androidx.navigation.compose) 96 | testImplementation(libs.junit) 97 | androidTestImplementation(libs.androidx.junit) 98 | androidTestImplementation(libs.androidx.espresso.core) 99 | androidTestImplementation(platform(libs.androidx.compose.bom)) 100 | androidTestImplementation(libs.androidx.ui.test.junit4) 101 | debugImplementation(libs.androidx.ui.tooling) 102 | debugImplementation(libs.androidx.ui.test.manifest) 103 | implementation(libs.coil.compose) 104 | implementation(libs.androidx.material.icons.extended) 105 | 106 | implementation(libs.api) 107 | implementation(libs.provider) 108 | 109 | implementation(libs.androidx.datastore.core) 110 | implementation(libs.protobuf.javalite) 111 | implementation(libs.hiddenapibypass) 112 | implementation(libs.androidx.biometric) 113 | } 114 | 115 | protobuf { 116 | protoc { 117 | artifact = libs.protobuf.protoc.get().toString() 118 | } 119 | generateProtoTasks { 120 | all().forEach { task -> 121 | task.builtins { 122 | create("java") { 123 | option("lite") 124 | } 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/component/SettingsItem.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.component 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.text.KeyboardOptions 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.OutlinedTextField 14 | import androidx.compose.material3.Switch 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.vector.ImageVector 20 | import androidx.compose.ui.text.input.KeyboardType 21 | import androidx.compose.ui.unit.dp 22 | 23 | @Composable 24 | fun SettingsItem( 25 | title: String, 26 | description: String? = null, 27 | icon: ImageVector? = null, 28 | isSwitch: Boolean = false, 29 | checked: Boolean = false, 30 | onClick: (() -> Unit)? = null, 31 | onCheckedChange: ((Boolean) -> Unit)? = null 32 | ) { 33 | Row( 34 | modifier = Modifier 35 | .fillMaxWidth() 36 | .clickable( 37 | enabled = onClick != null || (isSwitch && onCheckedChange != null), 38 | onClick = { 39 | onClick?.invoke() 40 | if (isSwitch) { 41 | onCheckedChange?.invoke(!checked) 42 | } 43 | } 44 | ) 45 | .padding(16.dp), 46 | verticalAlignment = Alignment.CenterVertically 47 | ) { 48 | if (icon != null) { 49 | Icon( 50 | imageVector = icon, 51 | contentDescription = null, 52 | modifier = Modifier.padding(end = 16.dp).size(24.dp), 53 | tint = MaterialTheme.colorScheme.primary 54 | ) 55 | } 56 | 57 | Column( 58 | modifier = Modifier.weight(1f) 59 | ) { 60 | Text( 61 | text = title, 62 | style = MaterialTheme.typography.titleMedium 63 | ) 64 | if (description != null) { 65 | Text( 66 | text = description, 67 | style = MaterialTheme.typography.bodyMedium, 68 | color = MaterialTheme.colorScheme.onSurfaceVariant 69 | ) 70 | } 71 | } 72 | 73 | if (isSwitch) { 74 | Box(modifier = Modifier.padding(4.dp)) { 75 | Switch(checked = checked, onCheckedChange = onCheckedChange) 76 | } 77 | } 78 | } 79 | } 80 | 81 | @Composable 82 | fun SettingsTextItem( 83 | title: String, 84 | description: String? = null, 85 | icon: ImageVector? = null, 86 | value: String, 87 | onValueChange: (String) -> Unit, 88 | placeholder: String? = null, 89 | keyboardType: KeyboardType = KeyboardType.Text, 90 | ) { 91 | Row(modifier = Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.Top) { 92 | if (icon != null) { 93 | Icon( 94 | imageVector = icon, 95 | contentDescription = null, 96 | modifier = Modifier.padding(end = 16.dp, top = 12.dp).size(24.dp), 97 | tint = MaterialTheme.colorScheme.primary 98 | ) 99 | } 100 | 101 | Column(modifier = Modifier.weight(1f)) { 102 | Text( 103 | text = title, 104 | style = MaterialTheme.typography.titleMedium, 105 | modifier = Modifier.padding(bottom = 4.dp) 106 | ) 107 | if (description != null) { 108 | Text( 109 | text = description, 110 | style = MaterialTheme.typography.bodyMedium, 111 | color = MaterialTheme.colorScheme.onSurfaceVariant, 112 | modifier = Modifier.padding(bottom = 8.dp) 113 | ) 114 | } 115 | OutlinedTextField( 116 | value = value, 117 | onValueChange = onValueChange, 118 | modifier = Modifier.fillMaxWidth(), 119 | placeholder = placeholder?.let { { Text(it, softWrap = false) } }, 120 | keyboardOptions = KeyboardOptions(keyboardType = keyboardType), 121 | singleLine = true, 122 | ) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/dialog/SuccessDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.dialog 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.rememberScrollState 14 | import androidx.compose.foundation.verticalScroll 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.filled.Euro 17 | import androidx.compose.material3.BasicAlertDialog 18 | import androidx.compose.material3.Button 19 | import androidx.compose.material3.ExperimentalMaterial3Api 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.material3.Surface 23 | import androidx.compose.material3.Text 24 | import androidx.compose.material3.TextButton 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.platform.LocalContext 29 | import androidx.compose.ui.res.pluralStringResource 30 | import androidx.compose.ui.res.stringResource 31 | import androidx.compose.ui.text.font.FontWeight 32 | import androidx.compose.ui.tooling.preview.Preview 33 | import androidx.compose.ui.unit.dp 34 | import androidx.compose.ui.window.DialogProperties 35 | import io.github.samolego.canta.R 36 | 37 | private const val DONATE_URL = "https://www.paypal.com/donate/?hosted_button_id=FD4R46ZZ5EWME" 38 | 39 | @OptIn(ExperimentalMaterial3Api::class) 40 | @Composable 41 | fun SuccessDialog( 42 | count: Int, 43 | isReinstall: Boolean = false, 44 | onDismissRequest: () -> Unit 45 | ) { 46 | val context = LocalContext.current 47 | 48 | BasicAlertDialog( 49 | onDismissRequest = onDismissRequest, 50 | properties = DialogProperties( 51 | dismissOnBackPress = false, 52 | dismissOnClickOutside = false 53 | ) 54 | ) { 55 | Surface( 56 | shape = MaterialTheme.shapes.large, 57 | color = MaterialTheme.colorScheme.surfaceContainer 58 | ) { 59 | Column( 60 | modifier = Modifier 61 | .padding(24.dp) 62 | .verticalScroll(rememberScrollState()) 63 | ) { 64 | Text( 65 | text = stringResource(R.string.success, "\uD83C\uDF89"), 66 | style = MaterialTheme.typography.headlineSmall, 67 | fontWeight = FontWeight.Bold, 68 | ) 69 | 70 | Spacer(modifier = Modifier.height(16.dp)) 71 | 72 | Text( 73 | text = pluralStringResource( 74 | if (isReinstall) R.plurals.success_reinstalled else R.plurals.success_uninstalled, 75 | count, 76 | count 77 | ), 78 | style = MaterialTheme.typography.bodyMedium, 79 | fontWeight = FontWeight.Bold, 80 | ) 81 | 82 | Spacer(modifier = Modifier.height(16.dp)) 83 | 84 | Text( 85 | text = stringResource(R.string.canta_donate_request), 86 | style = MaterialTheme.typography.bodyMedium, 87 | ) 88 | 89 | Spacer(modifier = Modifier.height(24.dp)) 90 | 91 | Row( 92 | modifier = Modifier.fillMaxWidth(), 93 | horizontalArrangement = Arrangement.End 94 | ) { 95 | TextButton( 96 | onClick = onDismissRequest 97 | ) { 98 | Text(stringResource(R.string.cancel)) 99 | } 100 | 101 | Button( 102 | onClick = { 103 | onDismissRequest() 104 | // Launch URL for donations 105 | val donationIntent = Intent(Intent.ACTION_VIEW, Uri.parse(DONATE_URL)) 106 | context.startActivity(donationIntent) 107 | } 108 | ) { 109 | Row( 110 | verticalAlignment = Alignment.CenterVertically, 111 | ) { 112 | Text(stringResource(R.string.donate)) 113 | Icon( 114 | Icons.Default.Euro, 115 | modifier = Modifier 116 | .padding(start = 4.dp) 117 | .size(12.dp), 118 | contentDescription = "Donate", 119 | ) 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | @Preview 129 | @Composable 130 | private fun TestSuccessDialog() { 131 | SuccessDialog( 132 | count = 3, 133 | onDismissRequest = {} 134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/util/BloatUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.util 2 | 3 | import android.os.Parcelable 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.Android 6 | import androidx.compose.material.icons.filled.Check 7 | import androidx.compose.material.icons.filled.Close 8 | import androidx.compose.material.icons.filled.Settings 9 | import androidx.compose.material.icons.filled.Warning 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.vector.ImageVector 12 | import kotlinx.parcelize.Parcelize 13 | import org.json.JSONObject 14 | import java.io.File 15 | import java.net.URL 16 | 17 | const val DEFAULT_BLOAT_URL = 18 | "https://raw.githubusercontent.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/main/resources/assets/uad_lists.json" 19 | const val DEFAULT_BLOAT_COMMITS_URL = 20 | "https://api.github.com/repos/Universal-Debloater-Alliance/universal-android-debloater-next-generation/commits?path=resources%2Fassets%2Fuad_lists.json" 21 | 22 | /** 23 | * Parse commits to get latest commit hash 24 | */ 25 | fun parseLatestHash(commits: String): String { 26 | val c = commits.substringAfter("\"sha\":\"") 27 | return c.substringBefore("\"") 28 | } 29 | 30 | private const val TAG = "BloatUtils" 31 | 32 | class BloatUtils { 33 | fun fetchBloatList( 34 | uadList: File, 35 | bloatUrl: String = DEFAULT_BLOAT_URL, 36 | commitsUrl: String = DEFAULT_BLOAT_COMMITS_URL 37 | ): Pair { 38 | try { 39 | // Fetch json from bloatUrl and parse it 40 | val response = URL(bloatUrl).readText() 41 | // Parse response to json 42 | val json = JSONObject(response) 43 | 44 | val commits = URL(commitsUrl).readText() 45 | 46 | val hash = parseLatestHash(commits) 47 | 48 | // Write json to file 49 | uadList.writeText(json.toString()) 50 | 51 | LogUtils.i(TAG, "Successfully fetched latest bloat list.") 52 | 53 | return Pair(json, hash) 54 | } catch (e: Exception) { 55 | LogUtils.e(TAG, "Failed to fetch bloat list", e) 56 | return Pair(JSONObject(), "") 57 | } 58 | } 59 | 60 | fun checkForUpdates( 61 | latestBloatHash: String, 62 | commitsUrl: String = DEFAULT_BLOAT_COMMITS_URL 63 | ): Boolean { 64 | return try { 65 | val commits = URL(commitsUrl).readText() 66 | val hash = parseLatestHash(commits) 67 | 68 | val needsUpdate = hash != latestBloatHash 69 | LogUtils.i(TAG, "Bloat list needs update: $needsUpdate (commit hash: $hash)") 70 | 71 | return needsUpdate 72 | } catch (e: Exception) { 73 | LogUtils.e(TAG, "Failed to check for updates", e) 74 | false 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * App bloat information, parsed from the UAD json. 81 | */ 82 | @Parcelize 83 | data class BloatData( 84 | internal val installData: InstallData?, 85 | internal val description: String?, 86 | internal val removal: RemovalRecommendation?, 87 | ) : Parcelable { 88 | companion object { 89 | fun fromJson(json: JSONObject): BloatData { 90 | val installData = InstallData.byNameIgnoreCaseOrNull(json.getString("list")) 91 | val description = json.getString("description") 92 | val removal = RemovalRecommendation.byNameIgnoreCaseOrNull(json.getString("removal")) 93 | 94 | return BloatData(installData, description, removal) 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * Enum class to represent the removal recommendation, from the UAD list. 101 | */ 102 | enum class RemovalRecommendation( 103 | val icon: ImageVector, 104 | val badgeColor: Color, 105 | val description: String 106 | ) { 107 | RECOMMENDED( 108 | Icons.Default.Check, 109 | Color.Green, 110 | "Pointless or outright negative packages, and/or apps available through Google Play." 111 | ), 112 | ADVANCED( 113 | Icons.Default.Settings, 114 | Color.Yellow, 115 | "Breaks obscure or minor parts of functionality, or apps that aren't easily enabled/installed through Settings/Google Play. This category is also used for apps that are useful (default keyboard/gallery/launcher/music app.) but that can easily be replaced by a better alternative." 116 | ), 117 | EXPERT( 118 | Icons.Default.Warning, 119 | Color.Red, 120 | "Breaks widespread and/or important functionality, but nothing important to the basic operation of the operating system. Removing an 'Expert' package should not bootloop the device (unless mentioned in the description) but we can't guarantee it 100%." 121 | ), 122 | UNSAFE( 123 | Icons.Default.Close, 124 | Color.Magenta, 125 | "Can break vital parts of the operating system. Removing an 'Unsafe' package have an extremely high risk of bootlooping your device." 126 | ), 127 | SYSTEM( 128 | Icons.Default.Android, 129 | Color.DarkGray, 130 | "System apps are apps that come pre-installed with your device." 131 | ); 132 | 133 | companion object { 134 | fun byNameIgnoreCaseOrNull(input: String): RemovalRecommendation? { 135 | return entries.firstOrNull { it.name.equals(input, true) } 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Represents the install data from the UAD list. 142 | */ 143 | enum class InstallData { 144 | OEM, 145 | CARRIER; 146 | 147 | companion object { 148 | fun byNameIgnoreCaseOrNull(input: String): InstallData? { 149 | return entries.firstOrNull { it.name.equals(input, true) } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/dialog/preset/PresetEditDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.dialog.preset 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.platform.LocalContext 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.unit.dp 12 | import io.github.samolego.canta.R 13 | import io.github.samolego.canta.ui.viewmodel.AppListViewModel 14 | import io.github.samolego.canta.ui.viewmodel.PresetsViewModel 15 | import io.github.samolego.canta.util.CantaPresetData 16 | 17 | @Composable 18 | private fun PresetDialog( 19 | initialName: String, 20 | initialDescription: String, 21 | onDismiss: () -> Unit, 22 | onConfirm: (name: String, description: String) -> Unit 23 | ) { 24 | var name by remember { mutableStateOf(initialName) } 25 | var description by remember { mutableStateOf(initialDescription) } 26 | var nameError by remember { mutableStateOf(false) } 27 | 28 | AlertDialog( 29 | onDismissRequest = onDismiss, 30 | title = { 31 | Text( 32 | text = stringResource(R.string.create_preset), 33 | style = MaterialTheme.typography.headlineSmall, 34 | fontWeight = FontWeight.Bold 35 | ) 36 | }, 37 | text = { 38 | Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { 39 | Text( 40 | text = stringResource(R.string.create_preset_description), 41 | style = MaterialTheme.typography.bodyMedium, 42 | color = MaterialTheme.colorScheme.onSurfaceVariant 43 | ) 44 | 45 | OutlinedTextField( 46 | value = name, 47 | onValueChange = { 48 | name = it 49 | nameError = it.isBlank() 50 | }, 51 | label = { Text(stringResource(R.string.preset_name)) }, 52 | placeholder = { Text(stringResource(R.string.preset_name_placeholder)) }, 53 | isError = nameError, 54 | supportingText = 55 | if (nameError) { 56 | { Text(stringResource(R.string.preset_name_missing_error)) } 57 | } else null, 58 | modifier = Modifier.fillMaxWidth(), 59 | singleLine = true 60 | ) 61 | 62 | OutlinedTextField( 63 | value = description, 64 | onValueChange = { description = it }, 65 | label = { Text(stringResource(R.string.optional_description)) }, 66 | placeholder = { Text(stringResource(R.string.preset_description_placeholder)) }, 67 | modifier = Modifier.fillMaxWidth(), 68 | maxLines = 3 69 | ) 70 | } 71 | }, 72 | confirmButton = { 73 | Button( 74 | onClick = { 75 | if (name.isNotBlank()) { 76 | onConfirm(name.trim(), description.trim()) 77 | } else { 78 | nameError = true 79 | } 80 | }, 81 | enabled = name.isNotBlank() 82 | ) { Text(stringResource(R.string.save)) } 83 | }, 84 | dismissButton = { 85 | TextButton(onClick = onDismiss) { 86 | Text(stringResource(R.string.cancel)) 87 | } 88 | } 89 | ) 90 | } 91 | 92 | @Composable 93 | fun PresetCreateDialog( 94 | appListViewModel: AppListViewModel, 95 | presetViewModel: PresetsViewModel, 96 | closeDialog: () -> Unit, 97 | ) { 98 | val context = LocalContext.current 99 | val presetSaveErrorText = stringResource(R.string.preset_save_error) 100 | PresetDialog( 101 | initialName = "", 102 | initialDescription = "", 103 | onDismiss = closeDialog, 104 | onConfirm = { name, description -> 105 | presetViewModel.savePreset( 106 | name = name, 107 | description = description, 108 | apps = appListViewModel.appList.filter { it.isUninstalled }.map { it.packageName } 109 | .toSet(), 110 | onSuccess = { closeDialog() }, 111 | onError = { 112 | Toast.makeText( 113 | context, 114 | presetSaveErrorText, 115 | Toast.LENGTH_SHORT 116 | ) 117 | .show() 118 | }, 119 | ) 120 | } 121 | ) 122 | } 123 | 124 | @Composable 125 | fun PresetEditDialog( 126 | preset: CantaPresetData, 127 | presetViewModel: PresetsViewModel, 128 | closeDialog: () -> Unit, 129 | ) { 130 | val context = LocalContext.current 131 | val presetSaveErrorText = stringResource(R.string.preset_save_error) 132 | PresetDialog( 133 | initialName = preset.name, 134 | initialDescription = preset.description, 135 | onDismiss = closeDialog, 136 | onConfirm = { name, description -> 137 | presetViewModel.updatePreset( 138 | oldPreset = preset, 139 | newName = name, 140 | newDescription = description, 141 | onSuccess = { closeDialog() }, 142 | onError = { 143 | Toast.makeText( 144 | context, 145 | presetSaveErrorText, 146 | Toast.LENGTH_SHORT 147 | ) 148 | .show() 149 | }, 150 | ) 151 | } 152 | ) 153 | } 154 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/screen/LogsPage.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.screen 2 | 3 | import android.widget.Toast 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.foundation.ExperimentalFoundationApi 6 | import androidx.compose.foundation.combinedClickable 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.rememberScrollState 14 | import androidx.compose.foundation.shape.RoundedCornerShape 15 | import androidx.compose.foundation.verticalScroll 16 | import androidx.compose.material.icons.Icons 17 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 18 | import androidx.compose.material.icons.filled.ContentCopy 19 | import androidx.compose.material3.ExperimentalMaterial3Api 20 | import androidx.compose.material3.FloatingActionButton 21 | import androidx.compose.material3.Icon 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.material3.Scaffold 24 | import androidx.compose.material3.Surface 25 | import androidx.compose.material3.Text 26 | import androidx.compose.material3.TopAppBar 27 | import androidx.compose.material3.TopAppBarDefaults 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.runtime.mutableStateOf 31 | import androidx.compose.runtime.remember 32 | import androidx.compose.runtime.setValue 33 | import androidx.compose.ui.Modifier 34 | import androidx.compose.ui.platform.LocalClipboardManager 35 | import androidx.compose.ui.platform.LocalContext 36 | import androidx.compose.ui.res.stringResource 37 | import androidx.compose.ui.text.AnnotatedString 38 | import androidx.compose.ui.unit.dp 39 | import io.github.samolego.canta.R 40 | import io.github.samolego.canta.ui.component.IconClickButton 41 | import io.github.samolego.canta.util.LogUtils 42 | 43 | @OptIn(ExperimentalMaterial3Api::class) 44 | @Composable 45 | fun LogsPage(onNavigateBack: () -> Unit) { 46 | val clipboardManager = LocalClipboardManager.current 47 | val logs = LogUtils.getLogs() 48 | 49 | Scaffold( 50 | topBar = { 51 | TopAppBar( 52 | colors = TopAppBarDefaults.topAppBarColors( 53 | containerColor = MaterialTheme.colorScheme.primaryContainer 54 | ), 55 | title = { Text(stringResource(R.string.logs)) }, 56 | navigationIcon = { 57 | IconClickButton( 58 | onClick = onNavigateBack, 59 | icon = Icons.AutoMirrored.Filled.ArrowBack, 60 | contentDescription = stringResource(R.string.back), 61 | ) 62 | } 63 | ) 64 | }, 65 | floatingActionButton = { 66 | FloatingActionButton( 67 | onClick = { 68 | val logText = 69 | logs.joinToString("\n") { log -> 70 | "[${log.getFormattedTime()}] ${log.level} ${log.tag}: ${log.message}" 71 | } 72 | clipboardManager.setText(AnnotatedString(logText)) 73 | } 74 | ) { 75 | Icon( 76 | Icons.Default.ContentCopy, 77 | contentDescription = stringResource(R.string.copy_logs) 78 | ) 79 | } 80 | } 81 | ) { padding -> 82 | Column( 83 | modifier = 84 | Modifier.fillMaxSize() 85 | .padding(padding) 86 | .verticalScroll(rememberScrollState()) 87 | ) { logs.forEach { logEntry -> LogEntryChip(logEntry) } } 88 | } 89 | } 90 | 91 | @OptIn(ExperimentalFoundationApi::class) 92 | @Composable 93 | private fun LogEntryChip(logEntry: LogUtils.LogEntry) { 94 | var expanded by remember { mutableStateOf(false) } 95 | val clipboardManager = LocalClipboardManager.current 96 | val context = LocalContext.current 97 | 98 | Surface( 99 | modifier = Modifier 100 | .fillMaxWidth() 101 | .padding(top = 8.dp) 102 | .padding(horizontal = 8.dp) 103 | .combinedClickable( 104 | onClick = { expanded = !expanded }, 105 | onLongClick = { 106 | val logText = "[${logEntry.getFormattedTime()}] ${logEntry.level} ${logEntry.tag}: ${logEntry.message}" 107 | clipboardManager.setText(AnnotatedString(logText)) 108 | Toast.makeText(context, R.string.log_copied, Toast.LENGTH_SHORT).show() 109 | } 110 | ), 111 | color = logEntry.level.color.copy(alpha = 0.2f), 112 | shape = RoundedCornerShape(8.dp) 113 | ) { 114 | Column(modifier = Modifier.padding(8.dp)) { 115 | Row( 116 | modifier = Modifier.fillMaxWidth(), 117 | horizontalArrangement = Arrangement.SpaceBetween 118 | ) { 119 | Text( 120 | text = "${logEntry.level} ${logEntry.tag}", 121 | style = MaterialTheme.typography.bodyMedium 122 | ) 123 | Text( 124 | text = logEntry.getFormattedTime(), 125 | style = MaterialTheme.typography.bodySmall 126 | ) 127 | } 128 | 129 | AnimatedVisibility(visible = expanded) { 130 | Text( 131 | text = logEntry.message, 132 | style = MaterialTheme.typography.bodySmall, 133 | modifier = Modifier.padding(top = 4.dp) 134 | ) 135 | } 136 | } 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/samolego/canta/ui/viewmodel/PresetsViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.samolego.canta.ui.viewmodel 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.setValue 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import io.github.samolego.canta.data.PresetStore 12 | import io.github.samolego.canta.util.CantaPresetData 13 | import io.github.samolego.canta.util.LogUtils 14 | import kotlinx.coroutines.flow.SharingStarted 15 | import kotlinx.coroutines.flow.stateIn 16 | import kotlinx.coroutines.launch 17 | 18 | class PresetsViewModel : ViewModel() { 19 | 20 | companion object { 21 | private const val TAG = "PresetsViewModel" 22 | } 23 | 24 | var editingPreset by mutableStateOf(null) 25 | 26 | private lateinit var presetStore: PresetStore 27 | 28 | private val _presets = mutableStateOf>(emptyList()) 29 | val presets: List 30 | get() = _presets.value 31 | 32 | var isLoading by mutableStateOf(false) 33 | private set 34 | 35 | fun initialize(context: Context) { 36 | presetStore = PresetStore(context) 37 | // Collect presets flow and update state 38 | viewModelScope.launch { 39 | presetStore.presetsFlow.stateIn( 40 | scope = viewModelScope, 41 | started = SharingStarted.WhileSubscribed(5000), 42 | initialValue = emptyList() 43 | ) 44 | .collect { presetsList -> 45 | _presets.value = presetsList 46 | LogUtils.i(TAG, "Loaded ${presetsList.size} presets") 47 | } 48 | } 49 | } 50 | 51 | fun savePreset( 52 | name: String, 53 | description: String, 54 | apps: Set, 55 | onSuccess: () -> Unit, 56 | onError: () -> Unit 57 | ) { 58 | viewModelScope.launch { 59 | val preset = presetStore.createPresetFromUninstalledApps(apps, name, description) 60 | val success = presetStore.savePreset(preset) 61 | if (success) { 62 | onSuccess() 63 | } else { 64 | onError() 65 | LogUtils.e(TAG, "Failed to save preset ${preset.name}!") 66 | } 67 | } 68 | } 69 | 70 | fun deletePreset(preset: CantaPresetData, onSuccess: () -> Unit, onError: () -> Unit) { 71 | viewModelScope.launch { 72 | val success = presetStore.deletePreset(preset) 73 | if (success) { 74 | onSuccess() 75 | } else { 76 | onError() 77 | } 78 | } 79 | } 80 | 81 | fun exportToClipboard( 82 | context: Context, 83 | preset: CantaPresetData, 84 | ) { 85 | val jsonString = presetStore.exportToJson(preset) 86 | val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 87 | val clip = ClipData.newPlainText("Canta Preset", jsonString) 88 | clipboard.setPrimaryClip(clip) 89 | } 90 | 91 | fun importFromClipboard( 92 | context: Context, 93 | onSuccess: (CantaPresetData) -> Unit, 94 | onError: () -> Unit 95 | ) { 96 | val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 97 | val clipData = clipboard.primaryClip 98 | 99 | if (clipData != null && clipData.itemCount > 0) { 100 | val jsonString = clipData.getItemAt(0).text.toString() 101 | val preset = presetStore.importFromJson(jsonString) 102 | 103 | if (preset != null) { 104 | onSuccess(preset) 105 | } else { 106 | onError() 107 | } 108 | } else { 109 | onError() 110 | } 111 | } 112 | 113 | fun importFromJson( 114 | jsonString: String, 115 | onSuccess: (CantaPresetData) -> Unit, 116 | onError: () -> Unit 117 | ) { 118 | val preset = presetStore.importFromJson(jsonString) 119 | if (preset != null) { 120 | onSuccess(preset) 121 | } else { 122 | onError() 123 | } 124 | } 125 | 126 | fun formatDate(timestamp: Long): String { 127 | return presetStore.formatDate(timestamp) 128 | } 129 | 130 | fun updatePreset( 131 | oldPreset: CantaPresetData, 132 | newName: String, 133 | newDescription: String, 134 | onSuccess: () -> Unit, 135 | onError: () -> Unit 136 | ) { 137 | viewModelScope.launch { 138 | val updatedPreset = 139 | oldPreset.copy( 140 | name = newName, 141 | description = newDescription, 142 | apps = oldPreset.apps 143 | ) 144 | 145 | val success = presetStore.updatePreset(oldPreset, updatedPreset) 146 | if (success) { 147 | onSuccess() 148 | } else { 149 | onError() 150 | } 151 | } 152 | } 153 | 154 | fun setPresetApps( 155 | preset: CantaPresetData, 156 | newApps: Set, 157 | onSuccess: () -> Unit, 158 | onError: () -> Unit 159 | ) { 160 | viewModelScope.launch { 161 | val success = presetStore.setPresetApps(preset, newApps) 162 | if (success) { 163 | onSuccess() 164 | } else { 165 | onError() 166 | } 167 | } 168 | } 169 | 170 | fun saveImportedPreset(preset: CantaPresetData, onError: (() -> Unit)? = null) { 171 | viewModelScope.launch { 172 | val success = presetStore.savePreset(preset) 173 | if (!success) { 174 | LogUtils.e(TAG, "Failed to save imported preset") 175 | onError?.invoke() 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 取消 4 | 是的 5 | 您確定要解除安裝 %1$s 個應用程式嗎? 6 | 將包名複製到剪貼簿 7 | 沒有可用的描述 8 | 解除安裝 9 | 重新安裝 10 | 僅系統 11 | 沒有找到應用程式 12 | 正在載入應用程式 13 | 徽章資訊 14 | 知道了 15 | 正在載入徽章…… 16 | 日誌 17 | 返回 18 | 複製日誌 19 | 日誌已複製到剪貼簿 20 | 21 | 清除所選的 %d 個 22 | 23 | 24 | 已選擇 %d 個應用程式 25 | 26 | 27 | 你已成功解除安裝 %d 個應用程式! 28 | 29 | 30 | 你已成功重新安裝 %d 個應用程式! 31 | 32 | 複製 33 | 應用程式資訊 34 | 關閉 35 | Canta 是一個用於卸載應用的開源應用,它可憑藉 Shizuku 刪除系統和用戶應用程序。讓您無需電腦,隨心所欲地爲設備減負。\n\n特別感謝 %s。\n沒有他們的協助,Canta 就不會是現在這樣。 36 | 搜尋應用 37 | 自動更新膨脹軟體列表 38 | 自動取得新版本的應用程式的膨脹資訊,這保證了最新的徽章和應用程式描述。 39 | 設定 40 | 確認解除安裝 41 | 應用程式版本: %s 42 | 最新膨脹軟體列表 43 | 此應用程式根據第三方資訊提供應用程式刪除建議。這些建議僅供參考,並不保證您的特定設備的安全性或相容性。解除安裝系統應用程式可能會導致意外問題,包括但不限於:功能喪失、系統不穩定、裝置循環啟動、資料遺失等。 44 | 免責聲明 不負責任 45 | 點擊“繼續”,您同意自行承擔使用此應用程式的風險。 46 | 不要再顯示此訊息 47 | 繼續 48 | 是否顯示確認解除安裝的對話框。 49 | 全選 50 | 點選 %d 更多次即可啟用全選功能。 51 | 已啟用全選,過濾類型「推薦」。 52 | 卸載選項 53 | 卸載應用程式 54 | 重置應用程式為出廠版本 55 | 導入預設 56 | 從剪貼簿導入預設或直接粘貼 JSON 文字。 57 | 剪貼簿 58 | 從剪貼簿導入預設,確保您已複製有效的 JSON 文字的 Canta 預設。 59 | 粘貼預設 JSON 60 | 將 JSON 預設貼到此處…… 61 | 請輸入有效的 JSON 62 | 導入 63 | 建立預設 64 | 從當前卸載的應用程式創建預設,這將保存可在其他設備上共用和應用的應用程式清單。 65 | 預設名稱 66 | 必須填寫名稱 67 | 例如三星設備 68 | 描述(可選) 69 | 描述此預設刪除的內容…… 70 | 儲存 71 | 從剪貼簿導入 72 | 預設 73 | 預設已刪除 74 | 無法刪除預設 75 | 預設已複製到剪貼板 76 | 匯入失敗 77 | 無預設 78 | 創建或導入預設以跨設備管理應用程式卸載清單 79 | 更多選項 80 | 編輯 81 | 添加應用程式 82 | 分享 83 | 删除 84 | 應用預設 85 | 儲存預設時出錯 86 | 選定的應用程式 87 | 文本 88 | 安裝 Shizuku 應用程式 89 | 啟動 Shizuku 服務 90 | 在 Shizuku 中向 Canta 授予許可權 91 | 需要 Shizuku 92 | Canta 使用 Shizuku 卸載應用程式,而無需 root 許可權。Shizuku 提供了一種訪問系統級 SDK 的安全方法。 93 | 提交紀錄 URL 94 | 從哪裡取得更新的提交資訊 95 | 重設為預設值 96 | 進階設定 97 | 點擊以展開 98 | 膨脹清單 URL 99 | 從哪裡取得徽章與描述。 100 | 允許不安全的選取 101 | 是否允許選取被標記為不安全的應用程式(套用預設時已自動略過此限制)。 102 | 贊助 103 | 喜歡 Canta 嗎?這是我在閒暇時間打造、以隱私為核心的作品 - 沒有廣告,也沒有追蹤。如果你覺得它實用,請考慮支持它的開發! 104 | 成功 %1$s! 105 | 隱藏成功對話框 106 | 在解除安裝或重新安裝後隱藏成功對話框。 107 | 需要驗證 108 | 此操作需要更高的權限。 109 | 解除安裝或重新安裝應用程式時需要驗證。 110 | 需要驗證 111 | 112 | -------------------------------------------------------------------------------- /app/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Install Shizuku app 4 | Start Shizuku service 5 | Grant permission to Canta in Shizuku 8 | Shizuku Required 9 | Canta uses Shizuku to uninstall apps without requiring root access. Shizuku provides a secure way to access system-level SDKs. 12 | نمونه 13 | لغو کردن 14 | خوب 15 | آیا مطمئناً برنامه های 1 $ S را حذف نصب کرده اید؟ 18 | نام بسته را در کلیپ بورد کپی کنید 21 | هیچ توضیحی در دسترس نیست 22 | حذف کردن 23 | دوباره نصب کردن 24 | فقط سیستم 25 | هیچ برنامه ای پیدا نشده است 26 | برنامه های بارگیری 27 | اطلاعات نشان 28 | آن را 29 | در حال بارگیری نشان ... 30 | سیاهه ها 31 | پشت 32 | سیاهههای مربوطه 33 | ورود به سیستم در کلیپ بورد 34 | کپی کردن 35 | اطلاعات برنامه 36 | نزدیک 37 | کانتا یک برنامه حذف نصب منبع باز است که از Shizuku برای حذف سیستم و برنامه های کاربر استفاده می کند. این امکان را به شما می دهد تا دستگاه خود را همانطور که می خواهید ، از طریق رایانه شخصی مورد نیاز خود قرار دهید. \ n \ n special به لطف ٪ s. 40 | برنامه های جستجو 41 | لیست بروزرسانی خودکار 42 | به طور خودکار نسخه جدید اطلاعات Bloat App را بدست آورید. این نشان می دهد که نشان های به روز و توضیحات برنامه. 45 | تنظیمات 46 | حذف را تأیید کنید 47 | نسخه برنامه: ٪ s 48 | آخرین لیست Bloatware 49 | این برنامه توصیه های حذف برنامه را بر اساس اطلاعات شخص ثالث ارائه می دهد. این توصیه ها از نظر ماهیت اطلاعاتی هستند و ایمنی یا سازگاری با دستگاه خاص شما را تضمین نمی کنند. حذف برنامه های سیستم می تواند باعث ایجاد مشکلات غیر منتظره ای شود ، از جمله اما محدود به این موارد نیست: از دست دادن عملکرد ، ناپایداری سیستم ، بوت لوپ های دستگاه ، از دست دادن داده ها و غیره. توسعه دهنده این برنامه مسئولیتی در مورد خسارت وارده به دستگاه شما ناشی از پیروی از توصیه های ارائه شده ندارد. 52 | سلب مسئولیت - بدون ضمانت 53 | با کلیک روی \ "ادامه \" ، شما موافقت می کنید از این برنامه در معرض خطر خود استفاده کنید. 56 | دوباره این پیام را نشان ندهید 57 | ادامه دادن 58 | اینکه آیا می توان گفتگوی را برای تأیید عملکرد حذف نشان داد. 61 | همه را انتخاب کنید 62 | برای فعال کردن همه قابلیت ها ، به d% بیشتر ضربه بزنید. 65 | انتخاب همه برای نوع فیلتر \ "توصیه شده" فعال شده است. 68 | گزینه های حذف را حذف کنید 69 | حذف برنامه 70 | بازنشانی به نسخه کارخانه 71 | Commits URL 72 | Where to get commit info for updates from 75 | Reset to default 76 | Advanced settings 77 | Click to expand 78 | Bloat list URL 79 | Where to get badges and descriptions from. 82 | Allow unsafe selections 83 | Whether to allow selecting apps marked as unsafe (applying presets bypasses this already). 86 | Donate 87 | Enjoying Canta? It\'s built in my free time with privacy in mind - no ads, no tracking. If you find it useful, consider supporting its development! 90 | Success %1$s! 91 | Hide success dialog 92 | Hides the success dialog after uninstall / reinstall. 93 | Authentication required 94 | This action requires higher priviliges. 95 | Require authentication for uninstalling or reinstalling apps. 96 | Require authentication 97 | 98 | You have successfully uninstalled %d app! 99 | You have successfully uninstalled %d apps! 100 | 101 | 102 | You have successfully reinstalled %d app! 103 | You have successfully reinstalled %d apps! 104 | 105 | 106 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 取消 4 | 是的 5 | 您确定要卸载 %1$s 个应用吗? 6 | 将包名复制到剪贴板 7 | 无可用描述 8 | 卸载 9 | 重新安装 10 | 仅显示系统应用 11 | 未找到应用 12 | 正在加载应用列表 13 | “特殊标志”信息 14 | 明白 15 | 正在加载特殊标志 …… 16 | 日志 17 | 返回 18 | 复制日志 19 | 日志已复制到剪贴板 20 | 21 | 清除对 %d 个应用的选中 22 | 23 | 24 | 已选择 %d 个应用 25 | 26 | 27 | 您已成功卸载 %d 个应用! 28 | 29 | 30 | 您已成功恢复 %d 个应用! 31 | 32 | 复制 33 | 应用信息 34 | 关闭 35 | Canta 是一个开源的卸载工具,可借助 Shizuku 删除系统和用户应用程序。通过Canta,您无需电脑辅助即可随心精简设备。\n\n特别感谢 %s。\n没有他们的协助,Canta 将无法做到如今的程度。 36 | 搜索应用 37 | 自动更新应用的标注列表 38 | 自动获取新版本的应用标注列表,这保证最新的特殊标志和应用程序描述。 39 | 设置 40 | 确认卸载 41 | 软件版本:%s 42 | 最新的应用标记列表 43 | 本应用基于第三方来源提供应用卸载的相关建议。这些建议仅供参考,不保证安全性,及与您的特定设备兼容。卸载系统应用可能导致意外问题,包括但不限于功能缺失、系统不稳定、设备无限重启、数据丢失等。对于您因遵循上述第三方卸载建议而造成的任何设备损坏,本应用开发者概不负责。 44 | 免责声明 - 无保障 45 | 点击“继续”,即表示您同意自行承担使用此应用程序的风险。 46 | 不再提示此信息 47 | 继续 48 | 是否显示用于确认卸载的显示框。 49 | 全选 50 | 再次点击 %d 以启用全选功能。 51 | 已为特殊标志类型 “Recommended” 启用全选。 52 | 卸载选项 53 | 卸载应用 54 | 恢复出厂版本 55 | 导入预设 56 | 从剪贴板导入预设或直接粘贴 JSON 文本。 57 | 剪贴板 58 | 从剪贴板导入预设,请确保您已复制有效的 JSON 格式的 Canta 预设。 59 | 粘贴 JSON 格式的预设 60 | 在此处粘贴 JSON 格式的预设…… 61 | 请输入有效的 JSON 62 | 导入 63 | 创建预设 64 | 从当前已卸载的软件创建预设。这将保存一个可以共享和应用到其他设备的软件列表。 65 | 预设名称 66 | 必须填写名称 67 | 例如三星设备的软件 68 | 描述 (可选) 69 | 描述此预设会删除的内容…… 70 | 保存 71 | 从剪贴板导入 72 | 预设 73 | 预设已删除 74 | 删除预设失败 75 | 预设已复制到剪贴板 76 | 导入失败 77 | 没有预设 78 | 创建或导入预设以跨设备管理应用程序卸载列表 79 | 更多选项 80 | 编辑 81 | 添加应用 82 | 分享 83 | 删除 84 | 添加预设 85 | 保存预设出错 86 | 选择应用 87 | 文本 88 | 安装 Shizuku 应用 89 | 激活 Shizuku 服务 90 | 在 Shizuku 应用中向 Canta 授予权限 91 | 需要 Shizuku 92 | Canta 使用 Shizuku 来卸载软件而无需 root 权限。因为 Shizuku 提供了访问系统级SDK的安全方法。 93 | 提交 URL 94 | 从哪里获取更新的提交信息 95 | 重置为默认设置 96 | 高级设置 97 | 点击以展开 98 | 软件标记列表 URL 99 | 从哪里获取特殊标志和描述。 100 | 允许选择 Unsafe 选项 101 | 是否允许选择被标记为 Unsafe 选项的应用(使用预设时无视此设置)。 102 | 捐助 103 | 你喜欢 Canta 吗?这个软件是在我空闲的时间中开发的,它注重隐私 - 无广告、无跟踪。如果觉得它好用,可以考虑一下支持这个软件的开发! 104 | 成功 %1$s! 105 | 把完成后显示的提示给隐藏 106 | 当你卸载/恢复应用时,隐藏显示成功的提示框。 107 | 需要身份认证 108 | 本次操作需要更高的权限。 109 | 要卸载或重新安装应用,需要身份认证。 110 | 需要身份认证 111 | 112 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Canta 4 | 5 | [![](https://raw.githubusercontent.com/samolego/Canta/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png)](https://samolego.github.io/Canta/) 6 | 7 | Uninstall any\* app without root! 8 | Powered by [Shizuku](https://shizuku.rikka.app/). 9 | 10 | ## Download 11 | 12 | [![](https://img.shields.io/f-droid/v/io.github.samolego.canta?include_prereleases&style=for-the-badge&logo=F-Droid&color=blue)](https://f-droid.org/en/packages/io.github.samolego.canta/) 13 | [![](https://img.shields.io/endpoint?&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAADAFBMVEUA0////wAA0v8A0v8A0////wD//wAFz/QA0/8A0/8A0/8A0/8A0v///wAA0/8A0/8A0/8A0/8A0//8/gEA0/8A0/8B0/4A0/8A0/8A0/+j5QGAwwIA0//C9yEA0/8A0/8A0/8A0/8A0/8A0/+n4SAA0/8A0/8A0/+o6gCw3lKt7QCv5SC+422b3wC19AC36zAA0/+d1yMA0/8A0/+W2gEA0/+w8ACz8gCKzgG7+QC+9CFLfwkA0/8A0////wAA0/8A0/8A0/8A0/+f2xym3iuHxCGq5BoA1P+m2joI0vONyiCz3mLO7oYA0/8M1Piq3Ei78CbB8EPe8LLj9Ly751G77zWQ1AC96UYC0fi37CL//wAA0/8A0////wD//wCp3jcA0/+j3SGj2i/I72Sx4zHE8FLB8zak1kYeycDI6nRl3qEA0/7V7psA0v6WzTa95mGi2RvB5XkPy9zH5YJ3uwGV1yxVihRLiwdxtQ1ZkAf//wD//wD//wD//wD//wCn5gf//wD//wD//wD//wD//wAA0/+h4A3R6p8A0/+X1w565OD6/ARg237n9csz2vPz+gNt37V/vifO8HW68B/L6ZOCwxXY8KRQsWRzhExAtG/E612a1Rd/pTBpmR9qjysduKVhmxF9mTY51aUozK+CsDSA52T//wD//wAA0////wD//wBJ1JRRxFWjzlxDyXRc0pGT1wCG0CWB3VGUzSTh8h6c0TSr5CCJ5FFxvl6s4H3m8xML0/DA5CvK51EX1N+Y2gSt4Dag3ChE3fax2ki68yO57NF10FRZnUPl88eJxhuCxgCz5EOLwEGf1DFutmahzGW98x0W1PGk3R154MHE6bOn69qv3gy92oG90o+Hn07B7rhCmiyMwECv1nO+0pQfwrCo57xF2daXsVhKrEdenQAduaee1Bsjr42z5D9RoCXy+QNovXpy2Z5MtWDO/TiSukaF3UtE1K6j3B4YwLc5wXlzpyIK0u5zy3uJqg4pu5RTpkZmpVKyAP8A0wBHcExHcEyBUSeEAAABAHRSTlP///9F9wjAAxD7FCEGzBjd08QyEL39abMd6///8P/ZWAnipIv/cC6B//7////////L/1Dz/0D///////86/vYnquY3/v///5T//v///17///////////////84S3QNB/8L/////////////7r/////NP////9l/////wPD4yis/x7Ym2lWSP+em////0n////////v///////////////////7//7pdGN3Urr6/+v/6aT////+//H/o2P/1v+7r7jp4PM/3p4g////g///K///481LxO///v////9w////8v/////9/p3J///a+P9v/5KR/+n///+p/xf//8P//wAAe7FyaAAABCZJREFUSMdj+E8iYKBUgwIHnwQ3N7cEHxcH+///VayoAE0Dh41qR7aBnCIQ8MsJKHH9/99czYYMWlA0cIkJGjMgAKfq//9RNYzIgLcBWYOTiCgDMhDn+B9bh6LebiWyH6L5UZQzONoAHWSHoqEpDkkDsyKqelv1//9rG1HUN9YihZK9AKp6BkG+/6xNqA5ajhSsCkrIipmYGGRa//9vQXVQXSySBnkWJOUMfn5Myuz/G3hR1NdEIUUchwiy+bkTsg4dbW/fu6W/e1c3XMMy5JiOZkFxUFZo74mgKTqaKXu0+2HqVwkja3BH9kFu361JwcHTfPJD4mdfe8ULAdVRyGlJAcVFfg+CQOozZ4XrJ85+JgwBsVXIGriQw5Tp4ZScezd8JiWnBupru30qwJZa+ZAjmWlC8fUZM4qB6kPnLNSPLMWqQQ5ZQ5aOzs1HmamBaQHzFs6y+qAmJCTE8f9/QgKSBg4DJPWc6zVDQkIC09JkZSPD38kukpExFpT4z67uYI/QwCOOCCK/izvu5CWl6AcEWMnKWml7LWbKZfH9/99UkknQHhGsynDz+65eWXv3/JmJrq5eXienVlRUfH/z8VvCf45soKQIH1yDEQsszrp6gwq9C73T87xcXadKl5TkFev4A/2tygmSBqYXqAYJmK+ZuoJydDR1vP09DA0NOy2kpdML81+U/heCpH1JU3jig7lJ5nKOT4i/t6ZHkqGzs4lJmIVHfrj+JR4HqLQSD0yDkCNEpGNn5ix9D03/eJdElTZdKV2TpNOhkwt8YUlNUgimgV0dLMBvf1gz1MolPd5FRcVNSkpDQ8owJeBCDyIhrIDnOD5QcuIU+3/2QKSs9laQ+noNLS0zLWdtqyP7mBAFAw88TwsJgMuJYweBGjYngtWbmeuZOW+bvNQToUFOAlFqOBk4Ov3/L7Z60/aN0p1tUhpa5nqWlub7C3p2I9QzyAghlUvczOz/1fhzPT3XSIfpSmmYAdVbmm1gV0dSz8DSilpUQsqCddIWIA3meuZaJqdMJZEzl6gRqgZIWZAxUdoizERXN8yi5MltcZTChzMaRQM3JNUWHS8rL/+yaPGvMmvr5ywoGoxtkDWwQ+Pb89ycBeWfGSJeL/la+RS1eOPnRtbQKgMRjZg+t8x6PkP273nWQAoFOPAgaeAThKXAmXMrK39Kmr5fsuBlBqoXfJGLe3VbmHjG9Mczi9T//3h7vygXtcDlQtJg44iQiIjIBRbGPO7gghPJy0ZIxT2HOLIUgwxQzsgYrUR350HSIMaJLidhgKY+mw+pflBDrX8E7OGBjPCAPc76gQFSTqAIiYrb/8dRP4CyosJ/rmwU5XIxHMilt4QBJwsSkBMClxOQULBlkRRwEONmR2kJcDGjADX2/+xO8r5iqjExqmLyrWpcPFRta1BfAwCtyN3XpuJ4RgAAAABJRU5ErkJggg==&url=https://apt.izzysoft.de/fdroid/api/v1/shield/io.github.samolego.canta&style=for-the-badge&label=IzzyOnDroid&color=blue)](https://apt.izzysoft.de/fdroid/index/apk/io.github.samolego.canta) 14 | [![](https://img.shields.io/github/v/release/samolego/canta?include_prereleases&style=for-the-badge&logo=GitHub&label=GitHub&color=blue)](https://github.com/samolego/Canta/releases/latest/) 15 | 16 | 17 | 18 | 19 | 20 | [![GitHub all releases](https://img.shields.io/github/downloads/samolego/Canta/total?label=Downloads&logo=github)](https://github.com/samolego/Canta/releases/) 21 | [badge](https://shields.rbtlog.dev/io.github.samolego.canta) 22 | [![donate badge](https://img.shields.io/badge/Donate_via-Paypal-blue)](https://www.paypal.com/donate/?hosted_button_id=FD4R46ZZ5EWME) 23 | --- 24 | 25 |
26 | 27 | > [!Warning] 28 | > **DISCLAIMER:** ⛔ Use at your own risk. I am not responsible for any data loss or damage caused by this app ⛔. 29 | 30 | --- 31 | 32 | ## 📖 About 33 | 34 | Canta allows you to **uninstall pre-installed or user apps without root**, by leveraging [Shizuku](https://shizuku.rikka.app/). 35 | It integrates with the [Universal Debloat List](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/) to provide safe recommendations. 36 | 37 | - ✅ No root required 38 | - ✅ Detects previously uninstalled apps (even across reinstalls) 39 | - ✅ Works on Android **9.0+ (SDK 28+)** 40 | - ⚠️ No *permanent* bricking (If you uninstall **critical apps**, you can experience bootloop and have to **factory reset**!) 41 | --- 42 | 43 | ## Screenshots 44 | 45 | | | | | 46 | |:---:|:---:|:---:| 47 | | Home | Search | Uninstall | 48 | | | | | 49 | | [App descriptions](https://github.com/Universal-Debloater-Alliance/universal-android-preinstalled-lists/) | See uninstalled | | 50 | 51 | 52 | ### Verification 53 | You can verify the authenticity of downloaded APKs using this SHA-256 certificate fingerprint: 54 | ``` 55 | 0A:26:40:31:7C:43:27:21:88:C3:E1:31:94:C1:54:60:69:1F:12:C3:9E:A1:9B:BA:72:7D:D6:7F:B5:62:89:D4 56 | ``` 57 | 58 | ## How-to 59 | 60 | * install [Shizuku](https://play.google.com/store/apps/details?id=moe.shizuku.privileged.api) 61 | & [activate it](https://shizuku.rikka.app/guide/setup/) 62 | * you can 63 | follow [this Android Police guide for more in-depth explanation](https://www.androidpolice.com/how-to-use-shizuku-for-adb-rootless-mods-on-any-android-device/) 64 | * install Canta 65 | * select an app and click the trash button 66 | 67 | 68 | ## Translations 69 | 70 | Do you want to help translate Canta? You can help us at [Crowdin](https://crowdin.com/project/canta)! Thank you 🙂! 71 | 72 | 73 | 74 | Crowdin logo 75 | 76 | 77 | https://crowdin.com/project/canta 78 | 79 | ## Thanks 80 | 81 | * [Universal-Debloater-Alliance](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/) 82 | for the debloat list 83 | * @RikkaApps for Shizuku 84 | * Icon created on [icon kitchen](https://icon.kitchen) 85 | 86 | ## Star History 87 | 88 | [![Star History Chart](https://api.star-history.com/svg?repos=samolego/Canta&type=Date)](https://star-history.com/#samolego/Canta&Date) 89 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Canta is an open source uninstaller app that uses Shizuku to remove system and user apps.\nIt allows you to debloat your device as you wish, no PC required.\n\nSpecial thanks to %s.\nCanta would\'t be the same without their work. 6 | Auto update bloat list 7 | Automatically fetch new version of app bloat info. This guarantees up-to-date badges and app descriptions. 10 | Settings 11 | Confirm uninstallations 12 | App version: %s 13 | Latest bloatware list 14 | This app provides app removal recommendations based on third-party information. These recommendations are informational in nature and do not guarantee safety or compatibility with your specific device. Uninstalling system apps can cause unexpected issues, including but not limited to: loss of functionality, system instability, device bootloops, data loss, etc. The developer of this app is not responsible for any damage done to your device resulting from following the recommendations provided. 17 | Disclaimer - No Warranty 18 | By clicking \"Proceed\", you agree to use this app at your own risk. 21 | Don\'t show this message again 22 | Proceed 23 | Whether to show the dialog for confirming uninstall action. 26 | Select all 27 | Tap %d more times to enable select all functionality. 30 | Select all has been enabled for filter type \"Recommended\". 33 | Import preset 34 | Import a preset from clipboard or paste JSON text directly. 37 | Clipboard 38 | Import preset from clipboard. Make sure you have copied a valid Canta preset JSON. 41 | Paste preset JSON 42 | Paste JSON preset here ... 43 | Please enter valid JSON 44 | Import 45 | Create preset 46 | Create a preset from currently uninstalled apps. This will save a list of apps that can be shared and applied on other devices. 49 | Preset name 50 | Name is required 51 | e.g., Samsung Bloatware 52 | Description (Optional) 53 | Describe what this preset removes... 56 | Save 57 | Import from clipboard 58 | Presets 59 | Preset deleted 60 | Failed to delete preset 61 | Preset copied to clipboard 62 | Import failed 63 | No presets 64 | Create or import presets to manage app uninstall lists across devices 67 | More options 68 | Edit 69 | Add apps 70 | Share 71 | Delete 72 | Apply Preset 73 | Error saving preset 74 | Selected apps 75 | Text 76 | Install Shizuku app 77 | Start Shizuku service 78 | Grant permission to Canta in Shizuku 81 | Shizuku Required 82 | Canta uses Shizuku to uninstall apps without requiring root access. Shizuku provides a secure way to access system-level SDKs. 85 | Commits URL 86 | Where to get commit info for updates from 89 | Reset to default 90 | Advanced settings 91 | Click to expand 92 | Bloat list URL 93 | Where to get badges and descriptions from. 96 | Allow unsafe selections 97 | Whether to allow selecting apps marked as unsafe (applying presets bypasses this already). 100 | Donate 101 | Enjoying Canta? It\'s built in my free time with privacy in mind - no ads, no tracking. If you find it useful, consider supporting its development! 104 | Success %1$s! 105 | Hide success dialog 106 | Hides the success dialog after uninstall / reinstall. 107 | Authentication required 108 | This action requires higher priviliges. 109 | Require authentication for uninstalling or reinstalling apps. 110 | Require authentication 111 | 112 | %d app selected 113 | %d apps selected 114 | 115 | 116 | You have successfully uninstalled %d app! 117 | You have successfully uninstalled %d apps! 118 | 119 | 120 | You have successfully reinstalled %d app! 121 | You have successfully reinstalled %d apps! 122 | 123 | 124 | --------------------------------------------------------------------------------