├── .nvmrc ├── .env.example ├── .npmrc ├── src ├── app.css ├── routes │ ├── +layout.ts │ ├── +page.svelte │ ├── devices │ │ └── [slug] │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ ├── +layout.svelte │ ├── import │ │ ├── import.ts │ │ └── +page.svelte │ ├── settings │ │ └── +page.svelte │ ├── about │ │ └── +page.svelte │ └── export │ │ ├── +page.svelte │ │ └── export.ts ├── lib │ ├── index.ts │ ├── users │ │ ├── models.ts │ │ ├── stores.ts │ │ └── UsersDropdown.svelte │ ├── assets │ │ └── images │ │ │ ├── sad.png │ │ │ └── sad.svg │ ├── error │ │ ├── index.ts │ │ ├── Error.svelte │ │ ├── stores.ts │ │ └── SadError.svelte │ ├── config │ │ ├── models.ts │ │ └── stores.ts │ ├── devices │ │ ├── models.ts │ │ ├── cache.ts │ │ ├── adb.ts │ │ ├── NoDeviceBanner.svelte │ │ ├── RefreshDevicesButton.svelte │ │ ├── stores.ts │ │ └── DevicesSidebarItems.svelte │ ├── utils.ts │ ├── Greet.svelte │ ├── Sad.svelte │ ├── NavBar.svelte │ ├── notifications │ │ ├── Notif.svelte │ │ ├── SadToast.svelte │ │ └── stores.ts │ ├── packages │ │ ├── models.ts │ │ ├── discussions.ts │ │ ├── RefreshPackagesButton.svelte │ │ ├── adb.ts │ │ ├── stores.ts │ │ ├── PackagesList.ts │ │ ├── PackageCSVEnablerDisabler.svelte │ │ ├── PackagesList.svelte │ │ └── FilterAndSearchPackages.svelte │ ├── BreadCrumbs.svelte │ └── Sidebar.svelte ├── app.d.ts └── app.html ├── src-tauri ├── .gitignore ├── migrations │ ├── 20230824195146_AddConfigTable.down.sql │ └── 20230824195146_AddConfigTable.up.sql ├── icons │ ├── 32x32.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── src │ ├── sad.rs │ ├── err.rs │ ├── cache.rs │ ├── db.rs │ ├── users.rs │ ├── adb_devices.rs.bak │ ├── config.rs │ ├── store.rs │ ├── events.rs │ ├── adb_cmd.rs │ ├── devices.rs │ ├── main.rs │ └── packages.rs ├── build.rs ├── .sqlx │ └── query-007072b14b2c5a841d491799fd5515d273fe38ddda9a9ab62bee6ec8759812ce.json ├── Cargo.toml └── tauri.conf.json ├── static ├── favicon.png └── screenshots │ ├── discussion.png │ ├── settings.png │ ├── export_packages.png │ ├── import_packages.png │ ├── sad_alpha_v0.1.png │ ├── sad_beta_v0.1_a.png │ ├── sad_beta_v0.1_b.png │ ├── sad_beta_v0.1_c.png │ ├── sad_beta_v0.1_d.png │ ├── sad_alpha_v0.2_a.png │ ├── sad_alpha_v0.2_b.png │ ├── sad_beta_0.1_usage.png │ ├── bulk_disable_packages.png │ └── sad_v0.3.0-beta_usage.gif ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── postcss.config.js ├── .eslintignore ├── .prettierignore ├── vite.config.ts ├── .gitignore ├── .prettierrc ├── scripts ├── package.json ├── updater_template.js ├── discussions_dump.js └── package-lock.json ├── svelte.config.js ├── .eslintrc.cjs ├── tsconfig.json ├── tailwind.config.js ├── LICENSE.md ├── .github └── workflows │ ├── discussions_dump.yml │ ├── main.yml │ └── updater.yml ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.16.0 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SQLX_OFFLINE=true -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false; 2 | // export const prerender = true; 3 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/favicon.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "./src-tauri/Cargo.toml" 4 | ] 5 | } -------------------------------------------------------------------------------- /src-tauri/migrations/20230824195146_AddConfigTable.down.sql: -------------------------------------------------------------------------------- 1 | -- Add down migration script here 2 | DROP TABLE config; -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src/lib/users/models.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string; 3 | name: string; 4 | device_id: string; 5 | }; 6 | 7 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src/lib/assets/images/sad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src/lib/assets/images/sad.png -------------------------------------------------------------------------------- /static/screenshots/discussion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/discussion.png -------------------------------------------------------------------------------- /static/screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/settings.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /static/screenshots/export_packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/export_packages.png -------------------------------------------------------------------------------- /static/screenshots/import_packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/import_packages.png -------------------------------------------------------------------------------- /static/screenshots/sad_alpha_v0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/sad_alpha_v0.1.png -------------------------------------------------------------------------------- /static/screenshots/sad_beta_v0.1_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/sad_beta_v0.1_a.png -------------------------------------------------------------------------------- /static/screenshots/sad_beta_v0.1_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/sad_beta_v0.1_b.png -------------------------------------------------------------------------------- /static/screenshots/sad_beta_v0.1_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/sad_beta_v0.1_c.png -------------------------------------------------------------------------------- /static/screenshots/sad_beta_v0.1_d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/sad_beta_v0.1_d.png -------------------------------------------------------------------------------- /static/screenshots/sad_alpha_v0.2_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/sad_alpha_v0.2_a.png -------------------------------------------------------------------------------- /static/screenshots/sad_alpha_v0.2_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/sad_alpha_v0.2_b.png -------------------------------------------------------------------------------- /static/screenshots/sad_beta_0.1_usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/sad_beta_0.1_usage.png -------------------------------------------------------------------------------- /static/screenshots/bulk_disable_packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/bulk_disable_packages.png -------------------------------------------------------------------------------- /static/screenshots/sad_v0.3.0-beta_usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thulasi-ram/simple_android_debloater/HEAD/static/screenshots/sad_v0.3.0-beta_usage.gif -------------------------------------------------------------------------------- /src/lib/error/index.ts: -------------------------------------------------------------------------------- 1 | import { sadErrorStore } from "./stores"; 2 | 3 | export function setErrorModal(e: any) { 4 | sadErrorStore.setError(JSON.stringify(e)); 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/config/models.ts: -------------------------------------------------------------------------------- 1 | export type Config = { 2 | id: number; 3 | prompt_disable_package: boolean; 4 | custom_adb_path: string; 5 | clear_packages_on_disable: boolean; 6 | }; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | build: { 7 | assetsInlineLimit: 0 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | *.db 12 | *.db-shm 13 | *.db-wal 14 | discussions_dump.json 15 | sad_updater.json -------------------------------------------------------------------------------- /src/lib/devices/models.ts: -------------------------------------------------------------------------------- 1 | import type { User } from "$lib/users/models"; 2 | 3 | export type Device = { 4 | id: string; 5 | name: string; 6 | model: string; 7 | state: string; 8 | }; 9 | 10 | export type DeviceWithUsers = { 11 | device: Device; 12 | users: [User]; 13 | }; -------------------------------------------------------------------------------- /src/routes/devices/[slug]/+page.ts: -------------------------------------------------------------------------------- 1 | import { selectedDeviceIDStore } from "$lib/devices/stores"; 2 | 3 | export function load({ params }) { 4 | let deviceId = params.slug; 5 | 6 | selectedDeviceIDStore.set(deviceId); 7 | return { 8 | deviceId: deviceId 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/migrations/20230824195146_AddConfigTable.up.sql: -------------------------------------------------------------------------------- 1 | -- Add up migration script here 2 | create table config ( 3 | id integer primary key, 4 | prompt_disable_package boolean NOT NULL, 5 | custom_adb_path TEXT NOT NULL, 6 | clear_packages_on_disable boolean NOT NULL 7 | ); -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export { }; 13 | -------------------------------------------------------------------------------- /src/lib/devices/cache.ts: -------------------------------------------------------------------------------- 1 | import { LRUCache } from 'lru-cache'; 2 | 3 | const options = { 4 | max: 100, 5 | maxSize: 5000, 6 | sizeCalculation: (value: any, key: any) => { 7 | return 1 8 | }, 9 | 10 | ttl: 1000 * 15 11 | }; 12 | 13 | export const deviceHeartBeatCache = new LRUCache(options); 14 | -------------------------------------------------------------------------------- /src/lib/error/Error.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 |
12 | {message} 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@octokit/core": "^5.0.0", 13 | "node-fetch": "^3.3.2" 14 | }, 15 | "type": "module" 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/devices/adb.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/tauri'; 2 | import { info } from "tauri-plugin-log-api"; 3 | import type { DeviceWithUsers } from './models'; 4 | 5 | export async function adb_list_devices_with_users(): Promise { 6 | info(`invoking devices and users`); 7 | const cmdOutpt: [DeviceWithUsers] = await invoke('adb_list_devices_with_users'); 8 | return cmdOutpt; 9 | } 10 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DialogFilter } from '@tauri-apps/api/dialog'; 2 | 3 | export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); 4 | 5 | export const isLastItem = (a: any[], i: number) => i == a.length - 1; 6 | 7 | export const JSON_DIALOG_FILTER: DialogFilter = { name: 'JSON File', extensions: ['json'] }; 8 | export const CSV_DIALOG_FILTER: DialogFilter = { name: 'CSV File', extensions: ['csv', 'tsv'] }; 9 | -------------------------------------------------------------------------------- /src/lib/Greet.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | 14 | 15 |

{greetMsg}

16 |
17 | -------------------------------------------------------------------------------- /src/lib/Sad.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src-tauri/src/sad.rs: -------------------------------------------------------------------------------- 1 | // Simple Android Debloater 2 | use anyhow; 3 | use serde::{ser::Serializer, Serialize}; 4 | 5 | #[derive(Debug, thiserror::Error)] 6 | pub enum SADError { 7 | #[error(transparent)] 8 | E(#[from] anyhow::Error), 9 | } 10 | 11 | impl Serialize for SADError { 12 | fn serialize(&self, serializer: S) -> Result 13 | where 14 | S: Serializer, 15 | { 16 | serializer.serialize_str(self.to_string().as_ref()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | // use dotenvy; 2 | 3 | fn main() { 4 | 5 | // https://stackoverflow.com/a/73041609/6323666 6 | // using above to load in build script because dotenv macro cannot load custom path 7 | // let dotenv_path = dotenvy::from_filename("../.env").expect("failed to find .env file"); 8 | // println!("cargo:rerun-if-changed={}", dotenv_path.display()); 9 | 10 | // for env_var in dotenvy::dotenv_iter().unwrap() { 11 | // let (key, value) = env_var.unwrap(); 12 | // println!("cargo:rustc-env={key}={value}"); 13 | // } 14 | 15 | tauri_build::build() 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/NavBar.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 13 | 14 | Simple Android Debloater Logo 15 | Simple Android Debloater 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lib/notifications/Notif.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | {#if permissionGranted} 18 | "notification permission is missing" 19 | {/if} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/devices/NoDeviceBanner.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |

No Devices Selected

14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/kit/vite'; 2 | import adapter from '@sveltejs/adapter-static'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter({ 12 | // should be index.html and not app.html since vite converts app.html to index.html 13 | // tauri expects index.html to be present in ../build directory 14 | fallback: 'index.html' 15 | }) 16 | } 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /src/lib/error/stores.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | type SadError = { 4 | message: string; 5 | isPermanent: boolean; 6 | }; 7 | 8 | function createSadErrorStore() { 9 | const { set, update, subscribe } = writable({ 10 | message: '', 11 | isPermanent: false 12 | }); 13 | 14 | function setError(message: string, isPermanent: boolean = false) { 15 | update((store) => ({ 16 | ...store, 17 | message: message, 18 | isPermanent: isPermanent 19 | })); 20 | } 21 | 22 | return { 23 | subscribe, 24 | setError, 25 | reset: () => setError('', false) 26 | }; 27 | } 28 | 29 | export const sadErrorStore = createSadErrorStore(); 30 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:svelte/recommended', 7 | 'prettier' 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint'], 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020, 14 | extraFileExtensions: ['.svelte'] 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true 20 | }, 21 | overrides: [ 22 | { 23 | files: ['*.svelte'], 24 | parser: 'svelte-eslint-parser', 25 | parserOptions: { 26 | parser: '@typescript-eslint/parser' 27 | } 28 | } 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "outDir": "./build", 13 | }, 14 | "exclude": [ 15 | "./plugins/**/*", 16 | "./typings/**/*", 17 | "./built/**/*" 18 | ] 19 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 20 | // 21 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 22 | // from the referenced tsconfig.json - TypeScript does not merge them in 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/packages/models.ts: -------------------------------------------------------------------------------- 1 | export type Package = { 2 | name: string; 3 | ptype: string; 4 | state: string; 5 | package_prefix: string; 6 | }; 7 | 8 | export type DeviceUserPackages = { 9 | device_id: string; 10 | user_id: string; 11 | packages: Package[]; 12 | }; 13 | 14 | export type DeviceUserPackage = { 15 | device_id: string; 16 | user_id: string; 17 | package: Package; 18 | }; 19 | 20 | export type Config = { 21 | prompt_disable_package: boolean; 22 | }; 23 | 24 | export type Label = { 25 | name: string; 26 | description: string; 27 | }; 28 | 29 | export type PackageDiscussion = { 30 | id: string; 31 | title: string; 32 | closed: boolean; 33 | body: string; 34 | bodyHTML: string; 35 | answer: any; 36 | labels: Label[]; 37 | url: string; 38 | }; 39 | 40 | export type PackageDiscussions = Record; 41 | -------------------------------------------------------------------------------- /src/lib/packages/discussions.ts: -------------------------------------------------------------------------------- 1 | import { fetch } from '@tauri-apps/api/http'; 2 | import type { PackageDiscussion, PackageDiscussions } from './models'; 3 | import { packageDiscussionsStore } from './stores'; 4 | 5 | export const DISCUSSIONS_DUMP_URL = 'https://d31d7prv3kbkn6.cloudfront.net/discussions_dump.json'; 6 | 7 | export async function getPackageDiscussions(): Promise { 8 | const response = await fetch(DISCUSSIONS_DUMP_URL, { 9 | method: 'GET', 10 | timeout: 30 11 | }); 12 | let pdiscussions: PackageDiscussions = {}; 13 | for (let pd of response.data) { 14 | pdiscussions[pd.title] = pd; 15 | } 16 | 17 | return pdiscussions; 18 | } 19 | 20 | export async function loadPackageDiscussions() { 21 | const packageDiscussions = await getPackageDiscussions(); 22 | packageDiscussionsStore.set(packageDiscussions); 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/devices/RefreshDevicesButton.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /src/lib/users/stores.ts: -------------------------------------------------------------------------------- 1 | import { selectedDeviceStore } from '$lib/devices/stores'; 2 | import { derived, writable, type Readable, type Writable } from 'svelte/store'; 3 | import type { User } from './models'; 4 | 5 | export const selectedUserIDStore: Writable = writable(''); 6 | export const selectedUserStore: Readable = derived( 7 | [selectedDeviceStore, selectedUserIDStore], 8 | ([$selectedDeviceStore, $selectedUserIDStore]) => { 9 | let selectedUserID = $selectedUserIDStore; 10 | 11 | if (selectedUserID === '') { 12 | return null; 13 | } 14 | 15 | let selectedUser: User | null = null; 16 | $selectedDeviceStore?.users.forEach((user) => { 17 | if (user.id === selectedUserID) { 18 | selectedUser = user; 19 | return; 20 | } 21 | }); 22 | 23 | if (selectedUser == null) { 24 | selectedUserIDStore.set(''); 25 | } 26 | return selectedUser; 27 | } 28 | ); -------------------------------------------------------------------------------- /src-tauri/.sqlx/query-007072b14b2c5a841d491799fd5515d273fe38ddda9a9ab62bee6ec8759812ce.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT * FROM config where id = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id", 8 | "ordinal": 0, 9 | "type_info": "Int64" 10 | }, 11 | { 12 | "name": "prompt_disable_package", 13 | "ordinal": 1, 14 | "type_info": "Bool" 15 | }, 16 | { 17 | "name": "custom_adb_path", 18 | "ordinal": 2, 19 | "type_info": "Text" 20 | }, 21 | { 22 | "name": "clear_packages_on_disable", 23 | "ordinal": 3, 24 | "type_info": "Bool" 25 | } 26 | ], 27 | "parameters": { 28 | "Right": 1 29 | }, 30 | "nullable": [ 31 | false, 32 | false, 33 | false, 34 | false 35 | ] 36 | }, 37 | "hash": "007072b14b2c5a841d491799fd5515d273fe38ddda9a9ab62bee6ec8759812ce" 38 | } 39 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /src/lib/error/SadError.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 | 32 |
33 | {errMessage} 34 |
35 | 36 | {#if isPermanent} 37 | 42 | {/if} 43 |
44 |
45 | -------------------------------------------------------------------------------- /src-tauri/src/err.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | 3 | use crate::sad::SADError; 4 | use anyhow::anyhow; 5 | 6 | pub trait ResultOkPrintErrExt { 7 | fn ok_or_print_err(self, msg: &str) -> Option; 8 | } 9 | 10 | impl ResultOkPrintErrExt for Result 11 | where 12 | E: ::std::fmt::Debug, 13 | { 14 | fn ok_or_print_err(self, msg: &str) -> Option { 15 | match self { 16 | Ok(v) => Some(v), 17 | Err(e) => { 18 | error!("{}: {:?}", msg, e); 19 | None 20 | } 21 | } 22 | } 23 | } 24 | 25 | pub trait IntoSADError { 26 | fn into_sad_error(self, msg: &str) -> Result; 27 | } 28 | 29 | impl IntoSADError for Result 30 | where 31 | E: ::std::fmt::Display, 32 | { 33 | fn into_sad_error(self, msg: &str) -> Result { 34 | match self { 35 | Ok(v) => Ok(v), 36 | Err(e) => Err(anyhow!("{}: {}", msg, e.to_string()).into()), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/BreadCrumbs.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 15 | {#each crumbs as { href, name }, i} 16 | 17 | 18 | 19 | 20 | {@const innerSpanClass = isLastItem(crumbs, i) ? 'font-medium' : ''} 21 | {name} 22 | 23 | {/each} 24 | 25 |
26 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "ui:dev", 8 | "type": "shell", 9 | // `dev` keeps running in the background 10 | // ideally you should also configure a `problemMatcher` 11 | // see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson 12 | "isBackground": true, 13 | // change this to your `beforeDevCommand`: 14 | "command": "npm", 15 | "args": ["run", "dev"], 16 | }, 17 | { 18 | "label": "ui:dev1", 19 | "type": "npm", 20 | "isBackground": true, 21 | "script": "dev", 22 | }, 23 | { 24 | "label": "ui:build", 25 | "type": "shell", 26 | // change this to your `beforeBuildCommand`: 27 | "command": "npm", 28 | "args": ["run", "build"] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /src-tauri/src/cache.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Ok; 2 | use sqlx::SqlitePool; 3 | 4 | use crate::config; 5 | use crate::store; 6 | use anyhow::{anyhow, Result}; 7 | 8 | pub struct Cache { 9 | pub devices_store: store::Store, 10 | config: Option, 11 | } 12 | 13 | impl Cache { 14 | pub fn new() -> Self { 15 | return Self { 16 | devices_store: store::Store::new(), 17 | config: None, 18 | }; 19 | } 20 | 21 | pub async fn get_config(&mut self, db: &SqlitePool) -> Result { 22 | if self.config.is_none() { 23 | let config_svc = config::SqliteImpl { db }; 24 | let dcnfg = config_svc.get_default_config().await?; 25 | self.set_config(dcnfg); 26 | } 27 | 28 | match &self.config { 29 | Some(c) => Ok(c.clone()), 30 | None => Err(anyhow!("cache: config not found")), 31 | } 32 | } 33 | 34 | pub fn set_config(&mut self, config: config::Config) { 35 | self.config = Some(config) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/packages/RefreshPackagesButton.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 30 | -------------------------------------------------------------------------------- /src/lib/config/stores.ts: -------------------------------------------------------------------------------- 1 | import { sadErrorStore } from '$lib/error/stores'; 2 | import { invoke } from '@tauri-apps/api/tauri'; 3 | import { writable, type Writable } from 'svelte/store'; 4 | import { info } from 'tauri-plugin-log-api'; 5 | import type { Config } from './models'; 6 | 7 | export function createConfigStore() { 8 | let store: Writable = writable(null); 9 | 10 | const { subscribe, set: _set } = store; 11 | 12 | function set(c: Config) { 13 | info(`invoking update_config ${JSON.stringify(c)}`); 14 | 15 | invoke('update_config', { config: c }) 16 | .then(() => { 17 | _set(c); 18 | }) 19 | .catch((e) => { 20 | sadErrorStore.setError(`unable to update config: ${e}`); 21 | }); 22 | } 23 | 24 | return { 25 | set, 26 | subscribe, 27 | init: async () => { 28 | try { 29 | let res: Config = await invoke('get_config'); 30 | set(res); 31 | } catch (error) { 32 | console.error(error); 33 | sadErrorStore.setError(`unable to load config: ${error}`); 34 | } 35 | } 36 | }; 37 | } 38 | 39 | export const configStore = createConfigStore(); -------------------------------------------------------------------------------- /src-tauri/src/db.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{sqlite::SqliteConnectOptions, SqlitePool}; 4 | use std::{str::FromStr, fs}; 5 | 6 | // https://github.com/tauri-apps/tauri/discussions/5557 7 | // https://github.com/RandomEngy/tauri-sqlite/blob/main/src-tauri/src/main.rs 8 | pub async fn init(app_handle: &tauri::AppHandle) -> Result { 9 | let app_dir = app_handle 10 | .path_resolver() 11 | .app_data_dir() 12 | .expect("The app data directory should exist."); 13 | 14 | fs::create_dir_all(&app_dir).expect("The app data directory should be created."); 15 | 16 | let sqlite_path = app_dir.join("sad.sqlite"); 17 | 18 | info!("db_url {:?}", sqlite_path); 19 | 20 | let db_url = sqlite_path 21 | .to_str() 22 | .expect("unable to get db_path is from PathBuf"); 23 | 24 | let mut conn_options = SqliteConnectOptions::from_str(db_url)?; 25 | conn_options = conn_options.create_if_missing(true); 26 | 27 | let db = SqlitePool::connect_with(conn_options).await?; 28 | sqlx::migrate!().run(&db).await?; 29 | 30 | return Ok(db); 31 | } 32 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/**/*.{html,js,svelte,ts}', 5 | './node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}' 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | // primary: { 11 | // 50: '#FFF5F2', 12 | // 100: '#FFF1EE', 13 | // 200: '#FFE4DE', 14 | // 300: '#FFD5CC', 15 | // 400: '#FFBCAD', 16 | // 500: '#FE795D', 17 | // 600: '#EF562F', 18 | // 700: '#EB4F27', 19 | // 800: '#CC4522', 20 | // 900: '#A5371B' 21 | // }, 22 | // https://uicolors.app/browse/tailwind-colors 23 | // by pasting the icon color 24 | primary: { 25 | 50: '#f8f9ec', 26 | 100: '#edf1d6', 27 | 200: '#dee5b1', 28 | 300: '#c7d482', 29 | 400: '#aec05b', 30 | 500: '#9fb543', 31 | 600: '#70832d', 32 | 700: '#576526', 33 | 800: '#465123', 34 | 900: '#3c4522', 35 | 950: '#1f250e' 36 | } 37 | }, 38 | fontSize: { 39 | xxs: '0.5rem' 40 | } 41 | } 42 | }, 43 | plugins: [require('flowbite/plugin')], 44 | darkMode: 'class' 45 | }; 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [Thulasiram] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/lib/notifications/SadToast.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | {#each $notifications as notif (notif.id)} 34 | {@const ncolor = getColorFromNotif(notif)} 35 | {@const nicon = getIconFromNotif(notif)} 36 | 37 | 44 | 45 | {notif.message} 46 | 47 | {/each} 48 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "npm-run-dev", 9 | "request": "launch", 10 | "runtimeArgs": [ 11 | "run", 12 | "dev", 13 | ], 14 | "runtimeExecutable": "npm", 15 | "skipFiles": [ 16 | "/**" 17 | ], 18 | "type": "node" 19 | }, 20 | { 21 | "type": "lldb", 22 | "request": "launch", 23 | "name": "Tauri Development Debug", 24 | "cargo": { 25 | "args": [ 26 | "build", 27 | "--manifest-path=./src-tauri/Cargo.toml", 28 | "--no-default-features" 29 | ] 30 | }, 31 | // task for the `beforeDevCommand` if used, must be configured in `.vscode/tasks.json` 32 | // "preLaunchTask": "ui:dev", 33 | }, 34 | { 35 | "type": "lldb", 36 | "request": "launch", 37 | "name": "Tauri Production Debug", 38 | "cargo": { 39 | "args": ["build", "--release", "--manifest-path=./src-tauri/Cargo.toml"] 40 | }, 41 | // task for the `beforeBuildCommand` if used, must be configured in `.vscode/tasks.json` 42 | "preLaunchTask": "ui:build" 43 | }, 44 | ] 45 | } -------------------------------------------------------------------------------- /src/routes/devices/[slug]/+page.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 |
30 | 31 |
32 | 33 |
34 | 35 | 36 |
37 |
38 |

Packages

39 | 40 |
41 | 42 | {#if renderPackages} 43 | 44 | {/if} 45 |
46 | -------------------------------------------------------------------------------- /src/lib/packages/adb.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/tauri'; 2 | import { info } from 'tauri-plugin-log-api'; 3 | import type { DeviceUserPackages, Package } from './models'; 4 | 5 | export async function adb_list_packages( 6 | deviceId: string, 7 | userId: string 8 | ): Promise { 9 | info(`invoking packages - ${deviceId} - ${userId}`); 10 | 11 | const cmdOutpt: Package[] = await invoke('adb_list_packages', { 12 | deviceId: deviceId, 13 | userId: userId 14 | }); 15 | return { device_id: deviceId, user_id: userId, packages: cmdOutpt }; 16 | } 17 | 18 | export async function adb_disable_package(deviceId: string, userId: string, pkg: string) { 19 | info(`invoking disable - ${userId} - ${pkg}`); 20 | 21 | let dpkg: Package = await invoke('adb_disable_clear_and_stop_package', { 22 | deviceId: deviceId, 23 | userId: userId, 24 | pkg: pkg 25 | }); 26 | 27 | return dpkg; 28 | } 29 | 30 | export async function adb_enable_package(deviceId: string, userId: string, pkg: string) { 31 | info(`invoking enable - ${userId} - ${pkg}`); 32 | 33 | let epkg: Package = await invoke('adb_enable_package', { 34 | deviceId: deviceId, 35 | userId: userId, 36 | pkg: pkg 37 | }); 38 | 39 | return epkg; 40 | } 41 | 42 | export async function adb_install_package(deviceId: string, userId: string, pkg: string) { 43 | info(`invoking install - ${userId} - ${pkg}`); 44 | 45 | await invoke('adb_install_package', { 46 | deviceId: deviceId, 47 | userId: userId, 48 | pkg: pkg 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/discussions_dump.yml: -------------------------------------------------------------------------------- 1 | name: Dump Discussions Workflow 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | discussions_dump: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | cache: 'npm' 17 | cache-dependency-path: 'package-lock.json' 18 | - env: 19 | GH_TOKEN: ${{ github.token }} 20 | run: | 21 | cd ./scripts 22 | npm ci 23 | ls -lahtr 24 | node ./discussions_dump.js 25 | - uses: actions/upload-artifact@v3.1.2 26 | with: 27 | name: discussions_dump.json 28 | path: ./scripts/discussions_dump.json 29 | if-no-files-found: error 30 | upload_dump_to_s3: 31 | runs-on: ubuntu-latest 32 | needs: [discussions_dump] 33 | # These permissions are needed to interact with GitHub's OIDC Token endpoint. 34 | permissions: 35 | id-token: write 36 | contents: read 37 | steps: 38 | - uses: actions/download-artifact@v2.1.1 39 | - run: ls -lahtr 40 | - run: pwd 41 | - uses: aws-actions/configure-aws-credentials@v3 42 | with: 43 | aws-region: us-east-1 44 | role-to-assume: ${{ secrets.AWS_SAD_UPLOADER_ROLE }} 45 | # use recursive: https://github.com/aws/aws-cli/issues/2929 46 | - run: aws s3 cp discussions_dump.json s3://simple-android-debloater --recursive 47 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish Cross Platform Builds' 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - release 7 | 8 | jobs: 9 | publish-tauri: 10 | permissions: 11 | contents: write 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: [macos-latest, ubuntu-20.04, windows-latest] 16 | 17 | runs-on: ${{ matrix.platform }} 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: setup node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | - name: install Rust stable 25 | uses: dtolnay/rust-toolchain@stable 26 | - name: install dependencies (ubuntu only) 27 | if: matrix.platform == 'ubuntu-20.04' 28 | run: | 29 | sudo apt-get update 30 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 31 | - name: install frontend dependencies 32 | run: npm install # change this to npm or pnpm depending on which one you use 33 | - uses: tauri-apps/tauri-action@v0 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 37 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 38 | with: 39 | tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version 40 | releaseName: 'Simple Android Debloater v__VERSION__' 41 | releaseBody: 'See the assets to download this version and install.' 42 | releaseDraft: true 43 | prerelease: false -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sad.rs", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 12 | "format": "prettier --plugin-search-dir . --write .", 13 | "tauri": "tauri" 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/adapter-auto": "^2.0.0", 17 | "@sveltejs/adapter-static": "^1.0.0-next.50", 18 | "@sveltejs/kit": "^1.20.4", 19 | "@tauri-apps/cli": "^1.4.0", 20 | "@types/papaparse": "^5.3.8", 21 | "@typescript-eslint/eslint-plugin": "^5.45.0", 22 | "@typescript-eslint/parser": "^5.45.0", 23 | "autoprefixer": "^10.4.14", 24 | "eslint": "^8.28.0", 25 | "eslint-config-prettier": "^8.5.0", 26 | "eslint-plugin-svelte": "^2.30.0", 27 | "postcss": "^8.4.25", 28 | "prettier": "^2.8.0", 29 | "prettier-plugin-svelte": "^2.10.1", 30 | "svelte": "^4.0.5", 31 | "svelte-check": "^3.4.3", 32 | "tailwindcss": "^3.3.2", 33 | "tslib": "^2.4.1", 34 | "typescript": "^5.0.0", 35 | "vite": "^4.4.2" 36 | }, 37 | "type": "module", 38 | "dependencies": { 39 | "@floating-ui/dom": "^1.5.1", 40 | "@tabler/icons-svelte": "^2.31.0", 41 | "@tauri-apps/api": "^1.4.0", 42 | "flowbite": "^1.8.1", 43 | "flowbite-svelte": "^0.40.2", 44 | "flowbite-svelte-icons": "^0.3.6", 45 | "lru-cache": "^10.0.1", 46 | "papaparse": "^5.4.1", 47 | "tailwind-merge": "^1.14.0", 48 | "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/updater.yml: -------------------------------------------------------------------------------- 1 | name: Update Manifest Workflow 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | user_input_version: 7 | description: "Version /tag to target" 8 | release: 9 | types: [released, published] 10 | 11 | jobs: 12 | generate_updater_json: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | cache: "npm" 20 | cache-dependency-path: "package-lock.json" 21 | - env: 22 | GH_TOKEN: ${{ github.token }} 23 | RELEASE_EVENT: ${{ github.event.release }} 24 | RELEASE_TAG: ${{ github.event.inputs.user_input_version }} 25 | run: | 26 | cd ./scripts 27 | npm ci 28 | ls -lahtr 29 | node ./updater_template.js 30 | - uses: actions/upload-artifact@v3.1.2 31 | with: 32 | name: sad_updater.json 33 | path: ./scripts/sad_updater.json 34 | if-no-files-found: error 35 | 36 | upload_updater_json_to_s3: 37 | runs-on: ubuntu-latest 38 | needs: [generate_updater_json] 39 | # These permissions are needed to interact with GitHub's OIDC Token endpoint. 40 | permissions: 41 | id-token: write 42 | contents: read 43 | steps: 44 | - uses: actions/download-artifact@v2.1.1 45 | - run: ls -lahtr 46 | - run: pwd 47 | - uses: aws-actions/configure-aws-credentials@v4 48 | with: 49 | aws-region: us-east-1 50 | role-to-assume: ${{ secrets.AWS_SAD_UPLOADER_ROLE }} 51 | # use recursive: https://github.com/aws/aws-cli/issues/2929 52 | - run: aws s3 cp sad_updater.json s3://simple-android-debloater --recursive 53 | -------------------------------------------------------------------------------- /src/lib/notifications/stores.ts: -------------------------------------------------------------------------------- 1 | // https://svelte.dev/repl/2254c3b9b9ba4eeda05d81d2816f6276?version=4.2.0 2 | import { derived, writable, type Readable, type Writable } from 'svelte/store'; 3 | 4 | const TIMEOUT = 1000; 5 | 6 | export type Notif = { 7 | id: string; 8 | type: string; 9 | message: string; 10 | timeout: number; 11 | }; 12 | 13 | function createNotificationStore() { 14 | const _notifications: Writable = writable([]); 15 | 16 | function send(message: string, type = 'default', tm: number) { 17 | _notifications.update((state) => { 18 | return [...state, { id: id(), type, message, timeout: tm }]; 19 | }); 20 | } 21 | 22 | let timers = []; 23 | 24 | const notifications: Readable = derived(_notifications, ($_notifications, set) => { 25 | set($_notifications); 26 | if ($_notifications.length > 0) { 27 | const timer = setTimeout(() => { 28 | _notifications.update((state) => { 29 | state.shift(); 30 | return state; 31 | }); 32 | }, $_notifications[0].timeout); 33 | return () => { 34 | clearTimeout(timer); 35 | }; 36 | } 37 | }); 38 | const { subscribe } = notifications; 39 | 40 | return { 41 | subscribe, 42 | send, 43 | default: (msg: string, timeout: number = TIMEOUT) => send(msg, 'default', timeout), 44 | error: (msg: string, timeout: number = TIMEOUT) => send(msg, 'error', timeout), 45 | warning: (msg: string, timeout: number = TIMEOUT) => send(msg, 'warning', timeout), 46 | info: (msg: string, timeout: number = TIMEOUT) => send(msg, 'info', timeout), 47 | success: (msg: string, timeout: number = TIMEOUT) => send(msg, 'success', timeout) 48 | }; 49 | } 50 | 51 | function id() { 52 | return '_' + Math.random().toString(36).substr(2, 9); 53 | } 54 | 55 | export const notifications = createNotificationStore(); 56 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple_android_debloater" 3 | version = "0.1.0" 4 | description = "For Debloating Android Devices" 5 | authors = ["you"] 6 | license = "MIT License" 7 | repository = "https://github.com/thulasi-ram/simple_android_debloater" 8 | default-run = "simple_android_debloater" 9 | edition = "2021" 10 | rust-version = "1.60" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.4.0", features = [] } 16 | dotenvy= "0.15.7" 17 | 18 | [dependencies] 19 | serde_json = "1.0" 20 | serde = { version = "1.0", features = ["derive"] } 21 | tauri = { version = "1.4.0", features = [ "updater", "process-all", "fs-read-file", "fs-write-file", "dialog-all", "shell-open", "http-request", "path-all", "app-all", "notification-all", "devtools"] } 22 | thiserror = "1.0" 23 | anyhow = "*" 24 | tokio = { version = "*", features = ["full"] } 25 | lazy_static = "1.4.0" 26 | log = "^0.4" 27 | tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = ["colored"] } 28 | sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "sqlite" ] } 29 | regex = "1.9.4" 30 | futures = "0.3.28" 31 | 32 | 33 | [features] 34 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. 35 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. 36 | # DO NOT REMOVE!! 37 | custom-protocol = [ "tauri/custom-protocol" ] 38 | 39 | # [dependencies.tauri-plugin-sql] 40 | # git = "https://github.com/tauri-apps/plugins-workspace" 41 | # branch = "v1" 42 | # features = ["sqlite"] # or "postgres", or "mysql" 43 | 44 | [dependencies.fix-path-env] 45 | git = "https://github.com/tauri-apps/fix-path-env-rs" 46 | branch = "release" 47 | -------------------------------------------------------------------------------- /src/lib/users/UsersDropdown.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | 45 | 46 | 47 | {#each userMap as user} 48 |
  • 49 | 59 |
  • 60 | {/each} 61 |
    62 |
    63 | -------------------------------------------------------------------------------- /src-tauri/src/users.rs: -------------------------------------------------------------------------------- 1 | use crate::adb_cmd::{self, ADBCommand, ADBShell}; 2 | use anyhow::{anyhow, Result}; 3 | use lazy_static::lazy_static; 4 | use regex::Regex; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | pub struct User { 9 | pub id: String, 10 | pub name: String, 11 | pub device_id: String, 12 | } 13 | 14 | impl std::fmt::Display for User { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | write!(f, "user {}", self.id) 17 | } 18 | } 19 | 20 | pub trait ListUsers { 21 | fn list_users(&self, device_id: String) -> Result>; 22 | } 23 | 24 | pub struct ADBTerminalImpl { 25 | pub adb_path: String, 26 | } 27 | 28 | lazy_static! { 29 | static ref USER_INFO_PARSE_REGEX: Regex = Regex::new(r"UserInfo\{(.*)\}").unwrap(); 30 | } 31 | 32 | impl ADBTerminalImpl { 33 | pub fn list_users(&self, device_id: String) -> Result> { 34 | let shell_cmd: ADBShell = adb_cmd::for_device( 35 | &ADBShell::new(self.adb_path.to_owned()), 36 | device_id.to_owned(), 37 | ); 38 | 39 | let res = shell_cmd.args(&["pm list users "]).execute(); 40 | match res { 41 | Err(e) => { 42 | return Err(e.into()); 43 | } 44 | Ok(o) => { 45 | let mut users: Vec = vec![]; 46 | for (_, [cap]) in USER_INFO_PARSE_REGEX.captures_iter(&o).map(|c| c.extract()) { 47 | let split: Vec<&str> = cap.split(":").collect(); 48 | if split.len() < 2 { 49 | return Err(anyhow!("unable to parse user. input {}", cap)); 50 | } 51 | users.push(User { 52 | id: split[0].to_string(), 53 | name: split[1].to_string(), 54 | device_id: device_id.to_owned(), 55 | }) 56 | } 57 | return Ok(users); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/devices/stores.ts: -------------------------------------------------------------------------------- 1 | import { deviceHeartBeatCache } from '$lib/devices/cache'; 2 | import { derived, get, readable, writable, type Readable, type Writable } from 'svelte/store'; 3 | import type { DeviceWithUsers } from './models'; 4 | 5 | function createDeviceWithUsersStore() { 6 | const store = writable>({}); 7 | const { set, update, subscribe } = store; 8 | 9 | function insertDevice(dev: DeviceWithUsers) { 10 | update((store) => { 11 | deviceHeartBeatCache.set(dev.device.id, new Date()); 12 | store[dev.device.id] = dev; 13 | return store; 14 | }); 15 | } 16 | 17 | function hasDevice(device_id: string) { 18 | return get(store).hasOwnProperty(device_id); 19 | } 20 | 21 | function getDevice(device_id: string): DeviceWithUsers | null { 22 | let devices = get(store); 23 | for (const key in devices) { 24 | if (key === device_id) { 25 | return devices[key]; 26 | } 27 | } 28 | return null; 29 | } 30 | 31 | return { 32 | subscribe, 33 | insertDevice, 34 | getDevice 35 | }; 36 | } 37 | 38 | export const devicesWithUsersStore = createDeviceWithUsersStore(); 39 | 40 | export const selectedDeviceIDStore: Writable = writable(''); 41 | 42 | export const selectedDeviceStore: Readable = derived( 43 | [devicesWithUsersStore, selectedDeviceIDStore], 44 | ([$devicesWithUsersStore, $selectedDeviceIDStore]) => { 45 | let selectedDeviceID = $selectedDeviceIDStore; 46 | 47 | if (!selectedDeviceID) { 48 | return null; 49 | } 50 | 51 | let device = devicesWithUsersStore.getDevice(selectedDeviceID); 52 | 53 | return device; 54 | } 55 | ); 56 | 57 | export const liveDevicesStore: Readable> = readable( 58 | {}, 59 | function start(set) { 60 | const interval = setInterval(() => { 61 | let _store: Record = {}; 62 | 63 | for (let [deviceId, du] of Object.entries(get(devicesWithUsersStore))) { 64 | let isLive = false; 65 | 66 | if (du.device.state === 'Device') { 67 | if (deviceHeartBeatCache.get(deviceId) !== undefined) { 68 | isLive = true; 69 | } 70 | } 71 | _store[deviceId] = isLive; 72 | } 73 | 74 | set(_store); 75 | }, 1000); 76 | 77 | return function stop() { 78 | clearInterval(interval); 79 | }; 80 | } 81 | ); 82 | -------------------------------------------------------------------------------- /src-tauri/src/adb_devices.rs.bak: -------------------------------------------------------------------------------- 1 | use adb_client::AdbTcpConnexion; 2 | use tokio::sync::broadcast::Sender; 3 | 4 | // pub struct ADBDevices { 5 | // // id: str, 6 | // comment: str, 7 | // } 8 | 9 | // pub struct Device { 10 | // // id: str, 11 | // is_enabled: bool, 12 | // make: str, 13 | // model: str, 14 | // comment: str, 15 | // } 16 | 17 | pub fn adb_list_devices(c: &mut AdbTcpConnexion) -> Result { 18 | let res = c.devices(); 19 | match res { 20 | Err(e) => { 21 | return Err(e.to_string()); 22 | } 23 | Ok(o) => { 24 | // let ot = o.replace("package:", ""); 25 | // let ots = ot.trim(); 26 | // for l in ots.lines() { 27 | // } 28 | return Ok(format!("{:?}", o)); 29 | } 30 | } 31 | } 32 | 33 | 34 | pub fn adb_track_devices(c: &mut AdbTcpConnexion, devices: &mut Vec) -> Result<(), String> { 35 | 36 | let callback = |device:adb_client::Device| { 37 | println!("{}", device); 38 | Ok(()) 39 | }; 40 | 41 | let res = c.track_devices(callback); 42 | 43 | match res { 44 | Err(e) => { 45 | return Err(e.to_string()); 46 | } 47 | Ok(_o) => { 48 | return Ok(()); 49 | } 50 | } 51 | } 52 | 53 | 54 | pub async fn aadb_track_devices(c: &mut AdbTcpConnexion, async_proc_input_tx: tokio::sync::MutexGuard<'_, Sender>) -> Result<(), String> { 55 | 56 | let callback = |device:adb_client::Device| { 57 | let res = async_proc_input_tx 58 | .send(format!("{}", device)) 59 | .map_err(|e| e.to_string()); 60 | 61 | return res; 62 | }; 63 | 64 | let res = c.track_devices(callback); 65 | 66 | match res { 67 | Err(e) => { 68 | return Err(e.to_string()); 69 | } 70 | Ok(_o) => { 71 | return Ok(()); 72 | } 73 | } 74 | } 75 | 76 | 77 | #[tauri::command] 78 | fn adb_track_devices( 79 | adbc: tauri::State<'_, ADBConn>, 80 | devices: tauri::State<'_, Devices>, 81 | ) -> Result { 82 | let mut _devices = devices.0.lock().unwrap(); 83 | let d = _devices.deref_mut(); 84 | 85 | let resTrack = adb_devices::adb_track_devices(adbc.clone().conn.lock().unwrap().deref_mut(), d); 86 | match resTrack { 87 | Err(e) => { 88 | return Err(e.to_string()); 89 | } 90 | Ok(_o) => { 91 | return Ok(format!("{:?}", d)); 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/routes/import/import.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '$lib/config/models'; 2 | import { configStore } from '$lib/config/stores'; 3 | import { CSV_DIALOG_FILTER, JSON_DIALOG_FILTER } from '$lib/utils'; 4 | import { open, type DialogFilter, save } from '@tauri-apps/api/dialog'; 5 | import { readTextFile, writeTextFile } from '@tauri-apps/api/fs'; 6 | import Papa, { type ParseResult } from 'papaparse'; 7 | 8 | export async function importSettingsJSON() { 9 | const openPath = await open({ 10 | directory: false, 11 | multiple: false, 12 | title: 'Settings JSON', 13 | filters: [JSON_DIALOG_FILTER] 14 | }); 15 | 16 | if (!openPath) return; 17 | 18 | if (Array.isArray(openPath)) { 19 | throw new Error('multiple selections for settings.json is not supported'); 20 | } 21 | 22 | const fcontent = await readTextFile(openPath as string); 23 | 24 | const config: Config = JSON.parse(fcontent); 25 | configStore.set(config); 26 | } 27 | 28 | export async function openDialongSingleFile( 29 | title: string, 30 | filters: DialogFilter[] 31 | ): Promise { 32 | const openPath = await open({ 33 | directory: false, 34 | multiple: false, 35 | title: title, 36 | filters: filters 37 | }); 38 | 39 | if (!openPath) return null; 40 | 41 | if (Array.isArray(openPath)) { 42 | throw new Error('multiple selections for settings.json is not supported'); 43 | } 44 | 45 | return openPath as string; 46 | } 47 | 48 | export type PackageNames = string[]; 49 | 50 | type PackageRow = { 51 | package: string; 52 | }; 53 | 54 | export async function openAndpParseCSVToJson(title: string) { 55 | let fpath: string = ''; 56 | 57 | let openfpath = await openDialongSingleFile(title, [CSV_DIALOG_FILTER]); 58 | 59 | if (!openfpath) { 60 | return; 61 | } 62 | 63 | fpath = openfpath; 64 | 65 | return async () => { 66 | const fcontent = await readTextFile(fpath); 67 | let parseResult: ParseResult = Papa.parse(fcontent, { header: true }); 68 | if (parseResult.errors.length > 1) { 69 | throw new Error(`unable to parse csv ${parseResult.errors}`); 70 | } 71 | let packageNames: PackageNames = []; 72 | for (let p of parseResult.data) { 73 | packageNames.push(p['package']); 74 | } 75 | 76 | return packageNames; 77 | }; 78 | } 79 | 80 | export type PackageResult = { 81 | package: string; 82 | result: string; 83 | }; 84 | 85 | export async function savePackagesResultsCSV(data: PackageResult[]) { 86 | const savePath = await save({ 87 | title: 'Save Packages Results CSV', 88 | filters: [CSV_DIALOG_FILTER] 89 | }); 90 | if (!savePath) return; 91 | const fcontent = Papa.unparse(data); 92 | 93 | await writeTextFile(savePath, fcontent); 94 | } 95 | -------------------------------------------------------------------------------- /src/routes/settings/+page.svelte: -------------------------------------------------------------------------------- 1 | 52 | 53 | 54 | 55 | 56 | 57 |
    58 | {#if config} 59 | 60 | Prompt on disable package 61 | 62 | 63 | Clear packages on disable 64 | 65 | 66 |
    67 | 68 | 73 |
    74 | 75 |
    76 | 83 | 84 | 91 |
    92 | {/if} 93 |
    94 | -------------------------------------------------------------------------------- /src/routes/about/+page.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 | 37 |
    38 |

    39 | Simpl Android Debloater is a free and open source project to disable unwanted system apps that 40 | careers / OEMs force install in our mobile phones. 41 |

    42 |

    43 | This tool is aimed to be beginner friendly so as to not uninstall apps unexpectedly which can 44 | brick the device. 45 |

    46 | 47 |
    48 | 57 | 66 | 75 |
    76 | 77 | 78 | Logs Directory 79 |
    {logDir}
    80 |
    81 | 82 | {#await appVersion then version} 83 |

    app version: {version}

    84 | {/await} 85 | 86 | 87 |
    88 | -------------------------------------------------------------------------------- /src-tauri/src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use core::result::Result::Ok; 3 | use serde::{Deserialize, Serialize}; 4 | use sqlx::SqlitePool; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 7 | pub struct Config { 8 | pub id: i64, 9 | pub prompt_disable_package: bool, 10 | pub custom_adb_path: String, 11 | pub clear_packages_on_disable: bool, 12 | } 13 | 14 | static DEFAULT_CONFIG_ID: i64 = 1; 15 | 16 | impl Default for Config { 17 | fn default() -> Self { 18 | Config { 19 | id: DEFAULT_CONFIG_ID, 20 | prompt_disable_package: true, 21 | custom_adb_path: String::from(""), 22 | clear_packages_on_disable: false, 23 | } 24 | } 25 | } 26 | 27 | // async traits needs another supporting crate for now 28 | // https://www.reddit.com/r/rust/comments/gt1ct3/using_sqlx_with_nonasync_code/ 29 | // https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/ 30 | // pub trait GetDefaultConfig { 31 | // fn get_default_config(&self) -> Result; 32 | // } 33 | 34 | // pub trait UpdateDefaultConfig { 35 | // fn update_default_config(&self, new_config: Config) -> Result; 36 | // } 37 | 38 | pub struct SqliteImpl<'a> { 39 | pub db: &'a SqlitePool, 40 | } 41 | 42 | impl SqliteImpl<'_> { 43 | pub async fn get_default_config(&self) -> Result { 44 | let res = sqlx::query_as!( 45 | Config, 46 | "SELECT * FROM config where id = ?", 47 | DEFAULT_CONFIG_ID 48 | ) 49 | .fetch_one(self.db) 50 | .await; 51 | 52 | match res { 53 | Err(e) => match e { 54 | sqlx::Error::RowNotFound => { 55 | return Ok(Config::default()); 56 | } 57 | _ => { 58 | return Err(anyhow!("error executing db: {}", e.to_string()).into()); 59 | } 60 | }, 61 | Ok(r) => { 62 | return Ok(r); 63 | } 64 | } 65 | } 66 | 67 | pub async fn update_default_config(&self, config: Config) -> Result { 68 | let res = sqlx::query( 69 | "insert into config(id, prompt_disable_package, custom_adb_path, clear_packages_on_disable) VALUES($1, $2, $3, $4) 70 | ON CONFLICT(id) DO UPDATE SET prompt_disable_package = $2, custom_adb_path = $3, clear_packages_on_disable = $4", 71 | ) 72 | .bind(DEFAULT_CONFIG_ID) 73 | .bind(config.prompt_disable_package) 74 | .bind(config.custom_adb_path.to_owned()) 75 | .bind(config.clear_packages_on_disable) 76 | .execute(self.db) 77 | .await?; 78 | 79 | if res.rows_affected() <= 0 { 80 | return Err(anyhow!("no rows updated").into()); 81 | } 82 | 83 | return Ok(config); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json", 3 | "build": { 4 | "beforeBuildCommand": "npm run build", 5 | "beforeDevCommand": "npm run dev", 6 | "devPath": "http://localhost:5173", 7 | "distDir": "../build" 8 | }, 9 | "package": { 10 | "productName": "Simple Android Debloater", 11 | "version": "0.6.0" 12 | }, 13 | "tauri": { 14 | "allowlist": { 15 | "app": { 16 | "all": true 17 | }, 18 | "dialog": { 19 | "all": true 20 | }, 21 | "fs": { 22 | "readFile": true, 23 | "writeFile": true, 24 | "scope": [ 25 | "$APP", 26 | "$APP/*", 27 | "$APP/**", 28 | "$DESKTOP", 29 | "$DESKTOP/*", 30 | "$DESKTOP/**", 31 | "$DOCUMENT", 32 | "$DOCUMENT/*", 33 | "$DOCUMENT/**", 34 | "$DOWNLOAD", 35 | "$DOWNLOAD/*", 36 | "$DOWNLOAD/**", 37 | "$HOME", 38 | "$HOME/*", 39 | "$HOME/**", 40 | "$PICTURE", 41 | "$PICTURE/*", 42 | "$PICTURE/**", 43 | "$PUBLIC", 44 | "$PUBLIC/*", 45 | "$PUBLIC/**" 46 | ] 47 | }, 48 | "http": { 49 | "request": true, 50 | "scope": ["https://d31d7prv3kbkn6.cloudfront.net/*"] 51 | }, 52 | "path": { 53 | "all": true 54 | }, 55 | "process": { 56 | "all": true 57 | }, 58 | "notification": { 59 | "all": true 60 | }, 61 | "shell": { 62 | "open": true 63 | } 64 | }, 65 | "bundle": { 66 | "active": true, 67 | "category": "DeveloperTool", 68 | "copyright": "", 69 | "deb": { 70 | "depends": [] 71 | }, 72 | "externalBin": [], 73 | "icon": [ 74 | "icons/32x32.png", 75 | "icons/128x128.png", 76 | "icons/128x128@2x.png", 77 | "icons/icon.icns", 78 | "icons/icon.ico" 79 | ], 80 | "identifier": "com.ahiravan.simple-android-debloater", 81 | "longDescription": "", 82 | "macOS": { 83 | "entitlements": null, 84 | "exceptionDomain": "", 85 | "frameworks": [], 86 | "providerShortName": null, 87 | "signingIdentity": null 88 | }, 89 | "resources": [], 90 | "shortDescription": "", 91 | "targets": "all", 92 | "windows": { 93 | "certificateThumbprint": null, 94 | "digestAlgorithm": "sha256", 95 | "timestampUrl": "" 96 | } 97 | }, 98 | "security": { 99 | "csp": null 100 | }, 101 | "updater": { 102 | "active": true, 103 | "endpoints": ["https://d31d7prv3kbkn6.cloudfront.net/sad_updater.json"], 104 | "dialog": true, 105 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdCNkMwNEMyNzA2NjRCNkUKUldSdVMyWnd3Z1JzZTVjRHRhdENCSEtDNzIvT2tqazJsL3djQlNzOHB4cmFuRDFQZ21ZVjExUXcK" 106 | }, 107 | "windows": [ 108 | { 109 | "fullscreen": false, 110 | "height": 600, 111 | "resizable": true, 112 | "title": "Simple Android Debloater", 113 | "width": 800 114 | } 115 | ] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/routes/export/+page.svelte: -------------------------------------------------------------------------------- 1 | 54 | 55 | 56 | 57 |
    58 |
    59 | 63 |

    Exports packages as a flattened CSV

    64 |
    65 | 66 |
    67 | 71 |

    Exports packages as JSON for advanced usecases

    72 |
    73 |
    74 | 78 |

    Export Settings as JSON

    79 |
    80 |
    81 | 85 |

    Open url to discssions_dump.json

    86 |
    87 |
    88 | -------------------------------------------------------------------------------- /src/lib/packages/stores.ts: -------------------------------------------------------------------------------- 1 | import { selectedDeviceStore } from '$lib/devices/stores'; 2 | import { setErrorModal } from '$lib/error'; 3 | import { selectedUserStore } from '$lib/users/stores'; 4 | import { derived, get, writable, type Writable } from 'svelte/store'; 5 | import { info } from 'tauri-plugin-log-api'; 6 | import type { Package, PackageDiscussions } from './models'; 7 | 8 | function createPackagesStore() { 9 | const store = writable>({}); 10 | const { set, update, subscribe } = store; 11 | 12 | function setPackages(device_id: string, user_id: string, packages: Package[]) { 13 | update((store) => { 14 | let pkey = packagesKey(device_id, user_id); 15 | if (!pkey) { 16 | setErrorModal('pkey is empty'); 17 | return store; 18 | } 19 | store[pkey] = packages; 20 | return store; 21 | }); 22 | } 23 | 24 | function addPackage(device_id: string, user_id: string, pkg: Package) { 25 | update((store) => { 26 | let pkey = packagesKey(device_id, user_id); 27 | if (!pkey) { 28 | setErrorModal('pkey is empty'); 29 | return store; 30 | } 31 | let existingPkgs = store[pkey]; 32 | 33 | existingPkgs = existingPkgs.filter((epkg) => epkg.name !== pkg.name); 34 | existingPkgs.push(pkg); 35 | store[pkey] = existingPkgs; 36 | return store; 37 | }); 38 | } 39 | 40 | function hasPackages(device_id: string, user_id: string): boolean { 41 | let pkey = packagesKey(device_id, user_id); 42 | if (!pkey) { 43 | return false; 44 | } 45 | return get(store).hasOwnProperty(pkey); 46 | } 47 | 48 | return { 49 | subscribe, 50 | setPackages, 51 | hasPackages, 52 | addPackage 53 | }; 54 | } 55 | 56 | export const packagesStore = createPackagesStore(); 57 | 58 | export const packagesKey = (deviceId: string | undefined, userId: string | undefined): string | null => { 59 | if (!deviceId || !userId) { 60 | info(`pkey is null ${deviceId} ${userId}`); 61 | return null; 62 | } 63 | return `${deviceId}-${userId}`; 64 | }; 65 | 66 | export const currentPackagesStore = derived( 67 | [packagesStore, selectedDeviceStore, selectedUserStore], 68 | ([$packagesStore, $selectedDeviceStore, $selectedUserStore]) => { 69 | let user = $selectedUserStore; 70 | let pkey = packagesKey(user?.device_id, user?.id); 71 | if (!pkey) { 72 | return []; 73 | } 74 | let pkgs = $packagesStore[pkey]; 75 | return pkgs || []; 76 | } 77 | ); 78 | 79 | export const filteredPackages: Writable = writable([]); 80 | export const searchTermStore = writable(''); 81 | export const packageDiscussionsStore: Writable = writable({}); 82 | 83 | export const packageLabelsStore = derived([packageDiscussionsStore], ([$packageDiscussionsStore]) => { 84 | let labels: Record = {}; 85 | 86 | for (let [_, pkgdiscussion] of Object.entries($packageDiscussionsStore)) { 87 | for (let l of pkgdiscussion.labels) { 88 | labels[l.name] = l.description; 89 | } 90 | } 91 | return labels; 92 | }); 93 | -------------------------------------------------------------------------------- /scripts/updater_template.js: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/core'; 2 | import fs from 'fs'; 3 | import fetch from 'node-fetch'; 4 | import { release } from 'os'; 5 | 6 | const octokit = new Octokit({ 7 | auth: process.env.GH_TOKEN 8 | }); 9 | 10 | async function getReleaseForTag(tag) { 11 | let json = await octokit.request( 12 | 'GET /repos/thulasi-ram/simple_android_debloater/releases/tags/{tag}', 13 | { 14 | tag: tag, 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | 'X-GitHub-Api-Version': '2022-11-28' 18 | } 19 | } 20 | ); 21 | 22 | let data = json.data; 23 | 24 | if (!data) { 25 | console.log(json); 26 | throw new Error('Error in response json'); 27 | } 28 | 29 | return data; 30 | } 31 | 32 | async function downloadLatestJSON(release) { 33 | let latestJsonURL = ''; 34 | 35 | for (let a of release.assets) { 36 | if (a.name == 'latest.json') { 37 | latestJsonURL = a.browser_download_url; 38 | } 39 | } 40 | 41 | if (!latestJsonURL) { 42 | throw new Error('latest.json is not present'); 43 | } 44 | 45 | const response = await fetch(latestJsonURL); 46 | let json = await response.json(); 47 | 48 | if (!json) { 49 | console.log(response); 50 | throw new Error('Error in response json'); 51 | } 52 | 53 | return json; 54 | } 55 | 56 | async function getReleaseFromEventOrTag() { 57 | const releaseEvent = process.env.RELEASE_EVENT; 58 | const releaseTag = process.env.RELEASE_TAG; 59 | 60 | if (releaseEvent && releaseTag) { 61 | throw new Error('one of releaseTag or releaseEvent is expected. Got both'); 62 | } 63 | 64 | if (!releaseEvent && !releaseTag) { 65 | throw new Error('one of releaseTag or releaseEvent is expected. Got neither'); 66 | } 67 | 68 | console.log('release event and tag', releaseEvent, releaseTag); 69 | 70 | let release = { 71 | id: '', 72 | tag: '', 73 | body: '', 74 | assets: [], 75 | published_at: '' 76 | }; 77 | 78 | if (releaseEvent) { 79 | let releaseData = JSON.parse(releaseEvent); 80 | console.log('release by event', releaseData.id); 81 | 82 | release = { 83 | id: releaseData.id, 84 | tag: releaseData.tag_name, 85 | body: releaseData.body, 86 | assets: releaseData.assets, 87 | published_at: releaseData.published_at 88 | }; 89 | } 90 | 91 | if (releaseTag != '') { 92 | let releaseData = await getReleaseForTag(releaseTag); 93 | console.log('release by tag', releaseData.id); 94 | 95 | release = { 96 | id: releaseData.id, 97 | tag: releaseData.tag_name, 98 | body: releaseData.body, 99 | assets: releaseData.assets, 100 | published_at: releaseData.published_at 101 | }; 102 | } 103 | 104 | let content = await downloadLatestJSON(release); 105 | content.notes = release.body; 106 | 107 | fs.writeFile('sad_updater.json', JSON.stringify(content), 'utf8', function (err) { 108 | if (err) throw err; 109 | console.log('Dumped sad_updater.json'); 110 | }); 111 | } 112 | 113 | getReleaseFromEventOrTag(); 114 | -------------------------------------------------------------------------------- /src/lib/Sidebar.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | 35 |

    DEVICES

    36 | 37 |
    38 | 39 | 40 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
    74 | 75 | Light Mode 76 |
    77 |
    78 | 79 |
    80 | 81 | Dark Mode 82 |
    83 |
    84 |
    85 | 86 | { 91 | await relaunch(); 92 | }} 93 | > 94 | 95 | 96 | 97 | 98 |
    99 |
    100 |
    101 | -------------------------------------------------------------------------------- /src-tauri/src/store.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{devices::Device, packages::Package, users::User, DeviceWithUsers}; 4 | use anyhow::{anyhow, Result}; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct UserWithPackage { 8 | user: User, 9 | packages_map: HashMap, 10 | } 11 | 12 | impl UserWithPackage { 13 | pub fn add_package(&mut self, p: Package) { 14 | self.packages_map.insert(p.name.to_owned(), p); 15 | } 16 | 17 | pub fn get_package(&mut self, p: &str) -> Option<&mut Package> { 18 | self.packages_map.get_mut(p) 19 | } 20 | } 21 | 22 | #[derive(Debug, Clone)] 23 | pub struct DeviceWithUserPackages { 24 | device: Device, 25 | users_map: HashMap, 26 | } 27 | 28 | impl DeviceWithUserPackages { 29 | pub fn new_from_device_with_users(du: DeviceWithUsers) -> Self { 30 | let mut users_map: HashMap = HashMap::new(); 31 | for user in du.users { 32 | users_map.insert( 33 | user.id.to_owned(), 34 | UserWithPackage { 35 | user: user, 36 | packages_map: HashMap::new(), 37 | }, 38 | ); 39 | } 40 | 41 | return Self { 42 | device: du.device, 43 | users_map: users_map, 44 | }; 45 | } 46 | 47 | pub fn user(&mut self, user_id: String) -> Result<&mut UserWithPackage> { 48 | let user = self 49 | .users_map 50 | .get_mut(&user_id) 51 | .ok_or(anyhow!("user is invalid"))?; 52 | 53 | return Ok(user); 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone)] 58 | pub struct Store(HashMap); 59 | 60 | impl Store { 61 | pub fn new() -> Store { 62 | Store(HashMap::new()) 63 | } 64 | 65 | pub fn device(&mut self, device_id: String) -> Result<&mut DeviceWithUserPackages> { 66 | let device = self 67 | .0 68 | .get_mut(&device_id) 69 | .ok_or(anyhow!("device is invalid"))?; 70 | 71 | return Ok(device); 72 | } 73 | 74 | pub fn insert_device_with_user(&mut self, du: DeviceWithUsers) { 75 | let mut new_dup = DeviceWithUserPackages::new_from_device_with_users(du.clone()); 76 | let existing_dup = self.0.get(&du.device.id.to_owned()); 77 | match existing_dup { 78 | Some(dup) => { 79 | for (user_id, uwp) in &dup.users_map { 80 | let new_uwp = new_dup.user(user_id.to_string()); 81 | match new_uwp { 82 | Ok(nuwp) => { 83 | for (_, p) in &uwp.packages_map { 84 | nuwp.add_package(p.clone()) 85 | } 86 | }, 87 | Err(_) => {} 88 | } 89 | } 90 | } 91 | None => { 92 | self.0.insert(du.device.id.to_owned(), new_dup); 93 | return; 94 | } 95 | } 96 | return; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/lib/packages/PackagesList.ts: -------------------------------------------------------------------------------- 1 | import { setErrorModal } from '$lib/error'; 2 | import { selectedUserStore } from '$lib/users/stores'; 3 | import { listen } from '@tauri-apps/api/event'; 4 | import { get, type Unsubscriber } from 'svelte/store'; 5 | import { notifications } from '../notifications/stores'; 6 | import { 7 | adb_disable_package, 8 | adb_enable_package, 9 | adb_install_package, 10 | adb_list_packages 11 | } from './adb'; 12 | import type { DeviceUserPackage, Package } from './models'; 13 | import { packagesStore } from './stores'; 14 | 15 | export const discussion_create_url = (pkg: Package) => { 16 | // https://eric.blog/2016/01/08/prefilling-github-issues/#:~:text=Creating%20issues&text=As%20long%20as%20you're,using%20and%20testing%20your%20software. 17 | let body = encodeURI(''); 18 | return `https://github.com/thulasi-ram/simple_android_debloater/discussions/new?category=packages&title=${pkg.name}&body=${body}`; 19 | }; 20 | 21 | export function fetchPackagesIfEmptySubscription(): Unsubscriber { 22 | const unsub = selectedUserStore.subscribe((su) => { 23 | if (su) { 24 | if (!packagesStore.hasPackages(su.device_id, su.id)) { 25 | adb_list_packages(su.device_id, su.id) 26 | .then((pkgs) => { 27 | notifications.info(`fetched packages for ${su?.name}`); 28 | packagesStore.setPackages(pkgs.device_id, pkgs.user_id, pkgs.packages); 29 | }) 30 | .catch(setErrorModal); 31 | } 32 | } 33 | }); 34 | return unsub; 35 | } 36 | 37 | export async function packageEventListener() { 38 | await listen('package_event', (event) => { 39 | let ep = event.payload as DeviceUserPackage; 40 | packagesStore.addPackage(ep.device_id, ep.user_id, ep.package); 41 | }); 42 | } 43 | 44 | export function disablePackage(pkg: string) { 45 | let user = get(selectedUserStore); 46 | if (!user) { 47 | return setErrorModal('user is not selected'); 48 | } 49 | 50 | notifications.info(`disabling package: {pkg} - ${user.name} ${pkg}`); 51 | 52 | adb_disable_package(user.device_id, user.id, pkg) 53 | .then(() => { 54 | notifications.success(`${pkg} successfully disabled`); 55 | }) 56 | .catch((e) => { 57 | notifications.error(`error disabling ${pkg} - ${JSON.stringify(e)}`); 58 | }); 59 | } 60 | 61 | export function enablePackage(pkg: string) { 62 | let user = get(selectedUserStore); 63 | if (!user) { 64 | return setErrorModal('user is not selected'); 65 | } 66 | notifications.info(`enabling package: {pkg} - ${user.name} ${pkg}`); 67 | 68 | adb_enable_package(user.device_id, user.id, pkg) 69 | .then(() => { 70 | notifications.success(`${pkg} successfully enabled`); 71 | }) 72 | .catch((e) => { 73 | notifications.error(`error enabling ${pkg} - ${JSON.stringify(e)}`); 74 | }); 75 | } 76 | 77 | export function installPackage(pkg: string) { 78 | let user = get(selectedUserStore); 79 | if (!user) { 80 | return setErrorModal('user is not selected'); 81 | } 82 | notifications.info(`installing package: {pkg} - ${user.name} ${pkg}`); 83 | 84 | adb_install_package(user.device_id, user.id, pkg) 85 | .then(() => { 86 | notifications.success(`${pkg} successfully installed`); 87 | }) 88 | .catch((e) => { 89 | notifications.error(`error installing ${pkg} - ${JSON.stringify(e)}`); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /src/lib/devices/DevicesSidebarItems.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 | {#if Object.keys($devicesWithUsersStore).length > 0} 48 | {#each Object.entries($devicesWithUsersStore) as [_, d]} 49 | {@const dev = d.device} 50 | {@const hrefUrl = `/devices/${dev.id}`} 51 | {@const isLive = $liveDevicesStore[dev.id]} 52 | 53 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ({dev.model}) 69 | {#if !isLive} 70 | Disconnected: {dev.state} 71 | {/if} 72 | 73 | 74 | 75 | {/each} 76 | {:else} 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | {/if} 90 | -------------------------------------------------------------------------------- /src/routes/import/+page.svelte: -------------------------------------------------------------------------------- 1 | 71 | 72 | 73 | 74 | 75 | 76 |
    77 |
    78 | 82 | 83 |

    Import settings json previously exported

    84 |
    85 | 86 |
    87 | 91 | 92 |

    Bulk Disables Packages

    93 |
    94 | 95 |
    96 | 100 | 101 |

    Bulk Enable Packages

    102 |
    103 |
    104 | -------------------------------------------------------------------------------- /src-tauri/src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::{packages::Package, DeviceWithUsers}; 2 | use anyhow::{anyhow, Error, Ok, Result}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use std::{ 6 | fmt::Display, 7 | str::FromStr, 8 | sync::atomic::{AtomicUsize, Ordering}, 9 | }; 10 | 11 | #[derive(Serialize, Deserialize, Debug, Clone)] 12 | pub enum EventType { 13 | DeviceEvent, 14 | PackageEvent, 15 | } 16 | 17 | impl Display for EventType { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | match self { 20 | EventType::DeviceEvent => write!(f, "device_event"), 21 | EventType::PackageEvent => write!(f, "package_event"), 22 | } 23 | } 24 | } 25 | 26 | impl FromStr for EventType { 27 | type Err = Error; 28 | 29 | fn from_str(s: &str) -> Result { 30 | let lowercased = s.to_ascii_lowercase(); 31 | match lowercased.as_str() { 32 | "device_event" => Ok(Self::DeviceEvent), 33 | "package_event" => Ok(Self::PackageEvent), 34 | _ => Err(anyhow!("unknown device state {}", lowercased)), 35 | } 36 | } 37 | } 38 | 39 | pub trait Event { 40 | fn eid(self: &Self) -> String; 41 | fn etype(self: &Self) -> EventType; 42 | fn epayload(self: &Self) -> Result; 43 | } 44 | 45 | static COUNTER: AtomicUsize = AtomicUsize::new(1); 46 | 47 | fn get_id() -> String { 48 | let us = COUNTER.fetch_add(1, Ordering::Relaxed); 49 | return us.to_string(); 50 | } 51 | 52 | #[derive(Debug, Clone)] 53 | pub struct DeviceEvent { 54 | eid: String, 55 | etype: EventType, 56 | device: DeviceWithUsers, 57 | } 58 | 59 | impl DeviceEvent { 60 | pub fn new(d: DeviceWithUsers) -> Self { 61 | Self { 62 | eid: get_id(), 63 | etype: EventType::DeviceEvent, 64 | device: d, 65 | } 66 | } 67 | } 68 | 69 | impl Event for DeviceEvent { 70 | fn eid(&self) -> String { 71 | self.eid.to_owned() 72 | } 73 | 74 | fn etype(&self) -> EventType { 75 | self.etype.to_owned() 76 | } 77 | 78 | fn epayload(&self) -> Result { 79 | let res = serde_json::to_string(&self.device)?; 80 | return Ok(res); 81 | } 82 | } 83 | 84 | #[derive(Serialize, Deserialize, Debug, Clone)] 85 | 86 | pub struct DeviceUserPackage { 87 | device_id: String, 88 | user_id: String, 89 | package: Package, 90 | } 91 | 92 | #[derive(Debug, Clone)] 93 | pub struct PackageEvent { 94 | eid: String, 95 | etype: EventType, 96 | package: DeviceUserPackage, 97 | } 98 | 99 | impl PackageEvent { 100 | pub fn new(device_id: String, user_id: String, package: Package) -> Self { 101 | Self { 102 | eid: get_id(), 103 | etype: EventType::PackageEvent, 104 | package: DeviceUserPackage { 105 | device_id, 106 | user_id, 107 | package, 108 | }, 109 | } 110 | } 111 | } 112 | 113 | impl Event for PackageEvent { 114 | fn eid(&self) -> String { 115 | self.eid.to_owned() 116 | } 117 | 118 | fn etype(&self) -> EventType { 119 | self.etype.to_owned() 120 | } 121 | 122 | fn epayload(&self) -> Result { 123 | let res = serde_json::to_string(&self.package)?; 124 | return Ok(res); 125 | } 126 | } 127 | 128 | pub type AsyncEvent = Box; 129 | -------------------------------------------------------------------------------- /src/routes/export/export.ts: -------------------------------------------------------------------------------- 1 | import { configStore } from '$lib/config/stores'; 2 | import type { Device } from '$lib/devices/models'; 3 | import { devicesWithUsersStore, selectedDeviceStore } from '$lib/devices/stores'; 4 | import type { Package } from '$lib/packages/models'; 5 | import { currentPackagesStore, packagesKey, packagesStore } from '$lib/packages/stores'; 6 | import type { User } from '$lib/users/models'; 7 | import { selectedUserStore } from '$lib/users/stores'; 8 | import { CSV_DIALOG_FILTER, JSON_DIALOG_FILTER } from '$lib/utils'; 9 | import { save } from '@tauri-apps/api/dialog'; 10 | import { writeTextFile } from '@tauri-apps/api/fs'; 11 | import Papa from 'papaparse'; 12 | import { get } from 'svelte/store'; 13 | 14 | type DeviceUserExport = { 15 | device_id: string | undefined; 16 | device_name: string | undefined; 17 | user_id: string | undefined; 18 | user_name: string | undefined; 19 | }; 20 | 21 | type PackageExportCSV = Package & DeviceUserExport; 22 | 23 | type UserWithPackages = { 24 | user: User; 25 | packages: Package[]; 26 | }; 27 | 28 | type DeviceWithUserPackages = { 29 | device: Device; 30 | users: UserWithPackages[]; 31 | }; 32 | 33 | type PackageExportJSON = DeviceWithUserPackages[]; 34 | 35 | async function savePackagesCSV(data: PackageExportCSV[]) { 36 | const savePath = await save({ 37 | title: 'Save Packages Export CSV', 38 | filters: [CSV_DIALOG_FILTER] 39 | }); 40 | if (!savePath) return; 41 | 42 | const fcontent = Papa.unparse(data); 43 | 44 | await writeTextFile(savePath, fcontent); 45 | } 46 | 47 | async function savePackagesJSON(data: PackageExportJSON) { 48 | const savePath = await save({ 49 | title: 'Save Packages Export JSON', 50 | filters: [JSON_DIALOG_FILTER] 51 | }); 52 | if (!savePath) return; 53 | 54 | const fcontent = JSON.stringify(data); 55 | 56 | await writeTextFile(savePath, fcontent); 57 | } 58 | 59 | export async function exportPackagesCSV() { 60 | let device = get(selectedDeviceStore); 61 | if (!device) { 62 | throw new Error('device must be selected for export'); 63 | } 64 | 65 | let user = get(selectedUserStore); 66 | if (!user) { 67 | throw new Error('user must be selected for export'); 68 | } 69 | 70 | let packages = get(currentPackagesStore); 71 | 72 | let exportablePackages: PackageExportCSV[] = packages.map((p) => { 73 | return { 74 | name: p.name, 75 | ptype: p.ptype, 76 | state: p.state, 77 | package_prefix: p.package_prefix, 78 | device_id: device?.device.id, 79 | device_name: device?.device.name, 80 | user_id: user?.id, 81 | user_name: user?.name 82 | }; 83 | }); 84 | 85 | await savePackagesCSV(exportablePackages); 86 | } 87 | 88 | export async function exportPackagesJSON() { 89 | let deviceWithUsers = get(devicesWithUsersStore); 90 | 91 | let allPackages = get(packagesStore); 92 | 93 | let exportabledeviceWithUsers: DeviceWithUserPackages[] = Object.entries(deviceWithUsers).map( 94 | ([_, du]) => { 95 | return { 96 | device: du.device, 97 | users: du.users.map((u) => { 98 | let pkey = packagesKey(du.device.id, u.id); 99 | let packages = pkey ? allPackages[pkey] : []; 100 | return { 101 | user: u, 102 | packages: packages || [] 103 | }; 104 | }) 105 | }; 106 | } 107 | ); 108 | 109 | await savePackagesJSON(exportabledeviceWithUsers); 110 | } 111 | 112 | export async function exportAndSaveSettingsJSON() { 113 | const savePath = await save({ 114 | title: 'Save Packages Export JSON', 115 | filters: [JSON_DIALOG_FILTER] 116 | }); 117 | if (!savePath) return; 118 | 119 | const fcontent = JSON.stringify(get(configStore)); 120 | 121 | await writeTextFile(savePath, fcontent); 122 | } 123 | -------------------------------------------------------------------------------- /src/lib/assets/images/sad.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/discussions_dump.js: -------------------------------------------------------------------------------- 1 | // https://stepzen.com/blog/consume-graphql-in-javascript 2 | // https://stackoverflow.com/questions/42938472/what-is-the-reason-for-having-edges-and-nodes-in-a-connection-in-your-graphql-sc 3 | // https://docs.github.com/en/graphql/guides/using-the-graphql-api-for-discussions 4 | import fetch from 'node-fetch'; 5 | import fs from 'fs'; 6 | 7 | async function getDiscussions(no_of_discussions, next_cursor) { 8 | const reqData = JSON.stringify({ 9 | query: `query($no_of_discussions:Int!, $next_cursor: String) { 10 | repository(owner: "thulasi-ram", name: "simple_android_debloater") { 11 | discussions(first: $no_of_discussions, after: $next_cursor) { 12 | # type: DiscussionConnection 13 | totalCount # Int! 14 | 15 | pageInfo { 16 | startCursor 17 | endCursor 18 | hasNextPage 19 | hasPreviousPage 20 | } 21 | 22 | nodes { 23 | # type: Discussion 24 | id 25 | title 26 | closed 27 | body 28 | bodyHTML 29 | url 30 | answer { 31 | body 32 | bodyHTML 33 | } 34 | labels(first: 100) { 35 | totalCount 36 | nodes { 37 | name 38 | description 39 | } 40 | } 41 | 42 | } 43 | } 44 | } 45 | }`, 46 | variables: `{ 47 | "no_of_discussions": ${no_of_discussions}, 48 | "next_cursor": ${next_cursor == null ? null : `"${next_cursor}"`} 49 | }` 50 | }); 51 | 52 | const response = await fetch('https://api.github.com/graphql', { 53 | method: 'post', 54 | body: reqData, 55 | headers: { 56 | 'Content-Type': 'application/json', 57 | 'Content-Length': reqData.length, 58 | Authorization: 'bearer ' + process.env.GH_TOKEN, 59 | 'User-Agent': 'Node' 60 | } 61 | }); 62 | const json = await response.json(); 63 | 64 | let data = json.data; 65 | 66 | if (!data) { 67 | console.log(response); 68 | console.log(json); 69 | throw new Error('Error in response json'); 70 | } 71 | 72 | const discussions = data?.repository?.discussions; 73 | 74 | if (!discussions) { 75 | console.log(JSON.stringify(data)); 76 | } 77 | 78 | return discussions; 79 | } 80 | 81 | async function getAllDiscussions() { 82 | // https://stackoverflow.com/questions/71952373/how-to-run-graphql-query-in-a-loop-until-condition-is-not-matched 83 | 84 | let hasNext = true; 85 | let nextCursor = null; 86 | let allDiscussions = []; 87 | while (hasNext) { 88 | let discussionsData = await getDiscussions(1, nextCursor); 89 | if (!discussionsData) { 90 | break; 91 | } 92 | hasNext = discussionsData.pageInfo.hasNextPage; 93 | nextCursor = discussionsData.pageInfo.endCursor; 94 | allDiscussions.push(...discussionsData.nodes); 95 | } 96 | 97 | allDiscussions = allDiscussions.map((d) => parseDiscussion(d)).filter((d) => d != null); 98 | let discussionsDump = JSON.stringify(allDiscussions); 99 | console.log(discussionsDump); 100 | 101 | fs.writeFile('discussions_dump.json', discussionsDump, 'utf8', function (err) { 102 | if (err) throw err; 103 | console.log('Dumped json'); 104 | }); 105 | } 106 | 107 | const FILTER_BY_PACKAGE_LABEL = 'pkg'; 108 | 109 | function parseDiscussion(discussion) { 110 | if (!discussion) { 111 | return null; 112 | } 113 | 114 | const labelCount = discussion.labels?.totalCount; 115 | if (labelCount && labelCount > 100) { 116 | console.error(`"discussoon ${discussion} has more than 100 labels"`); 117 | } 118 | const labels = discussion.labels?.nodes; 119 | if (!labels) { 120 | return null; 121 | } 122 | 123 | let matched = labels.filter((l) => l.name === FILTER_BY_PACKAGE_LABEL); 124 | if (!matched) { 125 | return null; 126 | } 127 | discussion.labels = labels; 128 | return discussion; 129 | } 130 | 131 | getAllDiscussions(); 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Android Debloater 2 | 3 | This software is in beta 🚧 4 | 5 | Simple Android Debloater is a free and open source project to disable unwanted system apps that carriers / OEMs force install in our mobile phones. 6 | 7 | This is an attempt like [Universal Android Debloater](https://github.com/0x192/universal-android-debloater/) built with [Tauri](https://tauri.app/) and [Sveltekit](https://kit.svelte.dev/). 8 | 9 | Unlike UAD, this tool is aimed to be beginner friendly so as to not uninstall apps unexpectedly which can brick the device. 10 | 11 | **Note**: *Disabling system apps can also soft brick the device* 12 | 13 | 14 | 15 | ## Features 16 | 17 | ### Standard features 18 | - Disabling and Enabling a package 19 | - Auto detect devices and heartbeats 20 | - Customizable prompt settings 21 | - Search and Filter Packages 22 | 23 | ### Features over UAD 24 | - Labels and Discussions powered by Github Discussions 25 | - Can be crowdsourced and moderated 26 | - Refreshed automatically once a day and can be triggered manually 27 | - ![Discussion Screenshot](./static/screenshots/discussion.png) 28 | - Bulk Enable and Disable 29 | - ![Bulk Disable Screenshot](./static/screenshots/bulk_disable_packages.png) 30 | - Export and Import Settings, Results and Other data 31 | - ![Export Screenshot](./static/screenshots/export_packages.png) 32 | - ![Import Screenshot](./static/screenshots/import_packages.png) 33 | 34 | 35 | ## Download 36 | 37 | Goto the latest [Releases Page](https://github.com/thulasi-ram/simple_android_debloater/releases) click on assets and download the installers applicable for your OS. 38 | 39 | Supported Platforms: 40 | - Windows: Installer (setup.exe, .msi), 41 | - Mac: Installer (.dmg), App (.app) 42 | - Linux: Installer (.deb), Image (.AppImage) 43 | 44 | Screenshots are available in [static](./static/screenshots) directory. 45 | 46 | ## Usage 47 | 48 | ### Prerequisites 49 | - In the phone 50 | - Make sure Usb Debugging is turned on 51 | - This requires one to enable developer options 52 | - In the PC 53 | - Make sure to download [ADB Tools](https://developer.android.com/tools/releases/platform-tools#downloads) for your PC. 54 | - ADB need not be in Path. Setting path is tedious in windows. 55 | - Use settings -> custom_adb_path pointing to the downloaded folder 56 | - - ![Settings Screenshot](./static/screenshots/settings.png) 57 | 58 | [Read More from XDA On Setting up USB Debugging and ABD](https://www.xda-developers.com/install-adb-windows-macos-linux/) 59 | 60 | 61 | ### Usage 62 | 63 | ![Usage Screenshot](./static/screenshots/sad_v0.3.0-beta_usage.gif) 64 | 65 | - Click on Devices found in the left sidebar 66 | - Use the search bar for searching 67 | - Filtering for system, thirdparty, disabled, enabled app states 68 | - Packages that are disabled by DPM (Device Policy Manager) are hidden 69 | 70 | 71 | 72 | 73 | ## Development 74 | 75 | Frontend Server Only: 76 | `npm run dev` 77 | 78 | Run rust and node at once: 79 | `npm run tauri dev -- --verbose` 80 | 81 | [Tauri Quickstart Docs](https://tauri.app/v1/guides/getting-started/setup/sveltekit) 82 | 83 | ### Logs 84 | | Platform | Location | Example | 85 | |----------|----------------------------------------|------------------------------------------------------| 86 | | macOS | $HOME/Library/Logs/{bundleIdentifier} | /Users/Bob/Library/Logs/com.ahiravan.simple-android-debloater | 87 | | Windows | %APPDATA%\${bundleIdentifier}\logs | C:\Users\Bob\AppData\Roaming\com.ahiravan.simple-android-debloater\logs | 88 | | Linux | $HOME/.config/${bundleIdentifier}/logs | /home/bob/.config/com.ahiravan.simple-android-debloater/logs | 89 | 90 | 91 | ## TODOs: 92 | 93 | - [x] List Devices 94 | 95 | - [x] List Packages 96 | 97 | - [x] Hashset Packages 98 | 99 | - [x] Validate deviceID, userID, packageGetAll are valid 100 | 101 | - [x] Flowbite modal to open up if validation fails 102 | 103 | - [x] Disable packages 104 | 105 | - [x] Adb track device 106 | 107 | - [x] Github discussion for package 108 | 109 | - [x] ~Prepackage ADB~ Custom ADB Path Instead 110 | 111 | - [x] Persist Settings 112 | 113 | - [x] Dark Mode 114 | 115 | - [x] Export Packages in CSV / JSON 116 | 117 | - [x] Import CSV and Bulk Enable / Disable 118 | 119 | - [ ] SDK Compatability checks 120 | -------------------------------------------------------------------------------- /src-tauri/src/adb_cmd.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | #[cfg(target_os = "windows")] 4 | use std::os::windows::process::CommandExt; 5 | 6 | use log::info; 7 | 8 | pub trait ADBCommand: Sized { 9 | fn execute(&self) -> Result; 10 | fn arg>(self, arg: S) -> Self; 11 | fn args(self, args: I) -> Self 12 | where 13 | I: IntoIterator, 14 | S: AsRef, 15 | { 16 | let mut s1 = self; 17 | for arg in args { 18 | s1 = s1.arg(arg); 19 | } 20 | s1 21 | } 22 | 23 | fn arg_prepend>(self, arg: S) -> Self; 24 | fn args_prepend(self, args: I) -> Self 25 | where 26 | I: IntoIterator, 27 | S: AsRef, 28 | { 29 | let mut s1 = self; 30 | for arg in args { 31 | s1 = s1.arg_prepend(arg); 32 | } 33 | s1 34 | } 35 | } 36 | 37 | #[derive(Debug, thiserror::Error)] 38 | pub enum ADBError { 39 | #[error("ADB Error {0}")] 40 | Unknown(String), 41 | } 42 | 43 | #[derive(Debug, Clone, Eq, PartialEq)] 44 | pub struct ADBRaw { 45 | cmd_str: String, 46 | argsv: Vec, 47 | } 48 | 49 | impl ADBRaw { 50 | pub fn new(adb_path: String) -> Self { 51 | let mut cmd_str = "adb"; 52 | if !adb_path.is_empty() { 53 | cmd_str = adb_path.as_str(); 54 | } 55 | Self { 56 | cmd_str: cmd_str.to_string(), 57 | argsv: vec![], 58 | } 59 | } 60 | } 61 | 62 | impl ADBCommand for ADBRaw { 63 | fn arg>(self, arg: S) -> Self { 64 | // https://users.rust-lang.org/t/best-way-to-clone-and-append-a-single-element/68675/2 65 | 66 | let mut s1 = self; 67 | s1.argsv.push(arg.as_ref().to_owned()); 68 | return s1; 69 | } 70 | 71 | fn arg_prepend>(self, arg: S) -> Self { 72 | let mut s1 = self; 73 | s1.argsv.insert(0, arg.as_ref().to_owned()); 74 | return s1; 75 | } 76 | 77 | fn execute(&self) -> Result { 78 | let mut command = Command::new(self.cmd_str.to_owned()); 79 | command.args(self.argsv.to_vec()); 80 | 81 | // let args = self 82 | // .sub_commands 83 | // .iter() 84 | // .map(|s| s.as_str()) 85 | // .collect::>(); 86 | // command.args(args); 87 | 88 | info!("command {:?}", command); 89 | 90 | #[cfg(target_os = "windows")] 91 | let command = command.creation_flags(0x08000000); // do not open a cmd window 92 | 93 | match command.output() { 94 | Err(e) => Err(ADBError::Unknown(e.to_string())), 95 | Ok(o) => { 96 | if o.status.success() { 97 | Ok(String::from_utf8(o.stdout) 98 | .map_err(|e| ADBError::Unknown(e.to_string()))? 99 | .trim_end() 100 | .to_string()) 101 | } else { 102 | let stdout = String::from_utf8(o.stdout) 103 | .map_err(|e| ADBError::Unknown(e.to_string()))? 104 | .trim_end() 105 | .to_string(); 106 | let stderr = String::from_utf8(o.stderr) 107 | .map_err(|e| ADBError::Unknown(e.to_string()))? 108 | .trim_end() 109 | .to_string(); 110 | 111 | // ADB does really weird things. Some errors are not redirected to stderr 112 | let err = if stdout.is_empty() { stderr } else { stdout }; 113 | Err(ADBError::Unknown(err.to_string())) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | #[derive(Debug, Clone)] 121 | pub struct ADBShell { 122 | adb_raw: ADBRaw, 123 | } 124 | 125 | impl ADBShell { 126 | pub fn new(adb_path: String) -> Self { 127 | let adbr = ADBRaw::new(adb_path).arg("shell"); 128 | Self { adb_raw: adbr } 129 | } 130 | } 131 | 132 | impl ADBCommand for ADBShell { 133 | fn arg>(self, arg: S) -> Self { 134 | let mut s1 = self; 135 | s1.adb_raw = s1.adb_raw.arg(arg.as_ref()); 136 | return s1; 137 | } 138 | 139 | fn arg_prepend>(self, arg: S) -> Self { 140 | let mut s1 = self; 141 | s1.adb_raw = s1.adb_raw.arg_prepend(arg.as_ref()); 142 | return s1; 143 | } 144 | 145 | fn execute(&self) -> Result { 146 | return self.adb_raw.execute(); 147 | } 148 | } 149 | 150 | pub fn for_device<'a, T: ADBCommand + Clone>(abdc: &'a T, device_id: String) -> T { 151 | // ideally its -s but we send in reverse so prepend works properly 152 | return abdc 153 | .clone() 154 | .args_prepend(vec!["-s", &device_id].into_iter().rev()); 155 | } 156 | -------------------------------------------------------------------------------- /src/lib/packages/PackageCSVEnablerDisabler.svelte: -------------------------------------------------------------------------------- 1 | 124 | 125 | 126 | {#await packagesPromise()} 127 | Parsing CSV File... 128 | {:then} 129 |
    130 | 139 |
    140 | 141 | 142 | 143 | Packages 144 | Status 145 | 146 | 147 | {#each Object.entries(packagesStatusMap) as [p, status]} 148 | 149 | {p} 150 | {status} 151 | 152 | {/each} 153 | 154 |
    155 | {:catch error} 156 | {error} 157 | {/await} 158 | 159 | 160 |
    161 | 162 | 163 | This is a potentially dangerous action and can brick the device. I agree to process. 164 | 165 | 166 | 167 |
    168 | {#if processingCount > 0} 169 | 182 | {:else} 183 | 184 | {/if} 185 | 186 |
    187 | 188 | 192 |
    193 |
    194 | 195 | 196 | -------------------------------------------------------------------------------- /src/lib/packages/PackagesList.svelte: -------------------------------------------------------------------------------- 1 | 64 | 65 |
    66 | 67 |
    68 |

    69 | Are you sure you want to disable package {disablePackageName}? 70 |

    71 | 78 | 79 |
    80 |
    81 | 82 | 83 | 84 | {#each $filteredPackages as pkg} 85 | {@const pkgDiscussion = $packageDiscussionsStore[pkg.name]} 86 | {@const isDiscussionRowActive = selectedRowPkgName === pkg.name} 87 | {@const discussionRowActiveClass = isDiscussionRowActive 88 | ? 'border-b-0 bg-gray-100 dark:bg-gray-700' 89 | : ''} 90 | 91 | toggleSeletcedRowPkgName(pkg.name)} 93 | class="cursor-pointer {discussionRowActiveClass}" 94 | > 95 | 96 | {#if !isDiscussionRowActive} 97 | 100 | {:else} 101 | 102 | {/if} 103 | 104 | 105 | {pkg.name} 106 | {#if pkg.ptype == 'system'} 107 | {pkg.ptype} 108 | {:else} 109 | {pkg.ptype} 110 | {/if} 111 |

    {pkg.package_prefix}

    112 |
    113 | 114 | 115 | {#if pkg.state == 'Enabled'} 116 | 123 | {:else if pkg.state == 'Disabled'} 124 | 131 | {:else if pkg.state == 'Uninstalled'} 132 | 139 | {:else} 140 | {@const hideID = `hide-${pkg.name}`} 141 | 144 | {/if} 145 | 146 |
    147 | 148 | {#if isDiscussionRowActive} 149 | 150 | 151 | 156 | {#if pkgDiscussion} 157 |
    158 | {#each pkgDiscussion.labels as l} 159 | {l.name} 160 | {/each} 161 |
    162 | 163 | 164 | {@html pkgDiscussion.bodyHTML} 165 | 166 | View the entire discussion 172 | {:else} 173 |

    No Discussion for this package.

    174 | Create one? 179 | {/if} 180 |
    181 |
    182 | {/if} 183 | {/each} 184 |
    185 |
    186 |
    187 | -------------------------------------------------------------------------------- /src/lib/packages/FilterAndSearchPackages.svelte: -------------------------------------------------------------------------------- 1 | 94 | 95 |
    96 |
    97 |
    98 | 99 | 100 | 101 | 102 | 103 |
    104 |
    105 | 106 |
    107 | 112 | 113 | 114 | 115 | 116 | 124 | 125 |
    126 |
    127 |
    Package State
    128 |
    129 | 130 | {#each filterPackageStates as fs} 131 | 132 | {fs[0]} 133 | 134 | {/each} 135 |
    136 | 137 |
    138 |
    Package Type
    139 |
    140 | 141 | {#each filterPackageTypes as ft} 142 | 143 | {ft[0]} 144 | 145 | {/each} 146 |
    147 | 148 |
    149 |
    Label Types
    150 |
    151 |
    152 | {#each Object.entries($packageLabelsStore) as [labelName, labelDesc]} 153 | {@const labelID = `label-cb-${labelName}`} 154 | 155 | 161 | {labelName} 167 | 168 | 169 | 172 | 173 | {/each} 174 |
    175 |
    176 |
    177 |
    178 | 179 |
    180 |
    181 | -------------------------------------------------------------------------------- /src-tauri/src/devices.rs: -------------------------------------------------------------------------------- 1 | use crate::adb_cmd::{self, ADBCommand, ADBRaw, ADBShell}; 2 | use anyhow::{anyhow, Error, Result}; 3 | use core::result::Result::Ok; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{fmt::Display, str::FromStr}; 6 | 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | pub enum DeviceState { 9 | /// The device is not connected to adb or is not responding. 10 | Offline, 11 | /// The device is now connected to the adb server. Note that this state does not imply that the Android system is fully booted and operational because the device connects to adb while the system is still booting. However, after boot-up, this is the normal operational state of an device. 12 | Device, 13 | /// There is no device connected. 14 | NoDevice, 15 | /// The device is unauthorized. 16 | Unauthorized, 17 | } 18 | 19 | impl Display for DeviceState { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | match self { 22 | DeviceState::Offline => write!(f, "offline"), 23 | DeviceState::Device => write!(f, "device"), 24 | DeviceState::NoDevice => write!(f, "no device"), 25 | DeviceState::Unauthorized => write!(f, "unauthorized"), 26 | } 27 | } 28 | } 29 | 30 | impl FromStr for DeviceState { 31 | type Err = Error; 32 | 33 | fn from_str(s: &str) -> Result { 34 | let lowercased = s.to_ascii_lowercase(); 35 | match lowercased.as_str() { 36 | "offline" => Ok(Self::Offline), 37 | "device" => Ok(Self::Device), 38 | "no device" => Ok(Self::NoDevice), 39 | "unauthorized" => Ok(Self::Unauthorized), 40 | _ => Err(anyhow!("unknown device state {}", lowercased)), 41 | } 42 | } 43 | } 44 | 45 | #[derive(Serialize, Deserialize, Debug, Clone)] 46 | pub struct Device { 47 | pub id: String, 48 | pub state: DeviceState, 49 | pub make: String, 50 | pub model: String, 51 | pub name: String, 52 | } 53 | 54 | pub trait ListDevices { 55 | fn list_devices(&self) -> Result>; 56 | } 57 | 58 | pub struct ADBTerminalImpl { 59 | pub adb_path: String, 60 | } 61 | 62 | impl ADBTerminalImpl { 63 | pub fn list_devices(&self) -> Result> { 64 | let res = ADBRaw::new(self.adb_path.to_owned()) 65 | .arg("devices") 66 | .execute(); 67 | match res { 68 | Err(e) => { 69 | return Err(e.into()); 70 | } 71 | Ok(o) => { 72 | let ot = o.replace("List of devices attached", ""); 73 | let ots = ot.trim(); 74 | 75 | let devices: Result> = ots 76 | .lines() 77 | .map(|s| Self::_parse_device(s)) 78 | .map(|d| self._set_prop(d)) 79 | .collect(); 80 | 81 | return devices; 82 | } 83 | } 84 | } 85 | 86 | fn _parse_device(s: &str) -> Result { 87 | let ss: Vec<&str> = s.split_whitespace().collect(); 88 | if ss.len() < 2 { 89 | return Err(anyhow!("unable to parse device. input {}", s)); 90 | } 91 | return Ok(Device { 92 | id: ss[0].to_string(), 93 | state: DeviceState::from_str(ss[1]).unwrap(), 94 | make: String::from(""), 95 | model: String::from(""), 96 | name: String::from(""), 97 | }); 98 | } 99 | 100 | fn _set_prop(&self, device: Result) -> Result { 101 | match device { 102 | Err(e) => { 103 | return Err(e); 104 | } 105 | Ok(d) => { 106 | let shell_cmd: ADBShell = 107 | adb_cmd::for_device(&ADBShell::new(self.adb_path.to_owned()), d.id.to_owned()); 108 | let res = shell_cmd.arg("getprop").execute(); 109 | match res { 110 | Err(e) => { 111 | return Err(e.into()); 112 | } 113 | Ok(o) => { 114 | // helper to extract the prop when a match is found 115 | let parse_val = |v: &str| { 116 | let split = v.split_once(":"); 117 | 118 | match split { 119 | Some(s) => { 120 | return s 121 | .1 122 | .trim() 123 | .trim_start_matches("[") 124 | .trim_end_matches("]") 125 | .to_string() 126 | } 127 | None => { 128 | return String::from("parse err"); 129 | } 130 | } 131 | }; 132 | 133 | let (mut make, mut model, mut name) = 134 | (String::from(""), String::from(""), String::from("")); 135 | 136 | for l in o.lines() { 137 | match l { 138 | s if s.contains("ro.product.product.brand") => { 139 | make = parse_val(s); 140 | } 141 | s if s.contains("ro.product.model") => { 142 | model = parse_val(s); 143 | } 144 | s if s.contains("ro.product.odm.marketname") => { 145 | name = parse_val(s); 146 | } 147 | _ => (), 148 | } 149 | } 150 | 151 | return Ok(Device { 152 | id: d.id.to_owned(), 153 | state: d.state, 154 | make, 155 | model, 156 | name, 157 | }); 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /scripts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "scripts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@octokit/core": "^5.0.0", 13 | "node-fetch": "^3.3.2" 14 | } 15 | }, 16 | "node_modules/@octokit/auth-token": { 17 | "version": "4.0.0", 18 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", 19 | "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", 20 | "engines": { 21 | "node": ">= 18" 22 | } 23 | }, 24 | "node_modules/@octokit/core": { 25 | "version": "5.0.0", 26 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.0.tgz", 27 | "integrity": "sha512-YbAtMWIrbZ9FCXbLwT9wWB8TyLjq9mxpKdgB3dUNxQcIVTf9hJ70gRPwAcqGZdY6WdJPZ0I7jLaaNDCiloGN2A==", 28 | "dependencies": { 29 | "@octokit/auth-token": "^4.0.0", 30 | "@octokit/graphql": "^7.0.0", 31 | "@octokit/request": "^8.0.2", 32 | "@octokit/request-error": "^5.0.0", 33 | "@octokit/types": "^11.0.0", 34 | "before-after-hook": "^2.2.0", 35 | "universal-user-agent": "^6.0.0" 36 | }, 37 | "engines": { 38 | "node": ">= 18" 39 | } 40 | }, 41 | "node_modules/@octokit/endpoint": { 42 | "version": "9.0.0", 43 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.0.tgz", 44 | "integrity": "sha512-szrQhiqJ88gghWY2Htt8MqUDO6++E/EIXqJ2ZEp5ma3uGS46o7LZAzSLt49myB7rT+Hfw5Y6gO3LmOxGzHijAQ==", 45 | "dependencies": { 46 | "@octokit/types": "^11.0.0", 47 | "is-plain-object": "^5.0.0", 48 | "universal-user-agent": "^6.0.0" 49 | }, 50 | "engines": { 51 | "node": ">= 18" 52 | } 53 | }, 54 | "node_modules/@octokit/graphql": { 55 | "version": "7.0.1", 56 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.1.tgz", 57 | "integrity": "sha512-T5S3oZ1JOE58gom6MIcrgwZXzTaxRnxBso58xhozxHpOqSTgDS6YNeEUvZ/kRvXgPrRz/KHnZhtb7jUMRi9E6w==", 58 | "dependencies": { 59 | "@octokit/request": "^8.0.1", 60 | "@octokit/types": "^11.0.0", 61 | "universal-user-agent": "^6.0.0" 62 | }, 63 | "engines": { 64 | "node": ">= 18" 65 | } 66 | }, 67 | "node_modules/@octokit/openapi-types": { 68 | "version": "18.0.0", 69 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz", 70 | "integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==" 71 | }, 72 | "node_modules/@octokit/request": { 73 | "version": "8.1.1", 74 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.1.tgz", 75 | "integrity": "sha512-8N+tdUz4aCqQmXl8FpHYfKG9GelDFd7XGVzyN8rc6WxVlYcfpHECnuRkgquzz+WzvHTK62co5di8gSXnzASZPQ==", 76 | "dependencies": { 77 | "@octokit/endpoint": "^9.0.0", 78 | "@octokit/request-error": "^5.0.0", 79 | "@octokit/types": "^11.1.0", 80 | "is-plain-object": "^5.0.0", 81 | "universal-user-agent": "^6.0.0" 82 | }, 83 | "engines": { 84 | "node": ">= 18" 85 | } 86 | }, 87 | "node_modules/@octokit/request-error": { 88 | "version": "5.0.0", 89 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.0.tgz", 90 | "integrity": "sha512-1ue0DH0Lif5iEqT52+Rf/hf0RmGO9NWFjrzmrkArpG9trFfDM/efx00BJHdLGuro4BR/gECxCU2Twf5OKrRFsQ==", 91 | "dependencies": { 92 | "@octokit/types": "^11.0.0", 93 | "deprecation": "^2.0.0", 94 | "once": "^1.4.0" 95 | }, 96 | "engines": { 97 | "node": ">= 18" 98 | } 99 | }, 100 | "node_modules/@octokit/types": { 101 | "version": "11.1.0", 102 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", 103 | "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", 104 | "dependencies": { 105 | "@octokit/openapi-types": "^18.0.0" 106 | } 107 | }, 108 | "node_modules/before-after-hook": { 109 | "version": "2.2.3", 110 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", 111 | "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" 112 | }, 113 | "node_modules/data-uri-to-buffer": { 114 | "version": "4.0.1", 115 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 116 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 117 | "engines": { 118 | "node": ">= 12" 119 | } 120 | }, 121 | "node_modules/deprecation": { 122 | "version": "2.3.1", 123 | "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", 124 | "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" 125 | }, 126 | "node_modules/fetch-blob": { 127 | "version": "3.2.0", 128 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 129 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 130 | "funding": [ 131 | { 132 | "type": "github", 133 | "url": "https://github.com/sponsors/jimmywarting" 134 | }, 135 | { 136 | "type": "paypal", 137 | "url": "https://paypal.me/jimmywarting" 138 | } 139 | ], 140 | "dependencies": { 141 | "node-domexception": "^1.0.0", 142 | "web-streams-polyfill": "^3.0.3" 143 | }, 144 | "engines": { 145 | "node": "^12.20 || >= 14.13" 146 | } 147 | }, 148 | "node_modules/formdata-polyfill": { 149 | "version": "4.0.10", 150 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 151 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 152 | "dependencies": { 153 | "fetch-blob": "^3.1.2" 154 | }, 155 | "engines": { 156 | "node": ">=12.20.0" 157 | } 158 | }, 159 | "node_modules/is-plain-object": { 160 | "version": "5.0.0", 161 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", 162 | "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", 163 | "engines": { 164 | "node": ">=0.10.0" 165 | } 166 | }, 167 | "node_modules/node-domexception": { 168 | "version": "1.0.0", 169 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 170 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 171 | "funding": [ 172 | { 173 | "type": "github", 174 | "url": "https://github.com/sponsors/jimmywarting" 175 | }, 176 | { 177 | "type": "github", 178 | "url": "https://paypal.me/jimmywarting" 179 | } 180 | ], 181 | "engines": { 182 | "node": ">=10.5.0" 183 | } 184 | }, 185 | "node_modules/node-fetch": { 186 | "version": "3.3.2", 187 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 188 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 189 | "dependencies": { 190 | "data-uri-to-buffer": "^4.0.0", 191 | "fetch-blob": "^3.1.4", 192 | "formdata-polyfill": "^4.0.10" 193 | }, 194 | "engines": { 195 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 196 | }, 197 | "funding": { 198 | "type": "opencollective", 199 | "url": "https://opencollective.com/node-fetch" 200 | } 201 | }, 202 | "node_modules/once": { 203 | "version": "1.4.0", 204 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 205 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 206 | "dependencies": { 207 | "wrappy": "1" 208 | } 209 | }, 210 | "node_modules/universal-user-agent": { 211 | "version": "6.0.0", 212 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", 213 | "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" 214 | }, 215 | "node_modules/web-streams-polyfill": { 216 | "version": "3.2.1", 217 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 218 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", 219 | "engines": { 220 | "node": ">= 8" 221 | } 222 | }, 223 | "node_modules/wrappy": { 224 | "version": "1.0.2", 225 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 226 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | mod adb_cmd; 5 | mod cache; 6 | mod config; 7 | mod db; 8 | mod devices; 9 | mod err; 10 | mod events; 11 | mod packages; 12 | mod sad; 13 | mod store; 14 | mod users; 15 | 16 | use std::{env, time::Duration}; 17 | 18 | use anyhow::anyhow; 19 | use config::Config; 20 | use err::{IntoSADError, ResultOkPrintErrExt}; 21 | use events::{Event, PackageEvent}; 22 | use futures; 23 | use log::error; 24 | use packages::Package; 25 | use serde::{Deserialize, Serialize}; 26 | use sqlx::SqlitePool; 27 | 28 | use tauri::Manager; 29 | use tokio::{sync::mpsc, time}; 30 | 31 | use devices::Device; 32 | use sad::SADError; 33 | use tauri_plugin_log::{fern::colors::ColoredLevelConfig, LogTarget}; 34 | use users::User; 35 | 36 | struct App { 37 | db: tokio::sync::Mutex>, 38 | pub event_emitter: tokio::sync::Mutex>, 39 | pub cache: tokio::sync::Mutex, 40 | } 41 | 42 | impl App { 43 | fn new(s: mpsc::Sender) -> Self { 44 | Self { 45 | db: tokio::sync::Mutex::new(Err(anyhow!("connection is none"))), 46 | event_emitter: tokio::sync::Mutex::new(s), 47 | cache: tokio::sync::Mutex::new(cache::Cache::new()), 48 | } 49 | } 50 | 51 | async fn config(&self) -> anyhow::Result { 52 | let db_res = self.db.lock().await; 53 | let db = db_res.as_ref().unwrap(); 54 | let cache = &mut self.cache.lock().await; 55 | let config = cache.get_config(db).await?; 56 | return Ok(config); 57 | } 58 | } 59 | 60 | #[tokio::main] 61 | async fn main() { 62 | fix_path_env::fix().unwrap(); 63 | 64 | let (async_event_sender, mut async_event_receiver): ( 65 | mpsc::Sender, 66 | mpsc::Receiver, 67 | ) = mpsc::channel(1); 68 | 69 | let app = App::new(async_event_sender); 70 | 71 | #[cfg(debug_assertions)] 72 | const LOG_TARGETS: [LogTarget; 2] = [LogTarget::Stdout, LogTarget::Webview]; 73 | 74 | #[cfg(not(debug_assertions))] 75 | const LOG_TARGETS: [LogTarget; 2] = [LogTarget::Stdout, LogTarget::LogDir]; 76 | 77 | tauri::async_runtime::set(tokio::runtime::Handle::current()); 78 | 79 | tauri::Builder::default() 80 | .manage(app) 81 | .plugin( 82 | tauri_plugin_log::Builder::default() 83 | .targets(LOG_TARGETS) 84 | .with_colors(ColoredLevelConfig::default()) 85 | .build(), 86 | ) 87 | .invoke_handler(tauri::generate_handler![ 88 | greet, 89 | adb_list_devices_with_users, 90 | adb_list_packages, 91 | adb_disable_clear_and_stop_package, 92 | adb_enable_package, 93 | adb_install_package, 94 | get_config, 95 | update_config, 96 | ]) 97 | .setup(|app| { 98 | let app_handle = app.handle(); 99 | 100 | let init_db_fut = async move { 101 | let conn = db::init(&app_handle).await.expect("unable to init db"); 102 | let app_state: tauri::State = app_handle.state(); 103 | let mut app_db = app_state.db.lock().await; 104 | *app_db = Ok(conn); 105 | }; 106 | 107 | futures::executor::block_on(init_db_fut); 108 | 109 | let app_handle = app.handle(); 110 | tauri::async_runtime::spawn(async move { 111 | loop { 112 | if let Some(output) = async_event_receiver.recv().await { 113 | event_publisher(output, &app_handle); 114 | } 115 | } 116 | }); 117 | 118 | let app_handle = app.handle(); 119 | tauri::async_runtime::spawn(async move { 120 | let mut interval = time::interval(Duration::from_millis(3000)); 121 | loop { 122 | interval.tick().await; 123 | track_devices(&app_handle).await; 124 | } 125 | }); 126 | 127 | Ok(()) 128 | }) 129 | .run(tauri::generate_context!()) 130 | .expect("error while running tauri application"); 131 | } 132 | 133 | /// Since we pass events via mpsc channel a serializable trait is not object safe 134 | /// So we resort to deserialization before passing event and then again serialize in emit_all of tauri 135 | fn event_publisher(event: events::AsyncEvent, manager: &impl Manager) { 136 | let pl: serde_json::Value = serde_json::from_str(&event.epayload().unwrap()).unwrap(); 137 | manager.emit_all(&event.etype().to_string(), pl).unwrap(); 138 | } 139 | 140 | async fn track_devices(manager: &impl Manager) { 141 | let app: tauri::State<'_, App> = manager.state(); 142 | let config = app.config().await.unwrap(); 143 | 144 | let res = _adb_list_device_with_users(config).await; 145 | match res { 146 | Err(e) => { 147 | error!("Error getting async devices {:?}", e); 148 | } 149 | Ok(device_with_users) => { 150 | let w = manager.get_window("main").unwrap(); 151 | let mut cache = app.cache.lock().await; 152 | 153 | for du in device_with_users { 154 | let event = events::DeviceEvent::new(du.clone()); 155 | let pl: serde_json::Value = 156 | serde_json::from_str(&event.epayload().unwrap()).unwrap(); 157 | cache.devices_store.insert_device_with_user(du); 158 | 159 | let res = w.emit_all(&event.etype().to_string(), pl); 160 | match res { 161 | Ok(_) => {} 162 | Err(e) => { 163 | error!("Error emitting async devices {:?}", e); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | 171 | #[tauri::command] 172 | fn greet(name: &str) -> String { 173 | format!("Hello, {}!", name) 174 | } 175 | 176 | #[derive(Serialize, Deserialize, Debug, Clone)] 177 | pub struct DeviceWithUsers { 178 | device: Device, 179 | users: Vec, 180 | } 181 | 182 | #[tauri::command] 183 | async fn adb_list_devices_with_users( 184 | app: tauri::State<'_, App>, 185 | ) -> Result, SADError> { 186 | let config = app.config().await.into_sad_error("unable to get config")?; 187 | 188 | let res = _adb_list_device_with_users(config).await; 189 | match res { 190 | Err(e) => { 191 | return Err(SADError::E(e)); 192 | } 193 | Ok(device_with_users) => { 194 | let mut cache = app.cache.lock().await; 195 | 196 | for du in device_with_users.clone() { 197 | cache.devices_store.insert_device_with_user(du.clone()); 198 | } 199 | 200 | return Ok(device_with_users); 201 | } 202 | } 203 | } 204 | 205 | async fn _adb_list_device_with_users( 206 | config: config::Config, 207 | ) -> anyhow::Result> { 208 | let mut device_with_users: Vec = vec![]; 209 | 210 | let acd = devices::ADBTerminalImpl { 211 | adb_path: config.custom_adb_path.to_owned(), 212 | }; 213 | let acu = users::ADBTerminalImpl { 214 | adb_path: config.custom_adb_path.to_owned(), 215 | }; 216 | let devices = acd.list_devices()?; 217 | 218 | for device in devices { 219 | let users: Vec = acu.list_users(device.id.to_owned())?; 220 | device_with_users.push(DeviceWithUsers { device, users }); 221 | } 222 | 223 | return anyhow::Ok(device_with_users); 224 | } 225 | 226 | #[tauri::command] 227 | async fn adb_list_packages( 228 | device_id: &str, 229 | user_id: &str, 230 | app: tauri::State<'_, App>, 231 | ) -> Result, SADError> { 232 | let config = app.config().await.unwrap(); 233 | 234 | let acl = packages::ADBTerminalImpl::new(config.custom_adb_path); 235 | let packages = acl.list_packages(device_id.to_string(), user_id.to_string())?; 236 | 237 | let mut cache = app.cache.lock().await; 238 | let device = cache.devices_store.device(device_id.to_owned())?; 239 | let user = device.user(user_id.to_owned())?; 240 | for p in packages.clone() { 241 | user.add_package(p.clone()) 242 | } 243 | return Ok(packages); 244 | } 245 | 246 | #[tauri::command] 247 | async fn adb_disable_clear_and_stop_package( 248 | device_id: &str, 249 | user_id: &str, 250 | pkg: &str, 251 | app: tauri::State<'_, App>, 252 | ) -> Result { 253 | let config = app.config().await.into_sad_error("unable to get config")?; 254 | 255 | let acl = packages::ADBTerminalImpl::new(config.custom_adb_path); 256 | acl.disable_package( 257 | device_id.to_string(), 258 | user_id.to_string(), 259 | pkg.to_string(), 260 | config.clear_packages_on_disable, 261 | )?; 262 | 263 | { 264 | let mut cache = app.cache.lock().await; 265 | let device = cache.devices_store.device(device_id.to_owned())?; 266 | let user = device.user(user_id.to_owned())?; 267 | let package = user.get_package(pkg); 268 | 269 | match package { 270 | None => { 271 | return Err(anyhow!("package {} not found in cache", pkg.to_string()).into()); 272 | } 273 | Some(p) => { 274 | p.set_state(packages::PackageState::Disabled); 275 | let pe = PackageEvent::new(device_id.to_string(), user_id.to_string(), p.clone()); 276 | let esender = app.event_emitter.lock().await; 277 | esender 278 | .send(Box::new(pe)) 279 | .await 280 | .ok_or_print_err("error emitting"); 281 | return Ok(p.clone()); 282 | } 283 | } 284 | } 285 | } 286 | 287 | #[tauri::command] 288 | async fn adb_enable_package( 289 | device_id: &str, 290 | user_id: &str, 291 | pkg: &str, 292 | app: tauri::State<'_, App>, 293 | ) -> Result { 294 | let config = app.config().await.into_sad_error("unable to get config")?; 295 | 296 | let acl = packages::ADBTerminalImpl::new(config.custom_adb_path); 297 | acl.enable_package(device_id.to_string(), user_id.to_string(), pkg.to_string())?; 298 | 299 | { 300 | let mut cache = app.cache.lock().await; 301 | let device = cache.devices_store.device(device_id.to_owned())?; 302 | let user = device.user(user_id.to_owned())?; 303 | let package = user.get_package(pkg); 304 | 305 | match package { 306 | None => { 307 | return Err(anyhow!("package {} not found in cache", pkg.to_string()).into()); 308 | } 309 | Some(p) => { 310 | p.set_state(packages::PackageState::Enabled); 311 | let pe = PackageEvent::new(device_id.to_string(), user_id.to_string(), p.clone()); 312 | let esender = app.event_emitter.lock().await; 313 | esender 314 | .send(Box::new(pe)) 315 | .await 316 | .ok_or_print_err("error emitting"); 317 | return Ok(p.clone()); 318 | } 319 | } 320 | } 321 | } 322 | 323 | #[tauri::command] 324 | async fn adb_install_package( 325 | device_id: &str, 326 | user_id: &str, 327 | pkg: &str, 328 | app: tauri::State<'_, App>, 329 | ) -> Result<(), SADError> { 330 | let config = app.config().await.into_sad_error("unable to get config")?; 331 | 332 | let acl = packages::ADBTerminalImpl::new(config.custom_adb_path); 333 | acl.install_package(device_id.to_string(), user_id.to_string(), pkg.to_string())?; 334 | 335 | { 336 | let mut cache = app.cache.lock().await; 337 | let device = cache.devices_store.device(device_id.to_owned())?; 338 | let user = device.user(user_id.to_owned())?; 339 | let package = user.get_package(pkg); 340 | 341 | match package { 342 | None => { 343 | return Err( 344 | anyhow!("package {} not found in cache for install", pkg.to_string()).into(), 345 | ); 346 | } 347 | Some(p) => { 348 | p.set_state(packages::PackageState::Enabled); 349 | let pe = PackageEvent::new(device_id.to_string(), user_id.to_string(), p.clone()); 350 | let esender = app.event_emitter.lock().await; 351 | esender 352 | .send(Box::new(pe)) 353 | .await 354 | .ok_or_print_err("error emitting"); 355 | } 356 | } 357 | } 358 | 359 | return Ok(()); 360 | } 361 | 362 | #[tauri::command] 363 | async fn get_config(app: tauri::State<'_, App>) -> Result { 364 | let db_guard = &app.db.lock().await; 365 | let db_conn = db_guard.as_ref().into_sad_error("")?; 366 | let mut cache = app.cache.lock().await; 367 | let svc = config::SqliteImpl { db: db_conn }; 368 | let res = svc 369 | .get_default_config() 370 | .await 371 | .into_sad_error("unable to get config")?; 372 | cache.set_config(res.clone()); 373 | return Ok(res); 374 | } 375 | 376 | #[tauri::command] 377 | async fn update_config(config: config::Config, app: tauri::State<'_, App>) -> Result<(), SADError> { 378 | let db_guard = &app.db.lock().await; 379 | let db_conn = db_guard.as_ref().into_sad_error("")?; 380 | 381 | let mut cache = app.cache.lock().await; 382 | let svc = config::SqliteImpl { db: db_conn }; 383 | let res = svc 384 | .update_default_config(config) 385 | .await 386 | .into_sad_error("unable to update config")?; 387 | cache.set_config(res); 388 | return Ok(()); 389 | } 390 | -------------------------------------------------------------------------------- /src-tauri/src/packages.rs: -------------------------------------------------------------------------------- 1 | use crate::adb_cmd::{self, ADBCommand, ADBShell}; 2 | use anyhow::{anyhow, Error, Result}; 3 | use lazy_static::lazy_static; 4 | use regex::Regex; 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::{HashMap, HashSet}; 7 | use std::{fmt::Display, str::FromStr}; 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 10 | pub enum PackageState { 11 | Enabled, 12 | Uninstalled, 13 | Disabled, 14 | Hidden, 15 | } 16 | 17 | impl Display for PackageState { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | match self { 20 | PackageState::Enabled => write!(f, "enabled"), 21 | PackageState::Uninstalled => write!(f, "uninstalled"), 22 | PackageState::Disabled => write!(f, "disabled"), 23 | PackageState::Hidden => write!(f, "hidden"), 24 | } 25 | } 26 | } 27 | 28 | impl FromStr for PackageState { 29 | type Err = Error; 30 | 31 | fn from_str(s: &str) -> Result { 32 | let lowercased = s.to_ascii_lowercase(); 33 | match lowercased.as_str() { 34 | "enabled" => Ok(Self::Enabled), 35 | "uninstalled" => Ok(Self::Uninstalled), 36 | "disabled" => Ok(Self::Disabled), 37 | "hidden" => Ok(Self::Hidden), 38 | _ => Err(anyhow!("unknown package state {}", lowercased)), 39 | } 40 | } 41 | } 42 | 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 44 | pub enum PackageType { 45 | System, 46 | ThirdParty, 47 | Unknown, 48 | } 49 | 50 | impl Display for PackageType { 51 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 52 | match self { 53 | PackageType::System => write!(f, "system"), 54 | PackageType::ThirdParty => write!(f, "thirdparty"), 55 | PackageType::Unknown => write!(f, "unknown"), 56 | } 57 | } 58 | } 59 | 60 | impl FromStr for PackageType { 61 | type Err = Error; 62 | 63 | fn from_str(s: &str) -> Result { 64 | let lowercased = s.to_ascii_lowercase(); 65 | match lowercased.as_str() { 66 | "system" => Ok(Self::System), 67 | "thirdparty" => Ok(Self::ThirdParty), 68 | "unknown" => Ok(Self::Unknown), 69 | _ => Err(anyhow!("unsupported package type {}", lowercased)), 70 | } 71 | } 72 | } 73 | 74 | #[derive(Serialize, Deserialize, Debug, Clone)] 75 | pub struct Package { 76 | pub name: String, 77 | state: PackageState, 78 | ptype: PackageType, 79 | package_prefix: String, 80 | } 81 | 82 | impl Package { 83 | pub fn set_state(&mut self, s: PackageState) { 84 | self.state = s; 85 | } 86 | } 87 | 88 | pub trait ListPackages { 89 | fn list_packages(&self, device_id: String, user_id: String) -> Result>; 90 | } 91 | 92 | pub trait DisablePackage { 93 | fn disable_package( 94 | &self, 95 | device_id: String, 96 | user_id: String, 97 | pkg: String, 98 | clear_pkg: bool, 99 | ) -> Result<()>; 100 | } 101 | 102 | pub trait EnablePackage { 103 | fn enable_package(&self, device_id: String, user_id: String, pkg: String) -> Result<()>; 104 | } 105 | 106 | const LIST_ALL_PACKAGES_INCLUDING_UNINSTALLED: &str = "pm list packages -u -f"; 107 | const LIST_SYSTEM_PACKAGES: &str = "pm list packages -u -s"; 108 | const LIST_THIRD_PARTY_PACKAGES: &str = "pm list packages -u -3"; 109 | const LIST_ENABLED_PACKAGES: &str = "pm list packages -e"; 110 | const LIST_DISABLED_PACKAGES: &str = "pm list packages -d"; 111 | const LIST_UNINSTALLED_DISABLED_PACKAGES: &str = "pm list packages -u -d"; 112 | 113 | pub struct ADBTerminalImpl 114 | where 115 | F: ADBCommand + ?Sized, // TODO: Factory with closures? 116 | { 117 | adb_command: F, 118 | } 119 | 120 | impl ADBTerminalImpl { 121 | pub fn new(adb_path: String) -> Self { 122 | Self { 123 | adb_command: ADBShell::new(adb_path), 124 | } 125 | } 126 | } 127 | 128 | struct PackageAttribs { 129 | package_path: String, 130 | } 131 | 132 | impl PackageAttribs { 133 | fn package_prefix(&self) -> String { 134 | let splits: Vec<&str> = self.package_path.splitn(4, "/").collect(); 135 | return splits[..3].join("/"); 136 | } 137 | } 138 | 139 | lazy_static! { 140 | static ref PACKAGE_PARSE_REGEX: Regex = Regex::new(r"(.*)\.apk\=(.*)").unwrap(); 141 | } 142 | 143 | impl ADBTerminalImpl { 144 | pub fn list_packages(&self, device_id: String, user_id: String) -> Result> { 145 | let shell_cmd = adb_cmd::for_device(&self.adb_command.clone(), device_id.to_owned()); 146 | 147 | let ( 148 | mut all_pkg, 149 | mut enabled_pkg, 150 | mut disabled_pkg, 151 | mut sys_pkg, 152 | mut tpp_pkg, 153 | mut uninstalled_disabled_pkg, 154 | ): ( 155 | HashMap, 156 | HashSet, 157 | HashSet, 158 | HashSet, 159 | HashSet, 160 | HashSet, 161 | ) = ( 162 | HashMap::new(), 163 | HashSet::new(), 164 | HashSet::new(), 165 | HashSet::new(), 166 | HashSet::new(), 167 | HashSet::new(), 168 | ); 169 | 170 | let ( 171 | cmd_enabled_pkg, 172 | cmd_disabled_pkg, 173 | cmd_system_pkg, 174 | cmd_tpp_pkg, 175 | cmd_uninstalled_disabled_pkg, 176 | ) = ( 177 | shell_cmd 178 | .clone() 179 | .args(&[LIST_ENABLED_PACKAGES, "--user", &user_id]), 180 | shell_cmd 181 | .clone() 182 | .args(&[LIST_DISABLED_PACKAGES, "--user", &user_id]), 183 | shell_cmd 184 | .clone() 185 | .args(&[LIST_SYSTEM_PACKAGES, "--user", &user_id]), 186 | shell_cmd 187 | .clone() 188 | .args(&[LIST_THIRD_PARTY_PACKAGES, "--user", &user_id]), 189 | shell_cmd 190 | .clone() 191 | .args(&[LIST_UNINSTALLED_DISABLED_PACKAGES, "--user", &user_id]), 192 | ); 193 | 194 | fn callback(container: &mut HashSet) -> impl FnMut(String) -> Result<()> + '_ { 195 | let parser = |s: String| -> Result<()> { 196 | let ot = s.replace("package:", ""); 197 | for l in ot.lines() { 198 | container.insert(l.to_string()); 199 | } 200 | return Ok(()); 201 | }; 202 | return parser; 203 | } 204 | 205 | let res = 206 | Self::execute_list_all_with_fallback(&shell_cmd, user_id.to_owned(), &mut all_pkg) 207 | .and_then(|_| { 208 | Self::_execute_and_parse(&cmd_enabled_pkg, callback(&mut enabled_pkg)) 209 | }) 210 | .and_then(|_| { 211 | Self::_execute_and_parse(&cmd_disabled_pkg, callback(&mut disabled_pkg)) 212 | }) 213 | .and_then(|_| Self::_execute_and_parse(&cmd_system_pkg, callback(&mut sys_pkg))) 214 | .and_then(|_| Self::_execute_and_parse(&cmd_tpp_pkg, callback(&mut tpp_pkg))) 215 | .and_then(|_| { 216 | Self::_execute_and_parse( 217 | &cmd_uninstalled_disabled_pkg, 218 | callback(&mut uninstalled_disabled_pkg), 219 | ) 220 | }); 221 | 222 | match res { 223 | Err(e) => { 224 | return Err(e.into()); 225 | } 226 | Ok(_) => {} 227 | } 228 | 229 | let mut pkgs: Vec = vec![]; 230 | 231 | for (pname, pattrib) in all_pkg.iter() { 232 | let mut pstate = PackageState::Hidden; 233 | 234 | if enabled_pkg.contains(pname) { 235 | pstate = PackageState::Enabled; 236 | } else if disabled_pkg.contains(pname) { 237 | pstate = PackageState::Disabled; 238 | } else if uninstalled_disabled_pkg.contains(pname) { 239 | pstate = PackageState::Uninstalled 240 | } 241 | 242 | let mut ptype: PackageType = PackageType::Unknown; 243 | if sys_pkg.contains(pname) { 244 | ptype = PackageType::System 245 | } else if tpp_pkg.contains(pname) { 246 | ptype = PackageType::ThirdParty 247 | } 248 | 249 | pkgs.push(Package { 250 | name: pname.to_string(), 251 | state: pstate, 252 | ptype: ptype, 253 | package_prefix: pattrib.package_prefix(), 254 | }) 255 | } 256 | 257 | pkgs.sort_by(|a, b| a.name.cmp(&b.name)); 258 | 259 | return Ok(pkgs); 260 | } 261 | 262 | pub fn disable_package( 263 | &self, 264 | device_id: String, 265 | user_id: String, 266 | pkg: String, 267 | clear_pkg: bool, 268 | ) -> Result<()> { 269 | let shell_cmd = adb_cmd::for_device(&self.adb_command.clone(), device_id.to_owned()); 270 | 271 | let (cmd_disable_pkg, cmd_fstop_pkg, cmd_clear_pkg) = ( 272 | shell_cmd 273 | .clone() 274 | .args(["pm disable-user", "--user", &user_id, &pkg.to_owned()]), 275 | shell_cmd 276 | .clone() 277 | .args(["am force-stop", "--user", &user_id, &pkg.to_owned()]), 278 | shell_cmd 279 | .clone() 280 | .args(&["pm clear", "--user", &user_id, &pkg.to_owned()]), 281 | ); 282 | 283 | Self::_execute_and_parse(&cmd_disable_pkg, |s| { 284 | if s.contains(&format!( 285 | "Package {} new state: disabled-user", 286 | pkg.to_owned() 287 | )) { 288 | return Ok(()); 289 | } 290 | return Err(anyhow!(s)); 291 | }) 292 | .and_then(|_| { 293 | Self::_execute_and_parse(&cmd_fstop_pkg, |s| { 294 | if s.is_empty() { 295 | return Ok(()); 296 | } 297 | return Err(anyhow!(s)); 298 | }) 299 | })?; 300 | 301 | if clear_pkg { 302 | Self::_execute_and_parse(&cmd_clear_pkg, |s| { 303 | if s.eq("Success") { 304 | return Ok(()); 305 | } 306 | return Err(anyhow!(s)); 307 | })? 308 | } 309 | 310 | return Ok(()); 311 | } 312 | 313 | pub fn enable_package(&self, device_id: String, user_id: String, pkg: String) -> Result<()> { 314 | let shell_cmd = adb_cmd::for_device(&self.adb_command.clone(), device_id.to_owned()); 315 | 316 | let cmd_enable_pkg = shell_cmd.args(["pm enable", "--user", &user_id, &pkg.to_owned()]); 317 | 318 | Self::_execute_and_parse(&cmd_enable_pkg, |s| { 319 | if s.contains(&format!("Package {} new state: enabled", pkg.to_owned())) { 320 | return Ok(()); 321 | } 322 | return Err(anyhow!(s)); 323 | })?; 324 | 325 | return Ok(()); 326 | } 327 | 328 | pub fn install_package(&self, device_id: String, user_id: String, pkg: String) -> Result<()> { 329 | let shell_cmd = adb_cmd::for_device(&self.adb_command.clone(), device_id.to_owned()); 330 | 331 | let cmd_enable_pkg = shell_cmd.args([ 332 | "cmd package install-existing", 333 | "--user", 334 | &user_id, 335 | &pkg.to_owned(), 336 | ]); 337 | 338 | Self::_execute_and_parse(&cmd_enable_pkg, |s| { 339 | if s.contains(&format!("Package {} new state: enabled", pkg.to_owned())) { 340 | return Ok(()); 341 | } 342 | return Err(anyhow!(s)); 343 | })?; 344 | 345 | return Ok(()); 346 | } 347 | 348 | fn execute_list_all_with_fallback<'a, T: ADBCommand + Clone>( 349 | cmd: &'a T, 350 | user_id: String, 351 | mut container: &mut HashMap, 352 | ) -> Result<()> { 353 | let (cmd_all_pkg, cmd_all_package_user0_fallback, cmd_all_package_user_fallback) = ( 354 | cmd.clone().args([LIST_ALL_PACKAGES_INCLUDING_UNINSTALLED]), 355 | cmd.clone() 356 | .args([LIST_ALL_PACKAGES_INCLUDING_UNINSTALLED, "--user", "0"]), 357 | cmd.clone() 358 | .args([LIST_ALL_PACKAGES_INCLUDING_UNINSTALLED, "--user", &user_id]), 359 | ); 360 | 361 | fn callback( 362 | container: &mut HashMap, 363 | ) -> impl FnMut(String) -> Result<()> + '_ { 364 | let parser = |s: String| -> Result<()> { 365 | let ot = s.replace("package:", ""); 366 | for l in ot.lines() { 367 | let caps = PACKAGE_PARSE_REGEX.captures(l).unwrap(); 368 | container.insert( 369 | caps.get(2).unwrap().as_str().to_string(), 370 | PackageAttribs { 371 | package_path: caps.get(1).unwrap().as_str().to_string(), 372 | }, 373 | ); 374 | } 375 | return Ok(()); 376 | }; 377 | return parser; 378 | } 379 | 380 | return Self::_execute_and_parse(&cmd_all_pkg, callback(&mut container)) 381 | .or_else(|e| { 382 | if !e 383 | .to_string() 384 | .contains("Shell does not have permission to access user") 385 | { 386 | return Err(e); 387 | } 388 | Self::_execute_and_parse(&cmd_all_package_user0_fallback, callback(&mut container)) 389 | }) 390 | .or_else(|e| { 391 | if !e 392 | .to_string() 393 | .contains("Shell does not have permission to access user") 394 | { 395 | return Err(e); 396 | } 397 | Self::_execute_and_parse(&cmd_all_package_user_fallback, callback(&mut container)) 398 | }); 399 | } 400 | 401 | fn _execute_and_parse<'a, T: ADBCommand>( 402 | cmd: &'a T, 403 | mut parser: impl FnMut(String) -> Result<()>, 404 | ) -> Result<()> { 405 | let res = cmd.execute(); 406 | match res { 407 | Err(e) => { 408 | return Err(e.into()); 409 | } 410 | Ok(o) => { 411 | return parser(o); 412 | } 413 | } 414 | } 415 | } 416 | --------------------------------------------------------------------------------