├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── assets
│ │ │ ├── instructions_magisk.md
│ │ │ ├── instructions_generic.md
│ │ │ ├── instructions_debug.md
│ │ │ └── instructions_replace.md
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── themes.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_notifications_black_24dp.xml
│ │ │ │ ├── ic_home_black_24dp.xml
│ │ │ │ ├── ic_dashboard_black_24dp.xml
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ └── menu
│ │ │ │ └── bottom_nav_menu.xml
│ │ ├── graphql
│ │ │ └── GetReleases.graphql
│ │ ├── java
│ │ │ └── io
│ │ │ │ └── mkg20001
│ │ │ │ └── nixosimage
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Color.kt
│ │ │ │ ├── install
│ │ │ │ │ ├── Install.kt
│ │ │ │ │ ├── InstallComposable.kt
│ │ │ │ │ └── InstallMagic.kt
│ │ │ │ ├── DropdownItem.kt
│ │ │ │ └── home
│ │ │ │ │ └── HomeComposable.kt
│ │ │ │ ├── install
│ │ │ │ ├── ImageInstallMethod.kt
│ │ │ │ ├── DebugInstallMethod.kt
│ │ │ │ ├── MagiskInstallMethod.kt
│ │ │ │ └── ReplaceInstallMethod.kt
│ │ │ │ ├── extra
│ │ │ │ └── ExtraImageUtils.kt
│ │ │ │ ├── data
│ │ │ │ ├── Utils.kt
│ │ │ │ ├── GithHubRelease.kt
│ │ │ │ └── ImageDownloader.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── io
│ │ │ └── mkg20001
│ │ │ └── nixosimage
│ │ │ └── ExampleUnitTest.kt
│ ├── debug
│ │ └── AndroidManifest.xml
│ └── androidTest
│ │ └── java
│ │ └── io
│ │ └── mkg20001
│ │ └── nixosimage
│ │ ├── ExampleInstrumentedTest.kt
│ │ └── FastlaneScreenshot.kt
├── proguard-rules.pro
└── build.gradle.kts
├── fastlane
├── metadata
│ └── android
│ │ ├── en-US
│ │ ├── video.txt
│ │ ├── title.txt
│ │ ├── short_description.txt
│ │ ├── full_description.txt
│ │ └── images
│ │ │ ├── phoneScreenshots
│ │ │ ├── loaded_1745001334513.png
│ │ │ └── installing_1745001337235.png
│ │ │ ├── tenInchScreenshots
│ │ │ ├── loaded_1745001172244.png
│ │ │ └── installing_1745001174589.png
│ │ │ └── sevenInchScreenshots
│ │ │ ├── loaded_1745000981524.png
│ │ │ └── installing_1745000983710.png
│ │ └── screenshots.html
├── Appfile
├── README.md
└── Fastfile
├── .idea
├── .name
├── .gitignore
├── compiler.xml
├── kotlinc.xml
├── graphql-settings.xml
├── AndroidProjectSystem.xml
├── migrations.xml
├── vcs.xml
├── markdown.xml
├── misc.xml
├── gradle.xml
├── runConfigurations.xml
├── runConfigurations
│ ├── Run.xml
│ └── app.xml
├── deploymentTargetSelector.xml
├── appInsightsSettings.xml
├── androidTestResultsUserPreferences.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── Gemfile
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── package.nix
├── DPA.md
├── assets
├── terminal-svgrepo-com.svg
├── convert_material_colors.js
├── gradient.svg
├── nix-snowflake-white.svg
└── merged-logo.svg
├── Cargo.toml
├── old
├── dashboard
│ ├── DashboardViewModel.kt
│ └── DashboardFragment.kt
├── notifications
│ ├── NotificationsViewModel.kt
│ └── NotificationsFragment.kt
├── activity_install.xml
├── layout
│ ├── fragment_dashboard.xml
│ └── fragment_notifications.xml
├── mobile_navigation.xml
├── MainActivity.kt
├── activity_main.xml
├── HomeViewModel.kt
├── fragment_home.xml
└── HomeFragment.kt
├── flake.lock
├── .gitignore
├── flake.nix
├── settings.gradle.kts
├── README.md
├── ui-draft.txt
├── gradle.properties
├── module.nix
├── gradlew.bat
├── src
└── main.rs
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/video.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | NixOS Image Installer for Android Terminal
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | NixOS Image for Terminal
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Install NixOS on the Android Terminal
--------------------------------------------------------------------------------
/app/src/main/assets/instructions_magisk.md:
--------------------------------------------------------------------------------
1 | - This installation method is untested and may not work
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nix-community/nixos-avf-image-app/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/assets/instructions_generic.md:
--------------------------------------------------------------------------------
1 | - If there are any Terminal startup errors, please try closing and opening Terminal again before resetting, as this often resolves the error.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | This app downloads and installs the NixOS AVF Image for the Android Terminal App
2 |
3 | This allows you to use NixOS in Android Terminal
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/package.nix:
--------------------------------------------------------------------------------
1 | { rustPlatform }:
2 |
3 | rustPlatform.buildRustPackage rec {
4 | name = "nixos-image-proxy-server";
5 |
6 | src = ./.;
7 |
8 | cargoLock = {
9 | lockFile = ./Cargo.lock;
10 | };
11 | }
--------------------------------------------------------------------------------
/.idea/graphql-settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/loaded_1745001334513.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nix-community/nixos-avf-image-app/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/loaded_1745001334513.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/tenInchScreenshots/loaded_1745001172244.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nix-community/nixos-avf-image-app/HEAD/fastlane/metadata/android/en-US/images/tenInchScreenshots/loaded_1745001172244.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/installing_1745001337235.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nix-community/nixos-avf-image-app/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/installing_1745001337235.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/sevenInchScreenshots/loaded_1745000981524.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nix-community/nixos-avf-image-app/HEAD/fastlane/metadata/android/en-US/images/sevenInchScreenshots/loaded_1745000981524.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/tenInchScreenshots/installing_1745001174589.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nix-community/nixos-avf-image-app/HEAD/fastlane/metadata/android/en-US/images/tenInchScreenshots/installing_1745001174589.png
--------------------------------------------------------------------------------
/app/src/main/assets/instructions_debug.md:
--------------------------------------------------------------------------------
1 | - If you already have a Terminal image installed, you may need to clean it up using "Settings (Gear Icon) > Recovery > Restore > Reset to initial version"
2 | - Auto-installation should start after that
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | json_key_file("/home/maciej/.private.d/fastlane-key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
2 | package_name("io.mkg20001.nixosimage") # e.g. com.krausefx.app
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/sevenInchScreenshots/installing_1745000983710.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nix-community/nixos-avf-image-app/HEAD/fastlane/metadata/android/en-US/images/sevenInchScreenshots/installing_1745000983710.png
--------------------------------------------------------------------------------
/DPA.md:
--------------------------------------------------------------------------------
1 | # Data Protection Agreement
2 |
3 | This app accesses services from GitHub Inc. which may store your IP Address in the server logs.
4 |
5 | GitHub Data Privacy Agreement: https://github.com/customer-terms/github-data-protection-agreement
6 |
--------------------------------------------------------------------------------
/.idea/AndroidProjectSystem.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Apr 06 03:19:24 GMT 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/markdown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/assets/terminal-svgrepo-com.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nixos-image-proxy-server"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | rocket = { version = "0.5.1", features = ["json"] }
8 | graphql_client = "0.14.0"
9 | serde = { version = "1.0", features = ["derive"] }
10 | reqwest = { version = "0.12.15", features = ["json", "rustls-tls"], default-features = false }
11 | twelf = { version = "0.15.0", features = ["yaml"] }
--------------------------------------------------------------------------------
/old/dashboard/DashboardViewModel.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.dashboard
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 |
7 | class DashboardViewModel : ViewModel() {
8 |
9 | private val _text = MutableLiveData().apply {
10 | value = "This is dashboard Fragment"
11 | }
12 | val text: LiveData = _text
13 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/old/notifications/NotificationsViewModel.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.notifications
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 |
7 | class NotificationsViewModel : ViewModel() {
8 |
9 | private val _text = MutableLiveData().apply {
10 | value = "This is notifications Fragment"
11 | }
12 | val text: LiveData = _text
13 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_notifications_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/graphql/GetReleases.graphql:
--------------------------------------------------------------------------------
1 | # Schema: https://docs.github.com/en/graphql/overview/public-schema
2 |
3 | query GetReleases {
4 | repository(owner: "nix-community", name: "nixos-avf") {
5 | releases(first: 20) {
6 | nodes {
7 | tagName
8 |
9 | releaseAssets(first: 20) {
10 | nodes {
11 | id
12 | name
13 | url
14 | updatedAt
15 | digest
16 | }
17 | }
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/assets/instructions_replace.md:
--------------------------------------------------------------------------------
1 | - If you didn't install the debian Terminal image, please click Install once Terminal opens to install the Debian Image
2 | - You will need to run this command in Terminal:
3 | - `bash /mnt/shared/image/replace.sh`
4 | - After that the Terminal will close.
5 | - Re-open Terminal
6 | - The script should run automatically this time.
7 | - It will take longer. After it finishes, Terminal should close.
8 | - Open Terminal again. You should have NixOS installed now (green prompt).
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_home_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "nixpkgs": {
4 | "locked": {
5 | "lastModified": 1763966396,
6 | "narHash": "sha256-6eeL1YPcY1MV3DDStIDIdy/zZCDKgHdkCmsrLJFiZf0=",
7 | "owner": "nixos",
8 | "repo": "nixpkgs",
9 | "rev": "5ae3b07d8d6527c42f17c876e404993199144b6a",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "nixos",
14 | "ref": "nixos-unstable",
15 | "repo": "nixpkgs",
16 | "type": "github"
17 | }
18 | },
19 | "root": {
20 | "inputs": {
21 | "nixpkgs": "nixpkgs"
22 | }
23 | }
24 | },
25 | "root": "root",
26 | "version": 7
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/test/java/io/mkg20001/nixosimage/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage
2 |
3 | import io.mkg20001.nixosimage.data.GitHubReleaseClient
4 | import kotlinx.coroutines.test.runTest
5 | import org.junit.Test
6 |
7 | import org.junit.Assert.*
8 |
9 | /**
10 | * Example local unit test, which will execute on the development machine (host).
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | class ExampleUnitTest {
15 | @Test
16 | fun addition_isCorrect() {
17 | assertEquals(4, 2 + 2)
18 | }
19 |
20 | @Test
21 | fun fetch_releases() = runTest {
22 | assert(GitHubReleaseClient.getReleases()?.isNotEmpty() == true)
23 | }
24 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | /app/src/main/java/io/mkg20001/nixosimage/data/Auth.kt
17 | /app/release
18 | .kotlin
19 |
20 |
21 | # Added by cargo
22 |
23 | /target
24 | config.yaml
25 | result
26 | wip
27 |
28 | # fastlane specific
29 | **/fastlane/report.xml
30 |
31 | # deliver temporary files
32 | **/fastlane/Preview.html
33 |
34 | # snapshot generated screenshots
35 | **/fastlane/screenshots
36 |
37 | # scan temporary files
38 | **/fastlane/test_output
39 |
40 | # Sentry Config File
41 | sentry.properties
42 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "A very basic flake";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
6 | };
7 |
8 | outputs = { self, nixpkgs }:
9 |
10 | let
11 | supportedSystems = [ "x86_64-linux" ];
12 | forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
13 | in
14 |
15 | {
16 | overlays.default = final: prev: {
17 | nixos-image-proxy-server = prev.callPackage ./package.nix { };
18 | };
19 |
20 | defaultPackage = forAllSystems (system: (import nixpkgs {
21 | inherit system;
22 | overlays = [ self.overlays.default ];
23 | }).nixos-image-proxy-server);
24 |
25 | nixosModules.nixos-image-proxy-server = import ./module.nix;
26 |
27 | };
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | maven {
13 | url = uri("https://jitpack.io")
14 | }
15 | }
16 | }
17 | dependencyResolutionManagement {
18 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
19 | repositories {
20 | google()
21 | mavenCentral()
22 | maven {
23 | url = uri("https://jitpack.io")
24 | }
25 | }
26 | }
27 |
28 | rootProject.name = "NixOS Image Installer for Android Terminal"
29 | include(":app")
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nixos-avf-image-app
2 |
3 | Companion app for [nixos avf image](https://github.com/nix-community/nixos-avf)
4 |
5 | Contains:
6 | - Rust proxy server for github graphql including nix flake+module
7 | - Android app source code for nixos image installer
8 |
9 | # Supports
10 |
11 | - Installation on debug builds (places file in /sdcard/linux/images.tar.gt)
12 | - Installation on prod builds using replace script (extracts image to /sdcard/Download/image and adds bash script to be run in VM for replacing the partitions)
13 |
14 | > [!IMPORTANT]
15 | > The image only works on Android 16+ and on Android 15 flavours that have the Android 16 Terminal patches backported (example: GrapheneOS)
16 |
17 | # Notes for dev on nixos
18 |
19 | fastlane must be run in android studio's terminal
20 | (otherwise it may fail to run aapt2 etc due to dynamic linking)
21 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 | -dontobfuscate
23 | -dontoptimize
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.dynamicDarkColorScheme
6 | import androidx.compose.material3.dynamicLightColorScheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.platform.LocalContext
9 |
10 | @Composable
11 | fun NixosImageTheme(
12 | darkTheme: Boolean = isSystemInDarkTheme(),
13 | content: @Composable () -> Unit
14 | ) {
15 | val colorScheme = when {
16 | darkTheme -> dynamicDarkColorScheme(LocalContext.current)
17 | else -> dynamicLightColorScheme(LocalContext.current)
18 | }
19 |
20 | MaterialTheme(
21 | colorScheme = colorScheme,
22 | typography = Typography,
23 | content = content
24 | )
25 | }
--------------------------------------------------------------------------------
/ui-draft.txt:
--------------------------------------------------------------------------------
1 | nixos image app:
2 |
3 | NixOS Image Installer will help you download and install the NixOS Image for the Android Terminal app
4 |
5 | Cat: Terminal App
6 |
7 | [ btn enable dev settings ]
8 |
9 | [ btn enable terminal app ]
10 |
11 | Cat: Download
12 |
13 | [ drop down select nixos version ]
14 |
15 | [ drop down select install method ]
16 |
17 | [ install button ]
18 |
19 |
20 | --------
21 |
22 | Nav:
23 | Home -> Install
24 | Extra -> Clear up VM data (root), Show VM logs (root)
25 | About -> Licenses, Info
26 |
27 | ---
28 |
29 |
30 | when press install button
31 |
32 | open activity via intent
33 | with version and arch
34 |
35 | activity:
36 | ----
37 |
38 | Task: Downloading NixOS image
39 |
40 | Version:
41 |
42 | Architecture:
43 |
44 | [ progress bar ]
45 |
46 | when done downloading install then open terminal
47 |
48 | ---
49 |
--------------------------------------------------------------------------------
/assets/convert_material_colors.js:
--------------------------------------------------------------------------------
1 | fs=require('fs')
2 |
3 | c=fs.readFileSync('colors.xml')
4 | s=c.toString().split('\n').filter(l => l.indexOf('name="material')!==-1).map(l => l.trim())
5 |
6 | function convertColorXmlToKotlin(xmlString) {
7 | const regex = /#([0-9A-Fa-f]+)<\/color>/;
8 | const match = xmlString.match(regex);
9 |
10 | if (!match) return null;
11 |
12 | const name = match[1];
13 | const hex = match[2];
14 |
15 | // Convert name like "material_deep_orange_500" to "DeepOrange500"
16 | const nameParts = name.split('_').slice(1); // remove "material"
17 | const kotlinName = nameParts
18 | .map((part, i) => i === nameParts.length - 1 ? part : part.charAt(0).toUpperCase() + part.slice(1))
19 | .join('');
20 |
21 | return `val ${kotlinName} = Color(0x${hex.toUpperCase()}FF)`;
22 | }
23 |
24 | d=s.map(convertColorXmlToKotlin)
25 | console.log(d)
26 | fs.writeFileSync('/tmp/kotlin', d.join('\n'))
27 |
--------------------------------------------------------------------------------
/old/activity_install.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
20 |
21 |
27 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/old/layout/fragment_dashboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
22 |
--------------------------------------------------------------------------------
/old/layout/fragment_notifications.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
22 |
--------------------------------------------------------------------------------
/old/mobile_navigation.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
19 |
20 |
25 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ----
3 |
4 | # Installation
5 |
6 | Make sure you have the latest version of the Xcode command line tools installed:
7 |
8 | ```sh
9 | xcode-select --install
10 | ```
11 |
12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
13 |
14 | # Available Actions
15 |
16 | ## Android
17 |
18 | ### android test
19 |
20 | ```sh
21 | [bundle exec] fastlane android test
22 | ```
23 |
24 | Runs all the tests
25 |
26 | ### android beta
27 |
28 | ```sh
29 | [bundle exec] fastlane android beta
30 | ```
31 |
32 | Submit a new Beta Build to Crashlytics Beta
33 |
34 | ### android deploy
35 |
36 | ```sh
37 | [bundle exec] fastlane android deploy
38 | ```
39 |
40 | Deploy a new version to the Google Play
41 |
42 | ### android capture_screen
43 |
44 | ```sh
45 | [bundle exec] fastlane android capture_screen
46 | ```
47 |
48 | Capture Screen
49 |
50 | ----
51 |
52 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
53 |
54 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
55 |
56 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
57 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_dashboard_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/install/ImageInstallMethod.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.install
2 |
3 | import android.content.Context
4 | import android.content.res.AssetManager
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import java.io.File
7 |
8 | interface ImageInstallMethod {
9 | val id: String
10 | val display: Int
11 |
12 | fun isAvailable(): Boolean
13 | suspend fun installImage (
14 | context: Context,
15 | image: File,
16 | assets: AssetManager,
17 | progress: MutableStateFlow
18 | ): Boolean
19 | val needsCleanup: Boolean
20 | get() = false
21 | val needsExternalStorage: Boolean
22 | get() = true
23 | val needsLaunchTerminalAfterwards: Boolean
24 | get() = true
25 | val needsImageClean: Boolean
26 | get() = true
27 | val showOpenTerminalAgainBtn: Boolean
28 | get() = true
29 |
30 | fun doCleanup() {
31 | throw RuntimeException("not applicable")
32 | }
33 | }
34 |
35 | object InstallMethods {
36 | val methods = listOf(DebugInstallMethod, MagiskInstallMethod, ReplaceInstallMethod)
37 | fun availableMethods(): List {
38 | return methods.filter { it.isAvailable() }
39 | }
40 | fun getMethod(id: String): ImageInstallMethod? {
41 | return methods.filter { it.id == id }.getOrNull(0)
42 | }
43 | }
--------------------------------------------------------------------------------
/old/dashboard/DashboardFragment.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.dashboard
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.TextView
8 | import androidx.fragment.app.Fragment
9 | import androidx.lifecycle.ViewModelProvider
10 | import io.mkg20001.nixosimage.databinding.FragmentDashboardBinding
11 |
12 | class DashboardFragment : Fragment() {
13 |
14 | private var _binding: FragmentDashboardBinding? = null
15 |
16 | // This property is only valid between onCreateView and
17 | // onDestroyView.
18 | private val binding get() = _binding!!
19 |
20 | override fun onCreateView(
21 | inflater: LayoutInflater,
22 | container: ViewGroup?,
23 | savedInstanceState: Bundle?
24 | ): View {
25 | val dashboardViewModel =
26 | ViewModelProvider(this).get(DashboardViewModel::class.java)
27 |
28 | _binding = FragmentDashboardBinding.inflate(inflater, container, false)
29 | val root: View = binding.root
30 |
31 | val textView: TextView = binding.textDashboard
32 | dashboardViewModel.text.observe(viewLifecycleOwner) {
33 | textView.text = it
34 | }
35 | return root
36 | }
37 |
38 | override fun onDestroyView() {
39 | super.onDestroyView()
40 | _binding = null
41 | }
42 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/extra/ExtraImageUtils.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.extra
2 |
3 | import android.util.Log
4 | import com.topjohnwu.superuser.Shell
5 | import io.sentry.Sentry
6 |
7 | class ExtraImageUtils() {
8 | companion object {
9 | val RM_EXISTING = "rm -rfv /data/data/com.android.virtualization.terminal/{files/nixos.log,files/debian.log,files/linux,vm/nixos,vm/debian}"
10 | val LAUNCH_INSTALLER = "am start -n com.android.virtualization.terminal/.InstallerActivity"
11 | }
12 |
13 | val shell = Shell.getShell()
14 | var hasRoot = shell.isRoot
15 |
16 | fun executeWithRoot(cmd: String): Boolean {
17 | if (!hasRoot) return false
18 |
19 | try {
20 | Log.w("Magisk", "execute with su: ${cmd}")
21 | val process = Shell.cmd(cmd).exec()
22 | Log.w("Magisk", " => res ${process.code}")
23 | process.err.forEach {
24 | Log.w("Magisk", " => err ${it}")
25 | }
26 | return process.code == 0
27 | } catch (e: InterruptedException) {
28 | e.printStackTrace()
29 | Sentry.captureException(e)
30 | return false
31 | }
32 | }
33 |
34 | fun cleanupImage(): Boolean {
35 | return executeWithRoot(RM_EXISTING)
36 | }
37 |
38 | fun launchInstaller(): Boolean {
39 | return executeWithRoot(LAUNCH_INSTALLER)
40 | }
41 | }
--------------------------------------------------------------------------------
/old/notifications/NotificationsFragment.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.notifications
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.TextView
8 | import androidx.fragment.app.Fragment
9 | import androidx.lifecycle.ViewModelProvider
10 | import io.mkg20001.nixosimage.databinding.FragmentNotificationsBinding
11 |
12 | class NotificationsFragment : Fragment() {
13 |
14 | private var _binding: FragmentNotificationsBinding? = null
15 |
16 | // This property is only valid between onCreateView and
17 | // onDestroyView.
18 | private val binding get() = _binding!!
19 |
20 | override fun onCreateView(
21 | inflater: LayoutInflater,
22 | container: ViewGroup?,
23 | savedInstanceState: Bundle?
24 | ): View {
25 | val notificationsViewModel =
26 | ViewModelProvider(this).get(NotificationsViewModel::class.java)
27 |
28 | _binding = FragmentNotificationsBinding.inflate(inflater, container, false)
29 | val root: View = binding.root
30 |
31 | val textView: TextView = binding.textNotifications
32 | notificationsViewModel.text.observe(viewLifecycleOwner) {
33 | textView.text = it
34 | }
35 | return root
36 | }
37 |
38 | override fun onDestroyView() {
39 | super.onDestroyView()
40 | _binding = null
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # This file contains the fastlane.tools configuration
2 | # You can find the documentation at https://docs.fastlane.tools
3 | #
4 | # For a list of all available actions, check out
5 | #
6 | # https://docs.fastlane.tools/actions
7 | #
8 | # For a list of all available plugins, check out
9 | #
10 | # https://docs.fastlane.tools/plugins/available-plugins
11 | #
12 |
13 | # Uncomment the line if you want fastlane to automatically update itself
14 | # update_fastlane
15 |
16 | default_platform(:android)
17 |
18 | platform :android do
19 | desc "Runs all the tests"
20 | lane :test do
21 | gradle(task: "test")
22 | end
23 |
24 | desc "Submit a new Beta Build to Crashlytics Beta"
25 | lane :beta do
26 | gradle(task: "clean assembleRelease")
27 | crashlytics
28 |
29 | # sh "your_script.sh"
30 | # You can also use other beta testing services here
31 | end
32 |
33 | desc "Deploy a new version to the Google Play"
34 | lane :deploy do
35 | gradle(task: "clean assembleRelease")
36 | upload_to_play_store
37 | end
38 |
39 | desc "Capture Screen"
40 | lane :capture_screen do
41 | gradle(task: "clean assembleDebug assembleAndroidTest")
42 | screengrab(clear_previous_screenshots: true, tests_apk_path: "app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk", app_apk_path: "app/build/outputs/apk/debug/app-debug.apk", reinstall_app: true, use_tests_in_classes: "io.mkg20001.nixosimage.FastlaneScreenshot")
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/old/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.navigation.findNavController
6 | import androidx.navigation.ui.AppBarConfiguration
7 | import androidx.navigation.ui.setupActionBarWithNavController
8 | import androidx.navigation.ui.setupWithNavController
9 | import com.google.android.material.bottomnavigation.BottomNavigationView
10 | import io.mkg20001.nixosimage.databinding.ActivityMainBinding
11 | import io.mkg20001.nixosimage.install.initShell
12 |
13 |
14 | class MainActivity : AppCompatActivity() {
15 |
16 | private lateinit var binding: ActivityMainBinding
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 |
21 | initShell()
22 |
23 | binding = ActivityMainBinding.inflate(layoutInflater)
24 | setContentView(binding.root)
25 |
26 | val navView: BottomNavigationView = binding.navView
27 |
28 | val navController = findNavController(R.id.nav_host_fragment_activity_main)
29 | // Passing each menu ID as a set of Ids because each
30 | // menu should be considered as top level destinations.
31 | val appBarConfiguration = AppBarConfiguration(
32 | setOf(
33 | R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
34 | )
35 | )
36 | setupActionBarWithNavController(navController, appBarConfiguration)
37 | navView.setupWithNavController(navController)
38 | }
39 | }
--------------------------------------------------------------------------------
/old/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
18 |
19 |
21 |
24 |
25 |
28 |
29 |
32 |
33 |
34 |
37 |
--------------------------------------------------------------------------------
/.idea/appInsightsSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/module.nix:
--------------------------------------------------------------------------------
1 | { config, lib, pkgs, ... }:
2 |
3 | with lib;
4 |
5 | let
6 | cfg = config.services.nixos-image-proxy-server;
7 | nixos-image-proxy-server = pkgs.nixos-image-proxy-server;
8 | format = pkgs.formats.json {};
9 | in
10 | {
11 | options = {
12 | services.nixos-image-proxy-server = {
13 | enable = mkEnableOption "nixos-image-proxy-server";
14 |
15 | /* port = mkOption {
16 | description = "Port to listen at";
17 | type = types.int;
18 | default = 3333;
19 | };
20 |
21 | token = mkOption {
22 | description = "GitHub Token";
23 | type = types.str;
24 | }; */
25 |
26 | config = mkOption {
27 | description = "configuration";
28 | type = format.type;
29 | };
30 |
31 | openFirewall = mkOption {
32 | type = types.bool;
33 | default = false;
34 | description = "Open ports in the firewall for nixos-image-proxy-server.";
35 | };
36 | };
37 | };
38 |
39 | config = mkIf (cfg.enable) {
40 | networking.firewall = mkIf cfg.openFirewall {
41 | allowedTCPPorts = [ cfg.config.port ];
42 | };
43 |
44 | systemd.services.nixos-image-proxy-server = with pkgs; {
45 | wantedBy = [ "multi-user.target" ];
46 | after = [ "network.target" ];
47 | requires = [ "network-online.target" ];
48 |
49 | description = "nixos-image-proxy-server";
50 |
51 | environment.CONFIG = with builtins; format.generate "config.json" cfg.config;
52 |
53 | serviceConfig = {
54 | Type = "simple";
55 | DynamicUser = true;
56 | User = "nixos-image";
57 | ExecStart = "${nixos-image-proxy-server}/bin/nixos-image-proxy-server";
58 | };
59 | };
60 | };
61 | }
62 |
--------------------------------------------------------------------------------
/.idea/androidTestResultsUserPreferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/old/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.home
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import io.mkg20001.nixosimage.data.GitHubReleaseAsset
7 | import io.mkg20001.nixosimage.data.GitHubReleaseClient
8 | import io.mkg20001.nixosimage.install.ImageInstallMethod
9 | import io.mkg20001.nixosimage.install.InstallMethods
10 | import io.mkg20001.nixosimage.BuildConfig
11 |
12 | class HomeViewModel : ViewModel() {
13 |
14 | private val _state = MutableLiveData().apply {
15 | value = ImageViewState.LOADING
16 | }
17 | val state: LiveData = _state
18 |
19 | private val _installMethods = MutableLiveData>().apply {
20 | value = listOf()
21 | }
22 | val installMethods: LiveData> = _installMethods
23 |
24 | private val _imageReleases = MutableLiveData>().apply {
25 | value = listOf()
26 | }
27 | val imageRelease: LiveData> = _imageReleases
28 |
29 | suspend fun refresh() {
30 | _state.postValue(ImageViewState.LOADING)
31 |
32 | try {
33 | if (BuildConfig.ALLOW_ANY_METHOD) {
34 | _installMethods.postValue(InstallMethods.methods)
35 | } else {
36 | _installMethods.postValue(InstallMethods.availableMethods())
37 | }
38 | val rel = GitHubReleaseClient.getReleases()
39 | if (rel == null) {
40 | _state.postValue(ImageViewState.ERROR)
41 | return
42 | }
43 | _imageReleases.postValue(rel.map { it.getSupported() }.flatten())
44 |
45 | _state.postValue(ImageViewState.READY)
46 | } catch(e: Exception) {
47 | e.printStackTrace()
48 | _state.postValue(ImageViewState.ERROR)
49 | }
50 |
51 | }
52 | }
53 |
54 | enum class ImageViewState {
55 | LOADING,
56 | ERROR,
57 | READY
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/install/DebugInstallMethod.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.install
2 |
3 | import android.content.Context
4 | import android.content.res.AssetManager
5 | import android.os.Build
6 | import android.os.Environment
7 | import android.util.Log
8 | import io.mkg20001.nixosimage.R
9 | import io.mkg20001.nixosimage.data.copyFile
10 | import io.mkg20001.nixosimage.data.mkdirp
11 | import io.sentry.Sentry
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.flow.MutableStateFlow
14 | import kotlinx.coroutines.withContext
15 | import okio.IOException
16 | import java.io.File
17 | import java.nio.file.Path
18 |
19 | object DebugInstallMethod: ImageInstallMethod {
20 | override val id = "debug"
21 |
22 | override val display = R.string.method_debug
23 |
24 | override fun isAvailable(): Boolean {
25 | return Build.TYPE != null && (Build.TYPE.equals("userdebug") || Build.TYPE.equals("eng"))
26 | }
27 |
28 | // mostly verbatim from packages/modules/Virtualization/ android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
29 | private const val DIR_IN_SDCARD = "linux"
30 | private const val ARCHIVE_NAME = "images.tar.gz"
31 | fun getSdcardPathForTesting(): Path {
32 | return Environment.getExternalStoragePublicDirectory(DIR_IN_SDCARD).toPath()
33 | }
34 | fun fromSdCard(): Path {
35 | return getSdcardPathForTesting().resolve(ARCHIVE_NAME)
36 | }
37 |
38 | override suspend fun installImage(
39 | context: Context,
40 | image: File,
41 | assets: AssetManager,
42 | progress: MutableStateFlow
43 | ): Boolean {
44 | try {
45 | if (!mkdirp(getSdcardPathForTesting().toAbsolutePath().toString())) {
46 | return false
47 | }
48 | withContext(Dispatchers.IO) {
49 | copyFile(image, fromSdCard().toFile()) {
50 | if (progress.value != it) {
51 | Log.d("DebugInstallMethod", "Copy progress $it%")
52 | progress.tryEmit(it)
53 | }
54 | }
55 | }
56 | } catch (e: IOException) {
57 | e.printStackTrace()
58 | Sentry.captureException(e)
59 | return false
60 | }
61 |
62 | return true
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | NixOS Image Installer for Android Terminal
3 | Install Image
4 | Extra
5 | About/Licenses
6 |
7 | NixOS Image Installer will help you download and install the NixOS Image for the Android Terminal app
8 | Loading NixOS AVF Image versions…
9 | Failed to fetch NixOS AVF Image versions!
10 |
11 | Task:
12 | Version:
13 | Architecture:
14 | Downloading NixOS image
15 | Installing NixOS image
16 |
17 | Method
18 | Version
19 |
20 | Install using Magisk (untested)
21 | Install using Terminal Debug mode
22 | Install using replace script
23 | [No install methods found]
24 | Install
25 | Refresh
26 | (loading)
27 | [No compatible images found]
28 | NixOS Image for Terminal
29 | Error during load of Intent!
30 | %1$s (built %2$s, %3$s)
31 | Open Terminal Again
32 |
33 | Please grant external storage for install and click "Install" again
34 | Run in Terminal: bash /mnt/shared/image/replace.sh
35 |
36 | You may need to remove the existing installation using Settings > Recovery > Remove existing data
37 |
38 | Failed to install image!
39 | Failed to download image! Check your network connection or go back and use \"Refresh\" button!
40 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/old/fragment_home.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
28 |
29 |
38 |
39 |
46 |
47 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/ui/install/Install.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.install
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import android.view.WindowManager
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.enableEdgeToEdge
9 | import androidx.compose.foundation.layout.fillMaxHeight
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.Text
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.unit.dp
17 | import androidx.lifecycle.lifecycleScope
18 | import io.mkg20001.nixosimage.R
19 | import io.mkg20001.nixosimage.data.GitHubReleaseAsset
20 | import io.mkg20001.nixosimage.install.InstallMethods
21 | import io.mkg20001.nixosimage.ui.theme.Red400
22 | import kotlinx.coroutines.Dispatchers
23 | import kotlinx.coroutines.launch
24 | import kotlinx.coroutines.withContext
25 |
26 |
27 | class Install : ComponentActivity() {
28 | var magic: InstallMagic? = null
29 |
30 | @OptIn(ExperimentalMaterial3Api::class)
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 | enableEdgeToEdge()
34 |
35 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
36 |
37 | val b = intent.extras ?: return errorOut()
38 |
39 | val asset = b.getSerializable("image", GitHubReleaseAsset::class.java)
40 | ?: return errorOut()
41 |
42 | val m: String = b.getString("method")
43 | ?: return errorOut()
44 | val method = InstallMethods.getMethod(m)
45 | ?: return errorOut()
46 |
47 | magic = InstallMagic(applicationContext, method, asset)
48 |
49 | Log.w("Install", "launch activity composable")
50 | setContent {
51 | InstallView {
52 | InstallComposable(magic!!)
53 | }
54 | }
55 |
56 | lifecycleScope.launch {
57 | bg()
58 | }
59 | }
60 |
61 | suspend fun bg() {
62 | withContext(Dispatchers.IO) {
63 | magic!!.run()
64 | }
65 | }
66 |
67 | fun errorOut() {
68 | setContent {
69 | InstallView {
70 | Text(
71 | stringResource(R.string.toast_intent_error),
72 | color = Red400,
73 | modifier = Modifier.padding(24.dp).fillMaxWidth().fillMaxHeight()
74 | )
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/data/Utils.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.data
2 |
3 | import android.util.Log
4 | import java.io.File
5 | import java.io.FileInputStream
6 | import java.io.FileOutputStream
7 | import java.io.FilterInputStream
8 | import java.io.InputStream
9 | import java.security.MessageDigest
10 |
11 | fun copyFile(source: File, dest: File, onProgress: (Int) -> Unit) {
12 | ProgressStream(FileInputStream(source), source.length().toDouble(), 0, onProgress).use { input ->
13 | FileOutputStream(dest).use { output ->
14 | input.copyTo(output)
15 | }
16 | }
17 | }
18 |
19 | fun mkdirp(path: String): Boolean {
20 | val dir = File(path)
21 | return if (!dir.exists()) {
22 | dir.mkdirs() // returns true if the directories were created
23 | } else {
24 | dir.isDirectory // true if it already exists and is a directory
25 | }
26 | }
27 |
28 | class ProgressStream(val baseStream: InputStream, val totalSize: Double, var bytesRead: Long, val onProgress: (Int) -> Unit): FilterInputStream(baseStream) {
29 | override fun read(): Int = super.read().also {
30 | if (it != -1) updateProgress(1)
31 | }
32 |
33 | override fun read(b: ByteArray, off: Int, len: Int): Int = super.read(b, off, len).also {
34 | if (it > 0) updateProgress(it)
35 | }
36 |
37 | fun updateProgress(count: Int) {
38 | bytesRead += count
39 | onProgress(((bytesRead / totalSize) * 100).toInt().coerceIn(0, 100))
40 | }
41 | }
42 |
43 | fun hexToByteArray(hex: String): ByteArray {
44 | val cleanHex = hex.replace(" ", "")
45 | require(cleanHex.length % 2 == 0) { "Hex string must have even length" }
46 |
47 | return ByteArray(cleanHex.length / 2) { i ->
48 | cleanHex.substring(i * 2, i * 2 + 2).toInt(16).toByte()
49 | }
50 | }
51 |
52 | class DigestStream(val baseStream: InputStream, val digest: MessageDigest): FilterInputStream(baseStream) {
53 | override fun read(): Int = super.read().also {
54 | // Done
55 | }
56 |
57 | override fun read(b: ByteArray, off: Int, len: Int): Int = super.read(b, off, len).also {
58 | // Hash bytes
59 | digest.update(b)
60 | }
61 |
62 | fun validate(expectedHex: String): Boolean {
63 | val actual = digest.digest()
64 | return actual contentEquals hexToByteArray(expectedHex)
65 | }
66 | }
67 |
68 | fun clearOldFiles(dir: File) {
69 | val cutoff = System.currentTimeMillis() - 24 * 60 * 60 * 1000 // 1 day in milliseconds
70 |
71 | dir.listFiles()?.forEach { file ->
72 | if (file.isFile && file.lastModified() < cutoff) {
73 | Log.w("ClearCache", "Clearing old cache: $file")
74 | file.delete()
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Scaffold
12 | import androidx.compose.material3.Text
13 | import androidx.compose.material3.TopAppBar
14 | import androidx.compose.material3.TopAppBarDefaults
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.navigation.compose.NavHost
19 | import androidx.navigation.compose.composable
20 | import androidx.navigation.compose.rememberNavController
21 | import io.mkg20001.nixosimage.data.clearOldFiles
22 | import io.mkg20001.nixosimage.ui.home.HomeComposable
23 | import io.mkg20001.nixosimage.ui.theme.NixosImageTheme
24 |
25 | @Composable
26 | fun AppNavGraph(startDestination: String = "home") {
27 | val navController = rememberNavController()
28 |
29 | NavHost(navController = navController, startDestination = startDestination) {
30 | composable("home") {
31 | HomeComposable()
32 | }
33 | // composable("other") { OtherScreen() }
34 | }
35 | }
36 |
37 | class MainActivity : ComponentActivity() {
38 | override fun onCreate(savedInstanceState: Bundle?) {
39 | super.onCreate(savedInstanceState)
40 |
41 | clearOldFiles(applicationContext.cacheDir)
42 |
43 | setContent {
44 | MainView {
45 | AppNavGraph()
46 | }
47 | }
48 | }
49 | }
50 |
51 | @OptIn(ExperimentalMaterial3Api::class)
52 | @Composable
53 | fun MainView(content: @Composable () -> Unit) {
54 | NixosImageTheme {
55 | Scaffold(
56 | modifier = Modifier.fillMaxSize(),
57 | topBar = {
58 | TopAppBar(
59 | colors = TopAppBarDefaults.topAppBarColors(
60 | containerColor = MaterialTheme.colorScheme.primaryContainer,
61 | titleContentColor = MaterialTheme.colorScheme.primary,
62 | ),
63 | title = {
64 | Text(stringResource(R.string.short_name))
65 | }
66 | )
67 | },
68 | ) { innerPadding ->
69 | Column(modifier = Modifier.padding(innerPadding)) {
70 | content()
71 | }
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/assets/gradient.svg:
--------------------------------------------------------------------------------
1 |
2 |
98 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/mkg20001/nixosimage/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage
2 |
3 | import androidx.compose.ui.test.isDisplayed
4 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
5 | import androidx.compose.ui.test.onNodeWithTag
6 | import androidx.compose.ui.test.performClick
7 | import androidx.test.platform.app.InstrumentationRegistry
8 | import androidx.test.ext.junit.runners.AndroidJUnit4
9 |
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 |
13 | import org.junit.Assert.*
14 | import org.junit.BeforeClass
15 | import org.junit.Rule
16 |
17 | /**
18 | * Instrumented test, which will execute on an Android device.
19 | *
20 | * See [testing documentation](http://d.android.com/tools/testing).
21 | */
22 | @RunWith(AndroidJUnit4::class)
23 | class ExampleInstrumentedTest {
24 | companion object {
25 | @BeforeClass @JvmStatic
26 | fun before() {
27 | val instrumentation = InstrumentationRegistry.getInstrumentation()
28 | instrumentation.uiAutomation.executeShellCommand(
29 | "appops set ${BuildConfig.APPLICATION_ID} MANAGE_EXTERNAL_STORAGE allow"
30 | ).close()
31 | }
32 | }
33 |
34 |
35 | @Test
36 | fun useAppContext() {
37 | // Context of the app under test.
38 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
39 | assertEquals("io.mkg20001.nixosimage", appContext.packageName)
40 | }
41 |
42 | @get:Rule
43 | val composeTestRule = createAndroidComposeRule()
44 |
45 | @Test
46 | fun testInstallButton() {
47 | composeTestRule.waitUntil(6000) {
48 | // Replace with your condition, e.g., UI element becomes visible after async event
49 | composeTestRule.onNodeWithTag("loaded_ui").isDisplayed()
50 | }
51 |
52 | composeTestRule.onNodeWithTag("install").performClick()
53 |
54 | Thread.sleep(2000)
55 |
56 | composeTestRule.waitUntil(6000) {
57 | // Replace with your condition, e.g., UI element becomes visible after async event
58 | composeTestRule.onNodeWithTag("install_ui").isDisplayed()
59 | }
60 | }
61 |
62 | @Test
63 | fun testRefresh() {
64 | composeTestRule.waitUntil(6000) {
65 | // Replace with your condition, e.g., UI element becomes visible after async event
66 | composeTestRule.onNodeWithTag("loaded_ui").isDisplayed()
67 | }
68 |
69 | composeTestRule.onNodeWithTag("refresh").performClick()
70 |
71 | composeTestRule.waitUntil(2000) {
72 | // Replace with your condition, e.g., UI element becomes visible after async event
73 | composeTestRule.onNodeWithTag("loading_ui").isDisplayed()
74 | }
75 | }
76 |
77 | /*
78 |
79 | Also test:
80 | - no network error
81 |
82 | */
83 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/mkg20001/nixosimage/FastlaneScreenshot.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage
2 |
3 | import androidx.compose.ui.test.isDisplayed
4 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
5 | import androidx.compose.ui.test.onNodeWithTag
6 | import androidx.compose.ui.test.performClick
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.platform.app.InstrumentationRegistry
9 | import io.mkg20001.nixosimage.ui.install.Install
10 | import org.junit.AfterClass
11 | import org.junit.Assert.assertEquals
12 | import org.junit.BeforeClass
13 | import org.junit.Rule
14 | import org.junit.Test
15 | import org.junit.runner.RunWith
16 | import tools.fastlane.screengrab.Screengrab
17 | import tools.fastlane.screengrab.cleanstatusbar.CleanStatusBar
18 | import tools.fastlane.screengrab.locale.LocaleTestRule
19 |
20 |
21 | /**
22 | * Instrumented test, which will execute on an Android device.
23 | *
24 | * See [testing documentation](http://d.android.com/tools/testing).
25 | */
26 | @RunWith(AndroidJUnit4::class)
27 | class FastlaneScreenshot {
28 | companion object {
29 | @BeforeClass @JvmStatic
30 | fun beforeAll() {
31 | CleanStatusBar.enableWithDefaults()
32 |
33 | val instrumentation = InstrumentationRegistry.getInstrumentation()
34 | instrumentation.uiAutomation.executeShellCommand(
35 | "appops set ${BuildConfig.APPLICATION_ID} MANAGE_EXTERNAL_STORAGE allow"
36 | ).close()
37 | }
38 |
39 | @AfterClass @JvmStatic
40 | fun afterAll() {
41 | CleanStatusBar.disable()
42 | }
43 | }
44 |
45 | @Test
46 | fun useAppContext() {
47 | // Context of the app under test.
48 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
49 | assertEquals("io.mkg20001.nixosimage", appContext.packageName)
50 | }
51 |
52 | @Rule @JvmField
53 | val localeTestRule = LocaleTestRule()
54 |
55 | @get:Rule
56 | val composeTestRule = createAndroidComposeRule()
57 | @get:Rule
58 | val composeTestRuleTwo = createAndroidComposeRule()
59 |
60 | @Test
61 | fun testTakeScreenshot() {
62 | composeTestRule.waitUntil(6000) {
63 | // Replace with your condition, e.g., UI element becomes visible after async event
64 | composeTestRule.onNodeWithTag("loaded_ui").isDisplayed()
65 | }
66 |
67 | Thread.sleep(2000)
68 |
69 | Screengrab.screenshot("loaded")
70 |
71 | composeTestRule.onNodeWithTag("install").performClick()
72 |
73 | Thread.sleep(2000)
74 |
75 | composeTestRule.waitUntil(6000) {
76 | // Replace with your condition, e.g., UI element becomes visible after async event
77 | composeTestRule.onNodeWithTag("install_ui").isDisplayed()
78 | }
79 |
80 | Screengrab.screenshot("installing")
81 | }
82 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/install/MagiskInstallMethod.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.install
2 |
3 | import android.content.Context
4 | import android.content.res.AssetManager
5 | import android.util.Log
6 | import com.topjohnwu.superuser.Shell
7 | import io.mkg20001.nixosimage.BuildConfig
8 | import io.mkg20001.nixosimage.R
9 | import io.mkg20001.nixosimage.extra.ExtraImageUtils
10 | import io.sentry.Sentry
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import java.io.File
13 | import kotlin.io.path.pathString
14 |
15 | fun initShell() {
16 | Shell.enableVerboseLogging = BuildConfig.DEBUG
17 | Shell.setDefaultBuilder(
18 | Shell.Builder.create()
19 | .setFlags(Shell.FLAG_MOUNT_MASTER)
20 | // .setInitializers(ShellInit::class.java)
21 | .setTimeout(10)
22 | )
23 | }
24 |
25 | object MagiskInstallMethod: ImageInstallMethod {
26 | override val id = "magisk"
27 |
28 | override val display = R.string.method_magisk
29 | override val showOpenTerminalAgainBtn: Boolean = false
30 |
31 | override fun isAvailable(): Boolean {
32 | return Shell.getShell().isRoot
33 | }
34 |
35 | override suspend fun installImage(
36 | context: Context,
37 | image: File,
38 | assets: AssetManager,
39 | progress: MutableStateFlow
40 | ): Boolean {
41 | val shell = Shell.getShell()
42 | if (!shell.isRoot) {
43 | Log.e("Magisk", "acquired shell is not root, giving up")
44 | return false
45 | }
46 |
47 | fun executeWithRoot(cmd: String): Boolean {
48 | try {
49 | Log.w("Magisk", "execute with su: ${cmd}")
50 | val process = Shell.cmd(cmd).exec()
51 | Log.w("Magisk", " => res ${process.code}")
52 | process.err.forEach {
53 | Log.w("Magisk", " => err ${it}")
54 | }
55 | return process.code == 0
56 | } catch (e: InterruptedException) {
57 | e.printStackTrace()
58 | Sentry.captureException(e)
59 | return false
60 | }
61 | }
62 |
63 | // The main shell is now constructed and cached
64 | executeWithRoot("mkdir -p ${DebugInstallMethod.getSdcardPathForTesting().pathString}")
65 | executeWithRoot("cp ${image.path} ${DebugInstallMethod.fromSdCard().pathString}")
66 | executeWithRoot(ExtraImageUtils.RM_EXISTING)
67 | executeWithRoot("magisk resetprop ro.debuggable 1")
68 | // This will likely tear down the entire app, so we just start the activity right after
69 | executeWithRoot("stop; start; " + ExtraImageUtils.LAUNCH_INSTALLER)
70 |
71 | return true
72 | }
73 |
74 | override val needsExternalStorage: Boolean = false
75 | override val needsLaunchTerminalAfterwards: Boolean = false
76 | override val needsCleanup: Boolean = true
77 |
78 | override fun doCleanup() {
79 | // TODO: undo the prop change
80 | }
81 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/ui/DropdownItem.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.foundation.layout.wrapContentSize
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.filled.ArrowDropDown
13 | import androidx.compose.material3.DropdownMenu
14 | import androidx.compose.material3.DropdownMenuItem
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.runtime.setValue
22 | import androidx.compose.ui.Alignment
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.text.TextStyle
25 | import androidx.compose.ui.unit.dp
26 |
27 | data class ExtItem(
28 | val id: String,
29 | val label: String,
30 | // This is inverted so we can do "stateBla.value?.real" to check if anything valid is selected
31 | val real: Boolean = false
32 | ) {
33 | constructor(label: String): this("", label, false)
34 | constructor(id: String, label: String): this(id, label, true)
35 | }
36 |
37 |
38 | @Composable
39 | fun MyDropdown(modifier: Modifier, style: TextStyle, selectedItem: ExtItem?, items: List, onValueChange: (item: ExtItem) -> Unit) {
40 | var expanded by remember { mutableStateOf(false) }
41 |
42 | if (selectedItem == null && !items.isEmpty()) {
43 | onValueChange(items[0])
44 | }
45 |
46 | Box(modifier = modifier.wrapContentSize(Alignment.TopStart)) {
47 | Row(
48 | verticalAlignment = Alignment.CenterVertically,
49 | horizontalArrangement = Arrangement.SpaceBetween,
50 | modifier = Modifier
51 | .clickable { expanded = true }
52 | .padding(14.dp)
53 | .fillMaxWidth()
54 | ) {
55 | Text(
56 | text = selectedItem?.label ?: "(none)",
57 | style = style
58 | )
59 | Icon(
60 | imageVector = Icons.Default.ArrowDropDown,
61 | contentDescription = "Open dropdown"
62 | )
63 | }
64 | DropdownMenu(
65 | expanded = expanded,
66 | onDismissRequest = { expanded = false },
67 | modifier = modifier
68 | ) {
69 | items.forEach {
70 | DropdownMenuItem(
71 | text = { Text(it.label, style = style) },
72 | enabled = it.real,
73 | onClick = {
74 | expanded = false
75 | onValueChange(it)
76 | }
77 | )
78 | }
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/data/GithHubRelease.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.data
2 |
3 | import com.apollographql.apollo.ApolloClient
4 | import io.mkg20001.nixosimage.GetReleasesQuery
5 | import java.io.Serializable
6 |
7 | private val regexTag = Regex("^nixos-(?[a-z0-9.]+)$")
8 | private val regexImage = Regex("^image-(?[a-z0-9.]+)-(?[a-z0-9_-]+).tar.gz$")
9 |
10 | private val currentArch = System.getProperty("os.arch")
11 | val WANTED_ARCH = when(currentArch) {
12 | "arm64" -> "aarch64"
13 | else -> currentArch
14 | }
15 |
16 | data class GitHubRelease(
17 | val tagName: String,
18 | val assets: List
19 | ) {
20 | var nixosVersion = ""
21 |
22 | // nixos-24.11
23 | init {
24 | val res = regexTag.matchEntire(tagName)
25 | if (res != null) {
26 | nixosVersion = res.groups["version"]!!.value
27 | }
28 | }
29 |
30 | fun getSupported(): List {
31 | return assets.filter { it.isSupported() }
32 | }
33 | }
34 |
35 | data class GitHubReleaseAsset(
36 | val id: String,
37 | val name: String,
38 | var url: String,
39 | var updatedAt: Any,
40 | val digest: String,
41 | ): Serializable {
42 | var arch = ""
43 | var version = ""
44 |
45 | // image-unstable-aarch64.tar.gz
46 | init {
47 | val res = regexImage.matchEntire(name)
48 | if (res != null) {
49 | version = res.groups["version"]!!.value
50 | arch = res.groups["arch"]!!.value
51 | }
52 | }
53 |
54 | fun isSupported(): Boolean {
55 | return WANTED_ARCH == arch
56 | }
57 | }
58 |
59 | /* private class AuthorizationInterceptor() : Interceptor {
60 | override fun intercept(chain: Interceptor.Chain): Response {
61 | val request = chain.request().newBuilder()
62 | .apply {
63 | addHeader("Authorization", AUTH_HEADER)
64 | }
65 | .build()
66 | return chain.proceed(request)
67 | }
68 | } */
69 |
70 | val apolloClient = ApolloClient.Builder()
71 | .serverUrl("https://nixos-image.mkg20001.io/graphql")
72 | // If you want to develop on the server, run "cargo run" and fill in your IP here
73 | // .serverUrl("http://192.168.178.69:8000/graphql")
74 | // Access github directly with auth
75 | /* .okHttpClient(
76 | OkHttpClient.Builder()
77 | .addInterceptor(AuthorizationInterceptor())
78 | .build()
79 | ) */
80 | .build()
81 |
82 | object GitHubReleaseClient {
83 | suspend fun getReleases(): List? {
84 | val resp = apolloClient.query(GetReleasesQuery()).execute()
85 |
86 | if (resp.exception != null) {
87 | resp.exception!!.printStackTrace()
88 | return null
89 | }
90 |
91 | return resp.data!!.repository!!.releases.nodes!!.map {
92 | GitHubRelease(
93 | it!!.tagName,
94 | it.releaseAssets.nodes!!.map {
95 | GitHubReleaseAsset(it!!.id, it.name, it.url.toString(), it.updatedAt, it.digest!!)
96 | }.filter { it.version != "" }
97 | )
98 | }.filter { it.nixosVersion != "" }
99 | }
100 | }
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | #[macro_use] extern crate rocket;
2 |
3 | use std::env;
4 | use std::path::{PathBuf};
5 | use graphql_client::{GraphQLQuery, Response};
6 | use serde::{Deserialize, Serialize};
7 | use rocket::serde::{json::Json};
8 | use rocket::State;
9 | use crate::get_releases::{Variables, ResponseData};
10 | use twelf::{config, Layer};
11 |
12 | #[allow(clippy::upper_case_acronyms)]
13 | type URI = String;
14 | type DateTime = String;
15 |
16 | #[derive(Debug, Serialize, Deserialize, Clone)]
17 | struct QueryBody {
18 | /// The values for the variables. They must match those declared in the queries.
19 | pub variables: EmptyVars,
20 | /// The GraphQL query, as a string.
21 | pub query: String,
22 | /// The GraphQL operation name, as a string.
23 | #[serde(rename = "operationName")]
24 | pub operation_name: String,
25 | }
26 |
27 | /*
28 |
29 | #[derive(Debug, Serialize, Deserialize, Clone)]
30 | pub struct QueryBody<'a, Variables> {
31 | /// The values for the variables. They must match those declared in the queries.
32 | pub variables: Variables,
33 | /// The GraphQL query, as a string.
34 | pub query: &'a str,
35 | /// The GraphQL operation name, as a string.
36 | #[serde(rename = "operationName")]
37 | pub operation_name: &'a str,
38 | }
39 |
40 | */
41 |
42 | #[derive(Debug, Serialize, Deserialize, Clone)]
43 | struct EmptyVars {
44 |
45 | }
46 |
47 | #[derive(GraphQLQuery, Serialize, Deserialize, Clone)]
48 | #[graphql(
49 | schema_path = "app/src/main/graphql/schema.graphqls",
50 | query_path = "app/src/main/graphql/GetReleases.graphql",
51 | response_derives = "Serialize,PartialEq",
52 | )]
53 | struct GetReleases;
54 |
55 | #[config]
56 | #[derive(Debug, Default, Serialize, Clone)]
57 | pub struct Config {
58 | pub port: u16,
59 | pub token: String,
60 | }
61 |
62 | pub fn load_config() -> Config {
63 | // Layer from different sources to build configuration. Order matters!
64 | let conf_file = match env::var("CONFIG") {
65 | Ok(val) => PathBuf::from(val),
66 | Err(_) => env::current_dir().expect("Failed to get CWD").join("config.yaml"),
67 | };
68 |
69 | let conf = Config::with_layers(&[
70 | Layer::Yaml(conf_file),
71 | Layer::Env(Some(String::from("APP_"))),
72 | ]).expect("Failed to load config");
73 |
74 | conf
75 | }
76 |
77 | #[post("/graphql", data = "<_query>")]
78 | async fn graphql(_query: Json, config: &State) -> Json> {
79 | let request_body = GetReleases::build_query(Variables {});
80 |
81 | let response: Response = reqwest::Client::new()
82 | .post("https://api.github.com/graphql")
83 | .header(reqwest::header::USER_AGENT, "github.com/mkg20001/nixos-avf-image-app")
84 | .bearer_auth(config.token.clone())
85 | .json(&request_body)
86 | .send()
87 | .await
88 | .expect("Failed to send GraphQL request")
89 | .json()
90 | .await
91 | .expect("Failed to parse GraphQL response");
92 |
93 | Json(response)
94 | }
95 |
96 | #[launch]
97 | fn rocket() -> _ {
98 | let config = load_config();
99 | rocket::build()
100 | .configure(rocket::Config::figment().merge(("port", config.port)).merge(("address", "::")))
101 | .manage(config)
102 | .mount("/", routes![graphql])
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/install/ReplaceInstallMethod.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.install
2 |
3 | import android.content.Context
4 | import android.content.res.AssetManager
5 | import android.os.Environment
6 | import android.util.Log
7 | import android.widget.Toast
8 | import io.mkg20001.nixosimage.R
9 | import io.mkg20001.nixosimage.data.ProgressStream
10 | import io.mkg20001.nixosimage.data.mkdirp
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.withContext
14 | import okio.IOException
15 | import org.apache.commons.compress.archivers.ArchiveEntry
16 | import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
17 | import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
18 | import java.io.File
19 | import java.nio.file.Files
20 | import java.nio.file.Path
21 | import java.nio.file.StandardCopyOption
22 |
23 | object ReplaceInstallMethod: ImageInstallMethod {
24 | val TAG = "ReplaceInstall"
25 |
26 | override val id = "replace"
27 |
28 | override val display = R.string.method_replace
29 | override val needsImageClean: Boolean = false
30 |
31 | override fun isAvailable(): Boolean {
32 | // no special requirements
33 | return true
34 | }
35 |
36 | fun getImageDownloadsDir(): Path {
37 | return Environment.getExternalStoragePublicDirectory("Download/image").toPath()
38 | }
39 |
40 | @Throws(IOException::class)
41 | fun installTo(source: File, dir: Path, onProgress: (Int) -> Unit) {
42 | Log.i(TAG, "Extracting. source: $source, destination: $dir")
43 |
44 | val progressStream = ProgressStream(source.inputStream(), source.length().toDouble(), 0, onProgress)
45 |
46 | TarArchiveInputStream(GzipCompressorInputStream(progressStream)).use { tarStream ->
47 | Files.createDirectories(dir)
48 | var entry: ArchiveEntry?
49 | while ((tarStream.nextEntry.also { entry = it }) != null) {
50 | val to = dir.resolve(entry!!.name)
51 | if (Files.isDirectory(to)) {
52 | Files.createDirectories(to)
53 | continue
54 | }
55 | Files.copy(tarStream, to, StandardCopyOption.REPLACE_EXISTING)
56 | }
57 | }
58 | Log.i(TAG, "Done extracting!")
59 | }
60 |
61 | override suspend fun installImage(
62 | context: Context,
63 | image: File,
64 | assets: AssetManager,
65 | progress: MutableStateFlow
66 | ): Boolean {
67 |
68 | val dir = getImageDownloadsDir()
69 |
70 | if (!mkdirp(dir.toString())) {
71 | return false
72 | }
73 |
74 | withContext(Dispatchers.IO) {
75 | Log.i(TAG, "Scripts")
76 |
77 | // extract image to /sdcard/Download/image
78 | installTo(image, dir) {
79 | if (progress.value != it) {
80 | Log.d(TAG, "Extract progress: $it%")
81 | progress.tryEmit(it)
82 | }
83 | }
84 |
85 | // force scripts to be executable
86 | listOf("replace.sh").forEach {
87 | val file = File(dir.toFile(), it)
88 | file.setExecutable(true)
89 | }
90 | }
91 |
92 | withContext(Dispatchers.Main) {
93 | // tell user to run "bash /mnt/shared/image/replace.sh"
94 | Toast.makeText(context, R.string.toast_replace_script, Toast.LENGTH_LONG).show()
95 | }
96 |
97 | Log.i(TAG, "Done!")
98 |
99 | return true
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | appcompat = "1.7.1"
3 | material = "1.13.0"
4 | constraintlayout = "2.2.1"
5 | lifecycleLivedataKtx = "2.10.0"
6 | lifecycleViewmodelKtx = "2.10.0"
7 | navigationFragmentKtx = "2.9.6"
8 | navigationUiKtx = "2.9.6"
9 | apollo = "4.3.3"
10 | activity = "1.12.0"
11 | libsu = "6.0.0"
12 |
13 | agp = "8.13.1"
14 | kotlin = "2.2.21"
15 | coreKtx = "1.17.0"
16 | junit = "4.13.2"
17 | junitVersion = "1.3.0"
18 | espressoCore = "3.7.0"
19 | lifecycleRuntimeKtx = "2.10.0"
20 | activityCompose = "1.12.0"
21 | composeBom = "2025.11.01"
22 |
23 |
24 | [libraries]
25 | androidx-compose-material-icons-core = { module = "androidx.compose.material:material-icons-core" }
26 | androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
27 | libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
28 | libsu-service = { module = "com.github.topjohnwu.libsu:service", version.ref = "libsu" }
29 | libsu-nio = { module = "com.github.topjohnwu.libsu:nio", version.ref = "libsu" }
30 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
31 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
32 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
33 | androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
34 | androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
35 | androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
36 | androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
37 | apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo"}
38 | androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
39 |
40 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
41 | junit = { group = "junit", name = "junit", version.ref = "junit" }
42 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
43 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
44 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
45 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
46 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
47 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
48 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
49 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
50 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
51 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
52 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
53 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
54 |
55 |
56 | [plugins]
57 | android-application = { id = "com.android.application", version.ref = "agp" }
58 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
59 | apollo = { id = "com.apollographql.apollo", version.ref = "apollo" }
60 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
61 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/app.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
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 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/ui/install/InstallComposable.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.install
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material3.Button
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.LinearProgressIndicator
12 | import androidx.compose.material3.LocalTextStyle
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Scaffold
15 | import androidx.compose.material3.Text
16 | import androidx.compose.material3.TopAppBar
17 | import androidx.compose.material3.TopAppBarDefaults
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.collectAsState
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.platform.LocalContext
22 | import androidx.compose.ui.platform.testTag
23 | import androidx.compose.ui.res.stringResource
24 | import androidx.compose.ui.unit.dp
25 | import dev.jeziellago.compose.markdowntext.MarkdownText
26 | import io.mkg20001.nixosimage.R
27 | import io.mkg20001.nixosimage.ui.theme.NixosImageTheme
28 |
29 | @Composable
30 | fun InstallComposable(install: InstallMagic) {
31 | val text = install.text.collectAsState().value
32 | val progress = install.progress.collectAsState().value
33 | val done = install.done.collectAsState().value
34 | val applicationContext = LocalContext.current.applicationContext
35 |
36 | Column {
37 | Text(text = text, modifier = Modifier.fillMaxWidth().padding(12.dp))
38 | LinearProgressIndicator(progress = { progress.toFloat() / 100 }, modifier = Modifier.fillMaxWidth().padding(12.dp))
39 | Instructions(install.method.id)
40 | if (install.method.showOpenTerminalAgainBtn) {
41 | Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) {
42 | Button(
43 | onClick = {
44 | OpenTerminal(applicationContext)
45 | },
46 | enabled = done,
47 | ) {
48 | Text(stringResource(R.string.open_terminal_again))
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | @OptIn(ExperimentalMaterial3Api::class)
56 | @Composable
57 | fun InstallView(content: @Composable () -> Unit) {
58 | NixosImageTheme {
59 | Scaffold(
60 | modifier = Modifier.fillMaxSize(),
61 | topBar = {
62 | TopAppBar(
63 | colors = TopAppBarDefaults.topAppBarColors(
64 | containerColor = MaterialTheme.colorScheme.primaryContainer,
65 | titleContentColor = MaterialTheme.colorScheme.primary,
66 | ),
67 | title = {
68 | Text(stringResource(R.string.install))
69 | }
70 | )
71 | },
72 | ) { innerPadding ->
73 | Column(modifier = Modifier.padding(innerPadding).testTag("install_ui")) {
74 | content()
75 | }
76 | }
77 | }
78 | }
79 |
80 | @Composable
81 | fun Instructions(method: String) {
82 | val applicationContext = LocalContext.current.applicationContext
83 |
84 | fun GetContent(file: String): String {
85 | return String(applicationContext.assets.open("instructions_$file.md").readAllBytes(),
86 | Charsets.UTF_8)
87 | }
88 |
89 | val markdown = GetContent(method) + "\n" + GetContent("generic")
90 |
91 | MarkdownText(
92 | modifier = Modifier.padding(8.dp),
93 | markdown = markdown,
94 | syntaxHighlightColor = LocalTextStyle.current.background,
95 | syntaxHighlightTextColor = LocalTextStyle.current.color,
96 | onLinkClicked = {
97 | if (it == "terminal://") {
98 | OpenTerminal(applicationContext)
99 | }
100 | },
101 | )
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/data/ImageDownloader.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.data
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import io.sentry.Sentry
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 | import okhttp3.OkHttpClient
9 | import okhttp3.Request
10 | import okhttp3.internal.http2.StreamResetException
11 | import java.io.File
12 | import java.io.FileOutputStream
13 | import java.io.OutputStream
14 | import java.security.MessageDigest
15 |
16 | suspend fun downloadFile(
17 | context: Context,
18 | fileUrl: String,
19 | fileName: String,
20 | digest: String,
21 | onProgress: (percent: Int) -> Unit,
22 | ): File? {
23 | return withContext(Dispatchers.IO) {
24 | try {
25 | val file = File(context.cacheDir, fileName)
26 | var retry = 0
27 |
28 | val digestSplit = digest.split(":", limit = 2)
29 | if (digestSplit.size != 2) {
30 | throw IllegalArgumentException("Invalid digest format: $digest")
31 | }
32 |
33 | val digestAlgo = when (digestSplit[0].lowercase()) {
34 | "sha256" -> "SHA-256"
35 | "sha512" -> "SHA-512"
36 | else -> throw IllegalArgumentException("Unsupported digest algorithm: ${digestSplit[0]}")
37 | }
38 |
39 | val expectedHex = digestSplit[1].lowercase()
40 |
41 | while (true) {
42 | Log.d("DL", "Trying download, try $retry/3")
43 |
44 | try {
45 | val alreadyDownloadedBytes = if (file.exists()) file.length() else 0L
46 |
47 | val client = OkHttpClient()
48 | val request = Request.Builder().url(fileUrl).apply {
49 | if (alreadyDownloadedBytes > 0) {
50 | addHeader("Range", "bytes=$alreadyDownloadedBytes-")
51 | }
52 | }.build()
53 |
54 | val response = client.newCall(request).execute()
55 | val body = response.body ?: return@withContext null
56 |
57 | // re-use already existing file
58 | if (file.exists() && body.contentLength() == file.length()) {
59 | onProgress(100) // update ui
60 | return@withContext file
61 | }
62 |
63 | val progressStream = ProgressStream(
64 | body.source().inputStream(),
65 | body.contentLength().toDouble(),
66 | alreadyDownloadedBytes,
67 | onProgress,
68 | )
69 |
70 | val digest = MessageDigest.getInstance(digestAlgo)
71 | val outputStream = FileOutputStream(file, true)
72 | var digestStream: DigestStream
73 |
74 |
75 | if (alreadyDownloadedBytes < 1) {
76 | digestStream = DigestStream(progressStream, digest)
77 |
78 | digestStream.use { input ->
79 | outputStream.use { output ->
80 | input.copyTo(output)
81 | }
82 | }
83 | } else {
84 | // data is only partial in this case, hash at the end
85 | progressStream.use { input ->
86 | outputStream.use { output ->
87 | input.copyTo(output)
88 | }
89 | }
90 |
91 | digestStream = DigestStream(file.inputStream(), digest)
92 | digestStream.copyTo(OutputStream.nullOutputStream())
93 | }
94 |
95 | if (!digestStream.validate(expectedHex)) {
96 | // TODO: toast
97 | Log.w("DL", "Hashsum missmatch")
98 | file.delete()
99 | continue
100 | }
101 |
102 | break
103 | } catch(e: StreamResetException) {
104 | if (retry != 3) {
105 | retry++
106 | } else {
107 | throw e
108 | }
109 | } catch (e: java.net.SocketException) {
110 | if (e.message?.contains("Software caused connection abort") == true && retry != 3) {
111 | retry++
112 | } else {
113 | throw e
114 | }
115 | }
116 | }
117 |
118 | file
119 | } catch (e: Exception) {
120 | e.printStackTrace()
121 | Sentry.captureException(e)
122 | null
123 | }
124 | }
125 | }
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.apollo)
5 | alias(libs.plugins.compose.compiler)
6 |
7 | id("io.sentry.android.gradle") version "5.7.0"
8 | }
9 |
10 | android {
11 | namespace = "io.mkg20001.nixosimage"
12 | compileSdk = 36
13 |
14 | defaultConfig {
15 | applicationId = "io.mkg20001.nixosimage"
16 | minSdk = 35
17 | targetSdk = 36
18 | versionCode = 8
19 | versionName = "0.2.2"
20 |
21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
22 | }
23 |
24 | buildTypes {
25 | debug {
26 | buildConfigField("boolean", "ALLOW_ANY_METHOD", "true")
27 | }
28 | release {
29 | buildConfigField("boolean", "ALLOW_ANY_METHOD", "false")
30 | isMinifyEnabled = true
31 | isShrinkResources = true
32 | proguardFiles("proguard-rules.pro")
33 | }
34 | }
35 | compileOptions {
36 | sourceCompatibility = JavaVersion.VERSION_11
37 | targetCompatibility = JavaVersion.VERSION_11
38 | }
39 | kotlinOptions {
40 | jvmTarget = "11"
41 | }
42 | buildFeatures {
43 | compose = true
44 | buildConfig = true
45 | }
46 | }
47 |
48 | dependencies {
49 | implementation(libs.androidx.core.ktx)
50 | implementation(libs.androidx.lifecycle.runtime.ktx)
51 | implementation(libs.androidx.activity.compose)
52 | implementation(platform(libs.androidx.compose.bom))
53 | implementation(libs.androidx.ui)
54 | implementation(libs.androidx.ui.graphics)
55 | implementation(libs.androidx.ui.tooling.preview)
56 | implementation(libs.androidx.material3)
57 | testImplementation(libs.junit)
58 | androidTestImplementation(libs.androidx.junit)
59 | androidTestImplementation(libs.androidx.espresso.core)
60 | androidTestImplementation(platform(libs.androidx.compose.bom))
61 | androidTestImplementation(libs.androidx.ui.test.junit4)
62 | debugImplementation(libs.androidx.ui.tooling)
63 | debugImplementation(libs.androidx.ui.test.manifest)
64 |
65 | implementation(libs.apollo.runtime)
66 | implementation(libs.libsu.core)
67 |
68 | // https://mvnrepository.com/artifact/org.apache.commons/commons-compress
69 | implementation("org.apache.commons:commons-compress:1.27.1")
70 |
71 | // for permission granting
72 | // implementation("androidx.test:rules:1.2.0")
73 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
74 |
75 | // graphql
76 | /* implementation("com.squareup.okhttp3:okhttp:4.10.0")
77 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
78 | implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")
79 | implementation("com.apollographql.apollo3:apollo-runtime:3.8.5")
80 | implementation("com.apollographql.apollo:apollo-api:4.1.1") */
81 |
82 | val nav_version = "2.8.9"
83 |
84 | implementation("androidx.navigation:navigation-compose:$nav_version")
85 |
86 | // Optional - Included automatically by material, only add when you need
87 | // the icons but not the material library (e.g. when using Material3 or a
88 | // custom design system based on Foundation)
89 | implementation(libs.androidx.compose.material.icons.core)
90 | // Optional - Add full set of material icons
91 | implementation(libs.androidx.compose.material.icons.extended)
92 | // Optional - Add window size utils
93 | // implementation("androidx.compose.material3.adaptive:adaptive")
94 |
95 | // Optional - Integration with ViewModels
96 | // implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5")
97 | // Optional - Integration with LiveData
98 | // implementation("androidx.compose.runtime:runtime-livedata")
99 |
100 | // implementation("androidx.lifecycle:lifecycle-viewmodel-ktx")
101 |
102 | // https://mvnrepository.com/artifact/tools.fastlane/screengrab
103 | implementation("tools.fastlane:screengrab:2.1.1")
104 |
105 | implementation("com.github.jeziellago:compose-markdown:0.5.7")
106 |
107 | // implementation("com.yazantarifi:markdown-compose:1.0.4")
108 | }
109 |
110 | apollo {
111 | service("service") {
112 | packageName.set("io.mkg20001.nixosimage")
113 | introspection {
114 | endpointUrl.set("https://api.github.com/graphql")
115 | schemaFile.set(file("src/main/graphql/schema.graphqls"))
116 | }
117 | }
118 | }
119 |
120 | configurations.all {
121 | resolutionStrategy {
122 | force("androidx.test.espresso:espresso-core:3.5.0")
123 | }
124 | }
125 |
126 | sentry {
127 | org.set("nixos-avf")
128 | projectName.set("nixos-avf-image-app")
129 |
130 | // this will upload your source code to Sentry to show it as part of the stack traces
131 | // disable if you don't want to expose your sources
132 | includeSourceContext.set(true)
133 | }
134 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/ui/install/InstallMagic.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.install
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
6 | import android.util.Log
7 | import android.widget.Toast
8 | import androidx.core.content.ContextCompat.startActivity
9 | import io.mkg20001.nixosimage.R
10 | import io.mkg20001.nixosimage.data.GitHubReleaseAsset
11 | import io.mkg20001.nixosimage.data.downloadFile
12 | import io.mkg20001.nixosimage.extra.ExtraImageUtils
13 | import io.mkg20001.nixosimage.install.ImageInstallMethod
14 | import io.sentry.Breadcrumb
15 | import io.sentry.Sentry
16 | import io.sentry.SentryLevel
17 | import kotlinx.coroutines.Dispatchers
18 | import kotlinx.coroutines.flow.MutableStateFlow
19 | import kotlinx.coroutines.flow.StateFlow
20 | import kotlinx.coroutines.withContext
21 |
22 | fun OpenTerminal(applicationContext: Context) {
23 | val packageName = "com.android.virtualization.terminal"
24 | val className = "com.android.virtualization.terminal.MainActivity"
25 |
26 | val intent = Intent().apply {
27 | setClassName(packageName, className)
28 | }
29 | intent.flags = FLAG_ACTIVITY_NEW_TASK
30 |
31 | try {
32 | startActivity(applicationContext, intent, null)
33 | } catch (e: Exception) {
34 | e.printStackTrace()
35 | // Handle the case when the target activity is not found or other issues
36 | }
37 | }
38 |
39 |
40 | class InstallMagic(
41 | val applicationContext: Context,
42 | val method: ImageInstallMethod,
43 | val asset: GitHubReleaseAsset
44 | ) {
45 | private val _text = MutableStateFlow("")
46 | val text: StateFlow = _text
47 |
48 | private val _progress = MutableStateFlow(0)
49 | val progress: StateFlow = _progress
50 |
51 | private val _done = MutableStateFlow(false)
52 | val done: StateFlow = _done
53 |
54 | suspend fun run() {
55 | Sentry.addBreadcrumb(Breadcrumb().apply {
56 | message = "Installing"
57 | category = "task"
58 | level = SentryLevel.INFO
59 | setData("method", method.id)
60 | setData("asset", asset)
61 | })
62 |
63 | val extra = ExtraImageUtils()
64 |
65 | updateStatus(R.string.install_step_downloading)
66 | _progress.tryEmit(0)
67 |
68 | fun installOK() {
69 | Log.i("Install", "ok")
70 | _done.tryEmit(true)
71 | if (method.needsLaunchTerminalAfterwards) {
72 | OpenTerminal(applicationContext)
73 | }
74 | }
75 |
76 | fun installFail(error: Int) {
77 | Log.e("Install", "failed")
78 | errorOut(error)
79 | }
80 |
81 | // TODO: include methods needing cleanup properly
82 | Log.i("Download", "Downloading image")
83 |
84 | val file = downloadFile(
85 | context = applicationContext,
86 | fileUrl = asset.url,
87 | digest = asset.digest,
88 | fileName = "image-cached-" + asset.id + "@" + asset.updatedAt + "#" + asset.digest
89 | ) { progress ->
90 | if (_progress.value != progress) {
91 | Log.d("Download", "Progress: $progress%")
92 | // You can update UI with LiveData or State here
93 | _progress.tryEmit(progress)
94 | }
95 | }
96 |
97 | if (file != null) {
98 | Log.i("Download", "File downloaded: ${file.absolutePath}")
99 |
100 | Log.i("Install", "Installing")
101 | updateStatus(R.string.install_step_installing)
102 |
103 | if (method.needsImageClean) {
104 | if (!extra.cleanupImage()) {
105 | withContext(Dispatchers.Main) {
106 | Toast.makeText(applicationContext, R.string.remove_existing_image, Toast.LENGTH_LONG).show()
107 | }
108 | }
109 | }
110 |
111 | try {
112 | val success = method.installImage(applicationContext, file, applicationContext.assets, _progress)
113 |
114 | if (success) {
115 | installOK()
116 | } else {
117 | installFail(R.string.install_err_image)
118 | }
119 | } catch(e: Exception) {
120 | e.printStackTrace()
121 | Sentry.captureException(e)
122 | installFail(R.string.install_err_image)
123 | }
124 | } else {
125 | Log.e("Download", "Failed to download file")
126 | errorOut(R.string.install_err_network)
127 | }
128 | }
129 |
130 | fun updateStatus(task: Int) {
131 | val out = applicationContext.getString(R.string.install_task) + " " + applicationContext.getString(task) + "\n\n" +
132 | applicationContext.getString(R.string.install_version) + " " + asset.version + "\n\n" +
133 | applicationContext.getString(R.string.install_architecture) + " " + asset.arch
134 |
135 | _text.tryEmit(out)
136 | }
137 |
138 | fun errorOut(error: Int) {
139 | // set status to "Error"
140 | _text.tryEmit("Error! " + applicationContext.getString(error))
141 | }
142 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
34 |
35 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
54 |
55 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
74 |
75 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
94 |
95 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
114 |
115 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
134 |
135 |
--------------------------------------------------------------------------------
/old/HomeFragment.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.home
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.os.Environment
7 | import android.provider.Settings
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.widget.TextView
12 | import android.widget.Toast
13 | import androidx.fragment.app.Fragment
14 | import androidx.lifecycle.ViewModelProvider
15 | import io.mkg20001.nixosimage.BuildConfig
16 | import io.mkg20001.nixosimage.Install
17 | import io.mkg20001.nixosimage.R
18 | import io.mkg20001.nixosimage.data.GitHubReleaseAsset
19 | import io.mkg20001.nixosimage.data.clearOldFiles
20 | import io.mkg20001.nixosimage.databinding.FragmentHomeBinding
21 | import io.mkg20001.nixosimage.install.ImageInstallMethod
22 | import io.mkg20001.nixosimage.install.InstallMethods
23 | import io.mkg20001.nixosimage.ui.DropdownItem
24 | import kotlinx.coroutines.CoroutineScope
25 | import kotlinx.coroutines.Dispatchers
26 | import kotlinx.coroutines.launch
27 |
28 |
29 | class HomeFragment : Fragment() {
30 |
31 | private var _binding: FragmentHomeBinding? = null
32 |
33 | // This property is only valid between onCreateView and
34 | // onDestroyView.
35 | private val binding get() = _binding!!
36 |
37 | override fun onCreateView(
38 | inflater: LayoutInflater,
39 | container: ViewGroup?,
40 | savedInstanceState: Bundle?
41 | ): View {
42 | val homeViewModel =
43 | ViewModelProvider(this).get(HomeViewModel::class.java)
44 |
45 | _binding = FragmentHomeBinding.inflate(inflater, container, false)
46 | val root: View = binding.root
47 |
48 | val textView: TextView = binding.textHome
49 | val installBtn = binding.installBtn
50 | val refreshBtn = binding.refreshBtn
51 |
52 | val methodsDropdown = binding.installMethod
53 | val versionsDropdown = binding.nixosVersion
54 |
55 | clearOldFiles(requireContext().cacheDir)
56 |
57 | homeViewModel.state.observe(viewLifecycleOwner) {
58 | val str = when(it) {
59 | ImageViewState.LOADING -> R.string.introduction_loading
60 | ImageViewState.ERROR -> R.string.introduction_error
61 | ImageViewState.READY -> R.string.introduction
62 | }
63 | textView.text = getString(str)
64 | installBtn.isEnabled = it == ImageViewState.READY
65 | refreshBtn.isEnabled = it != ImageViewState.LOADING
66 |
67 | methodsDropdown.isEnabled = it === ImageViewState.READY
68 | versionsDropdown.isEnabled = it === ImageViewState.READY
69 | }
70 |
71 | refreshBtn.setOnClickListener {
72 | CoroutineScope(Dispatchers.IO).launch {
73 | homeViewModel.refresh()
74 | }
75 | }
76 |
77 | installBtn.setOnClickListener { // is only enabled when everything is ok
78 | val method = DropdownItem.getItem(methodsDropdown)!!
79 | val release = DropdownItem.getItem(versionsDropdown)!!
80 | install(
81 | homeViewModel.imageRelease.value!!.filter { it.id == release.id }.getOrNull(0)!!,
82 | InstallMethods.getMethod(method.id)!!
83 | )
84 | }
85 |
86 | homeViewModel.installMethods.observe(viewLifecycleOwner) {
87 | var items = it.map {
88 | if (BuildConfig.ALLOW_ANY_METHOD && !it.isAvailable()) {
89 | DropdownItem(it.id, "#DEBUG# " + getString(it.display))
90 | } else {
91 | DropdownItem(it.id, getString(it.display))
92 | }
93 | }
94 |
95 | if (items.isEmpty()) {
96 | items = listOf(DropdownItem(true, resources.getString(R.string.no_method)))
97 | }
98 |
99 | DropdownItem.setItems(requireContext(), items, methodsDropdown)
100 | }
101 |
102 | homeViewModel.imageRelease.observe(viewLifecycleOwner) {
103 | var items = it.map {
104 | DropdownItem(it.id, it.version + " (" + it.updatedAt + ", " + it.arch + ")")
105 | }
106 |
107 | if (items.isEmpty()) {
108 | items = listOf(DropdownItem(true, getString(R.string.no_compat)))
109 | }
110 |
111 | DropdownItem.setItems(requireContext(), items, versionsDropdown)
112 | }
113 |
114 | fun updateInstall() {
115 | installBtn.isEnabled = homeViewModel.state.value == ImageViewState.READY &&
116 | DropdownItem.selectedAndNotPlaceholder(methodsDropdown) &&
117 | DropdownItem.selectedAndNotPlaceholder(versionsDropdown)
118 | }
119 |
120 | DropdownItem.onChange(versionsDropdown) { updateInstall() }
121 | DropdownItem.onChange(methodsDropdown) { updateInstall() }
122 |
123 | CoroutineScope(Dispatchers.IO).launch {
124 | homeViewModel.refresh()
125 | }
126 |
127 | return root
128 | }
129 |
130 | private fun install(r: GitHubReleaseAsset, m: ImageInstallMethod) {
131 | if (m.needsExternalStorage) {
132 | if (!Environment.isExternalStorageManager()) {
133 | val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
134 | intent.data = Uri.parse("package:" + requireContext().packageName)
135 | requireContext().startActivity(intent)
136 | Toast.makeText(context, getString(R.string.toast_external_storage), Toast.LENGTH_LONG).show()
137 | return
138 | }
139 | }
140 |
141 | val intent: Intent = Intent(
142 | this.context,
143 | Install::class.java
144 | )
145 | val b = Bundle()
146 | b.putSerializable("image", r)
147 | b.putString("method", m.id)
148 | intent.putExtras(b)
149 | startActivity(intent)
150 | }
151 |
152 | override fun onDestroyView() {
153 | super.onDestroyView()
154 | _binding = null
155 | }
156 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/assets/nix-snowflake-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
174 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/screenshots.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | fastlane/screengrab
5 |
6 |
66 |
67 |
68 | en-US
69 |
70 |
123 |
124 |
![]()
125 |
126 |
127 |
228 |
229 |
230 |
--------------------------------------------------------------------------------
/assets/merged-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
199 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/ui/home/HomeComposable.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.home
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.os.Environment
7 | import android.provider.Settings
8 | import android.widget.Toast
9 | import androidx.compose.foundation.background
10 | import androidx.compose.foundation.layout.Arrangement
11 | import androidx.compose.foundation.layout.Column
12 | import androidx.compose.foundation.layout.Row
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.material3.Button
16 | import androidx.compose.material3.CircularProgressIndicator
17 | import androidx.compose.material3.MaterialTheme
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.MutableState
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.produceState
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.platform.LocalContext
29 | import androidx.compose.ui.platform.testTag
30 | import androidx.compose.ui.res.stringResource
31 | import androidx.compose.ui.text.TextStyle
32 | import androidx.compose.ui.unit.dp
33 | import androidx.compose.ui.unit.em
34 | import androidx.core.content.ContextCompat.startActivity
35 | import io.mkg20001.nixosimage.BuildConfig
36 | import io.mkg20001.nixosimage.R
37 | import io.mkg20001.nixosimage.data.GitHubReleaseAsset
38 | import io.mkg20001.nixosimage.data.GitHubReleaseClient
39 | import io.mkg20001.nixosimage.install.ImageInstallMethod
40 | import io.mkg20001.nixosimage.install.InstallMethods
41 | import io.mkg20001.nixosimage.ui.ExtItem
42 | import io.mkg20001.nixosimage.ui.MyDropdown
43 | import io.mkg20001.nixosimage.ui.install.Install
44 | import io.sentry.Sentry
45 | import java.time.LocalDateTime
46 | import java.time.format.DateTimeFormatter
47 | import java.time.format.FormatStyle
48 | import java.util.Locale
49 |
50 | sealed class HomeUiValues {
51 | object Loading : HomeUiValues()
52 | object Error : HomeUiValues()
53 | class Success(val methods: List, val versions: List) : HomeUiValues()
54 | }
55 |
56 | @Composable
57 | fun HomeComposable() {
58 | // Can't be bothered to make this a view model so this hack must do
59 | var trigger by remember { mutableStateOf(0) }
60 |
61 | val stateMethod: MutableState = remember { mutableStateOf(null) }
62 | val stateVersion: MutableState = remember { mutableStateOf(null) }
63 |
64 | val v by produceState(initialValue = HomeUiValues.Loading, key1 = trigger) {
65 | value = HomeUiValues.Loading
66 |
67 | try {
68 | // Reset selection
69 | stateMethod.value = null
70 | stateVersion.value = null
71 |
72 | val methods =
73 | if (BuildConfig.ALLOW_ANY_METHOD) InstallMethods.methods
74 | else InstallMethods.availableMethods()
75 |
76 | val rel = GitHubReleaseClient.getReleases()
77 | if (rel == null) {
78 | value = HomeUiValues.Error
79 | } else {
80 | val releases = rel.map { it.getSupported() }.flatten()
81 |
82 | value = HomeUiValues.Success(methods, releases)
83 | }
84 | } catch(e: Exception) {
85 | e.printStackTrace()
86 | Sentry.captureException(e)
87 | value = HomeUiValues.Error
88 | }
89 | }
90 |
91 | val baseModifier = Modifier.fillMaxWidth()
92 |
93 | val headingModifer = Modifier.padding(0.dp, 10.dp)
94 | val headingStyle = TextStyle(color = MaterialTheme.colorScheme.primary, fontSize = 4.em)
95 |
96 | val listModifier = baseModifier.background(MaterialTheme.colorScheme.primaryContainer)
97 | val listStyle = TextStyle(color = MaterialTheme.colorScheme.onPrimaryContainer)
98 |
99 | val refresh = @Composable {
100 | Button(
101 | onClick = {
102 | trigger++
103 | },
104 | modifier = Modifier.padding(6.dp).testTag("refresh")
105 | ) {
106 | Text(stringResource(R.string.refresh))
107 | }
108 | }
109 |
110 |
111 | val context = LocalContext.current
112 |
113 | fun doInstall(r: GitHubReleaseAsset, m: ImageInstallMethod) {
114 | if (m.needsExternalStorage) {
115 | if (!Environment.isExternalStorageManager()) {
116 | val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
117 | intent.data = Uri.parse("package:" + context.packageName)
118 | context.startActivity(intent)
119 | Toast.makeText(context, context.getString(R.string.toast_external_storage), Toast.LENGTH_LONG).show()
120 | return
121 | }
122 | }
123 |
124 | val intent = Intent(
125 | context,
126 | Install::class.java
127 | )
128 | val b = Bundle()
129 | b.putSerializable("image", r)
130 | b.putString("method", m.id)
131 | intent.putExtras(b)
132 | startActivity(context, intent, null)
133 | }
134 |
135 | Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
136 | when (val s = v) {
137 | is HomeUiValues.Loading ->
138 | @Composable {
139 | Text(stringResource(R.string.introduction_loading), modifier = baseModifier)
140 | CircularProgressIndicator(modifier = Modifier.padding(12.dp).testTag("loading_ui"))
141 | }
142 | is HomeUiValues.Error ->
143 | @Composable {
144 | Text(stringResource(R.string.introduction_error), modifier = baseModifier)
145 | refresh()
146 | }
147 | is HomeUiValues.Success -> {
148 | Text(stringResource(R.string.introduction), modifier = baseModifier.padding(0.dp, 10.dp).testTag("loaded_ui"))
149 | Text(stringResource(R.string.menu_method), modifier = headingModifer, style = headingStyle)
150 | MenuInstallMethods(modifier = listModifier, style = listStyle, selectedItem = stateMethod.value, methods = s.methods) {
151 | stateMethod.value = it
152 | }
153 | Text(stringResource(R.string.menu_version), modifier = headingModifer, style = headingStyle)
154 | MenuReleaseAsssets(modifier = listModifier, style = listStyle, selectedItem = stateVersion.value, assets = s.versions) {
155 | stateVersion.value = it
156 | }
157 | Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.padding(6.dp)) {
158 | Button(
159 | onClick = {
160 | doInstall(
161 | s.versions.filter { it.id == stateVersion.value!!.id }.getOrNull(0)!!,
162 | InstallMethods.getMethod(stateMethod.value!!.id)!!
163 | )
164 | },
165 | enabled = stateMethod.value?.real == true && stateVersion.value?.real == true,
166 | modifier = Modifier.padding(6.dp).testTag("install")
167 | ) {
168 | Text(stringResource(R.string.install))
169 | }
170 |
171 | refresh()
172 | }
173 | }
174 | }
175 | }
176 | }
177 |
178 | @Composable
179 | fun MenuInstallMethods(methods: List, modifier: Modifier, style: TextStyle, selectedItem: ExtItem?, onValueChange: (ExtItem) -> Unit) {
180 | val items = if (methods.isEmpty()) listOf(ExtItem(stringResource(R.string.no_method)))
181 | else methods.map {
182 | if (!it.isAvailable()) {
183 | ExtItem(it.id, "#DEBUG# " + stringResource(it.display))
184 | } else {
185 | ExtItem(it.id, stringResource(it.display))
186 | }
187 | }
188 |
189 | MyDropdown(modifier = modifier, items = items, style = style, selectedItem = selectedItem) { onValueChange(it) }
190 | }
191 |
192 | fun formatDateTimeLocaleAware(dateTime: LocalDateTime): String {
193 | val currentLocale = Locale.getDefault()
194 | val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
195 | .withLocale(currentLocale)
196 | return dateTime.format(formatter)
197 | }
198 |
199 | @Composable
200 | fun MenuReleaseAsssets(assets: List, modifier: Modifier, style: TextStyle, selectedItem: ExtItem?, onValueChange: (ExtItem) -> Unit) {
201 | val items = if (assets.isEmpty()) listOf(ExtItem(stringResource(R.string.no_compat)))
202 | else assets.map {
203 | val date = LocalDateTime.parse(it.updatedAt as String, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
204 | ExtItem(it.id, stringResource(R.string.format_version_info, it.version, formatDateTimeLocaleAware(date), it.arch))
205 | }
206 |
207 | MyDropdown(modifier = modifier, items = items, style = style, selectedItem = selectedItem) { onValueChange(it) }
208 | }
209 |
--------------------------------------------------------------------------------
/app/src/main/java/io/mkg20001/nixosimage/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package io.mkg20001.nixosimage.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 |
13 |
14 | val Red500 = Color(0xF44336FF)
15 | val Red50 = Color(0xFFEBEEFF)
16 | val Red100 = Color(0xFFCDD2FF)
17 | val Red200 = Color(0xEF9A9AFF)
18 | val Red300 = Color(0xE57373FF)
19 | val Red400 = Color(0xEF5350FF)
20 | val Red600 = Color(0xE53935FF)
21 | val Red700 = Color(0xD32F2FFF)
22 | val Red800 = Color(0xC62828FF)
23 | val Red900 = Color(0xB71C1CFF)
24 | val RedA100 = Color(0xFF8A80FF)
25 | val RedA200 = Color(0xFF5252FF)
26 | val RedA400 = Color(0xFF1744FF)
27 | val RedA700 = Color(0xD50000FF)
28 | val Red50010percent = Color(0x1AF44336FF)
29 | val Red50020percent = Color(0x33F44336FF)
30 | val Red50050percent = Color(0x80F44336FF)
31 | val Pink500 = Color(0xE91E63FF)
32 | val Pink50 = Color(0xFCE4ECFF)
33 | val Pink100 = Color(0xF8BBD0FF)
34 | val Pink200 = Color(0xF48FB1FF)
35 | val Pink300 = Color(0xF06292FF)
36 | val Pink400 = Color(0xEC407AFF)
37 | val Pink600 = Color(0xD81B60FF)
38 | val Pink700 = Color(0xC2185BFF)
39 | val Pink800 = Color(0xAD1457FF)
40 | val Pink900 = Color(0x880E4FFF)
41 | val PinkA100 = Color(0xFF80ABFF)
42 | val PinkA200 = Color(0xFF4081FF)
43 | val PinkA400 = Color(0xF50057FF)
44 | val PinkA700 = Color(0xC51162FF)
45 | val Pink50010percent = Color(0x1AE91E63FF)
46 | val Pink50020percent = Color(0x33E91E63FF)
47 | val Pink50050percent = Color(0x80E91E63FF)
48 | val Purple500 = Color(0x9C27B0FF)
49 | val Purple50 = Color(0xF3E5F5FF)
50 | val Purple100 = Color(0xE1BEE7FF)
51 | val Purple200 = Color(0xCE93D8FF)
52 | val Purple300 = Color(0xBA68C8FF)
53 | val Purple400 = Color(0xAB47BCFF)
54 | val Purple600 = Color(0x8E24AAFF)
55 | val Purple700 = Color(0x7B1FA2FF)
56 | val Purple800 = Color(0x6A1B9AFF)
57 | val Purple900 = Color(0x4A148CFF)
58 | val PurpleA100 = Color(0xEA80FCFF)
59 | val PurpleA200 = Color(0xE040FBFF)
60 | val PurpleA400 = Color(0xD500F9FF)
61 | val PurpleA700 = Color(0xAA00FFFF)
62 | val Purple50010percent = Color(0x1A9C27B0FF)
63 | val Purple50020percent = Color(0x339C27B0FF)
64 | val Purple50050percent = Color(0x809C27B0FF)
65 | val DeepPurple500 = Color(0x673AB7FF)
66 | val DeepPurple50 = Color(0xEDE7F6FF)
67 | val DeepPurple100 = Color(0xD1C4E9FF)
68 | val DeepPurple200 = Color(0xB39DDBFF)
69 | val DeepPurple300 = Color(0x9575CDFF)
70 | val DeepPurple400 = Color(0x7E57C2FF)
71 | val DeepPurple600 = Color(0x5E35B1FF)
72 | val DeepPurple700 = Color(0x512DA8FF)
73 | val DeepPurple800 = Color(0x4527A0FF)
74 | val DeepPurple900 = Color(0x311B92FF)
75 | val DeepPurpleA100 = Color(0xB388FFFF)
76 | val DeepPurpleA200 = Color(0x7C4DFFFF)
77 | val DeepPurpleA400 = Color(0x651FFFFF)
78 | val DeepPurpleA700 = Color(0x6200EAFF)
79 | val DeepPurple50010percent = Color(0x1A673AB7FF)
80 | val DeepPurple50020percent = Color(0x33673AB7FF)
81 | val DeepPurple50050percent = Color(0x80673AB7FF)
82 | val Indigo500 = Color(0x3F51B5FF)
83 | val Indigo50 = Color(0xE8EAF6FF)
84 | val Indigo100 = Color(0xC5CAE9FF)
85 | val Indigo200 = Color(0x9FA8DAFF)
86 | val Indigo300 = Color(0x7986CBFF)
87 | val Indigo400 = Color(0x5C6BC0FF)
88 | val Indigo600 = Color(0x3949ABFF)
89 | val Indigo700 = Color(0x303F9FFF)
90 | val Indigo800 = Color(0x283593FF)
91 | val Indigo900 = Color(0x1A237EFF)
92 | val IndigoA100 = Color(0x8C9EFFFF)
93 | val IndigoA200 = Color(0x536DFEFF)
94 | val IndigoA400 = Color(0x3D5AFEFF)
95 | val IndigoA700 = Color(0x304FFEFF)
96 | val Indigo50010percent = Color(0x1A3F51B5FF)
97 | val Indigo50020percent = Color(0x333F51B5FF)
98 | val Indigo50050percent = Color(0x803F51B5FF)
99 | val Blue500 = Color(0x2196F3FF)
100 | val Blue50 = Color(0xE3F2FDFF)
101 | val Blue100 = Color(0xBBDEFBFF)
102 | val Blue200 = Color(0x90CAF9FF)
103 | val Blue300 = Color(0x64B5F6FF)
104 | val Blue400 = Color(0x42A5F5FF)
105 | val Blue600 = Color(0x1E88E5FF)
106 | val Blue700 = Color(0x1976D2FF)
107 | val Blue800 = Color(0x1565C0FF)
108 | val Blue900 = Color(0x0D47A1FF)
109 | val BlueA100 = Color(0x82B1FFFF)
110 | val BlueA200 = Color(0x448AFFFF)
111 | val BlueA400 = Color(0x2979FFFF)
112 | val BlueA700 = Color(0x2962FFFF)
113 | val Blue50010percent = Color(0x1A2196F3FF)
114 | val Blue50020percent = Color(0x332196F3FF)
115 | val Blue50050percent = Color(0x802196F3FF)
116 | val Blue60065Percent = Color(0xA51E88E5FF)
117 | val ModifiedBlue500 = Color(0x478FCCFF)
118 | val ModifiedBlue50 = Color(0xE4F1FBFF)
119 | val ModifiedBlue100 = Color(0xBDDDF4FF)
120 | val ModifiedBlue200 = Color(0x95C7ECFF)
121 | val ModifiedBlue300 = Color(0x70B2E1FF)
122 | val ModifiedBlue400 = Color(0x569FD7FF)
123 | val ModifiedBlue600 = Color(0x3D82C4FF)
124 | val ModifiedBlue700 = Color(0x3374BAFF)
125 | val ModifiedBlue800 = Color(0x2A65B0FF)
126 | val ModifiedBlue900 = Color(0x14499EFF)
127 | val ModifiedBlueA100 = Color(0x8CAEDBFF)
128 | val ModifiedBlueA200 = Color(0x5880C1FF)
129 | val ModifiedBlueA400 = Color(0x4972B8FF)
130 | val ModifiedBlueA700 = Color(0x4666AFFF)
131 | val ModifiedBlue50010percent = Color(0x1A478FCCFF)
132 | val ModifiedBlue50020percent = Color(0x33478FCCFF)
133 | val ModifiedBlue50050percent = Color(0x80478FCCFF)
134 | val LightBlue500 = Color(0x03A9F4FF)
135 | val LightBlue50 = Color(0xE1F5FEFF)
136 | val LightBlue100 = Color(0xB3E5FCFF)
137 | val LightBlue200 = Color(0x81D4FAFF)
138 | val LightBlue300 = Color(0x4FC3F7FF)
139 | val LightBlue400 = Color(0x29B6F6FF)
140 | val LightBlue600 = Color(0x039BE5FF)
141 | val LightBlue700 = Color(0x0288D1FF)
142 | val LightBlue800 = Color(0x0277BDFF)
143 | val LightBlue900 = Color(0x01579BFF)
144 | val LightBlueA100 = Color(0x80D8FFFF)
145 | val LightBlueA200 = Color(0x40C4FFFF)
146 | val LightBlueA400 = Color(0x00B0FFFF)
147 | val LightBlueA700 = Color(0x0091EAFF)
148 | val LightBlue50010percent = Color(0x1A03A9F4FF)
149 | val LightBlue50020percent = Color(0x3303A9F4FF)
150 | val LightBlue50050percent = Color(0x8003A9F4FF)
151 | val LightBlue700100percent = Color(0xFF0288D1FF)
152 | val Cyan500 = Color(0x00BCD4FF)
153 | val Cyan50 = Color(0xE0F7FAFF)
154 | val Cyan100 = Color(0xB2EBF2FF)
155 | val Cyan200 = Color(0x80DEEAFF)
156 | val Cyan300 = Color(0x4DD0E1FF)
157 | val Cyan400 = Color(0x26C6DAFF)
158 | val Cyan600 = Color(0x00ACC1FF)
159 | val Cyan700 = Color(0x0097A7FF)
160 | val Cyan800 = Color(0x00838FFF)
161 | val Cyan900 = Color(0x006064FF)
162 | val CyanA100 = Color(0x84FFFFFF)
163 | val CyanA200 = Color(0x18FFFFFF)
164 | val CyanA400 = Color(0x00E5FFFF)
165 | val CyanA700 = Color(0x00B8D4FF)
166 | val Cyan50010percent = Color(0x1A00BCD4FF)
167 | val Cyan50020percent = Color(0x3300BCD4FF)
168 | val Cyan50050percent = Color(0x8000BCD4FF)
169 | val Teal500 = Color(0x009688FF)
170 | val Teal50 = Color(0xE0F2F1FF)
171 | val Teal100 = Color(0xB2DFDBFF)
172 | val Teal200 = Color(0x80CBC4FF)
173 | val Teal300 = Color(0x4DB6ACFF)
174 | val Teal400 = Color(0x26A69AFF)
175 | val Teal600 = Color(0x00897BFF)
176 | val Teal700 = Color(0x00796BFF)
177 | val Teal800 = Color(0x00695CFF)
178 | val Teal900 = Color(0x004D40FF)
179 | val TealA100 = Color(0xA7FFEBFF)
180 | val TealA200 = Color(0x64FFDAFF)
181 | val TealA400 = Color(0x1DE9B6FF)
182 | val TealA700 = Color(0x00BFA5FF)
183 | val Teal50010percent = Color(0x1A009688FF)
184 | val Teal50020percent = Color(0x33009688FF)
185 | val Teal50050percent = Color(0x80009688FF)
186 | val Green500 = Color(0x4CAF50FF)
187 | val Green50 = Color(0xE8F5E9FF)
188 | val Green100 = Color(0xC8E6C9FF)
189 | val Green200 = Color(0xA5D6A7FF)
190 | val Green300 = Color(0x81C784FF)
191 | val Green400 = Color(0x66BB6AFF)
192 | val Green600 = Color(0x43A047FF)
193 | val Green700 = Color(0x388E3CFF)
194 | val Green800 = Color(0x2E7D32FF)
195 | val Green900 = Color(0x1B5E20FF)
196 | val GreenA100 = Color(0xB9F6CAFF)
197 | val GreenA200 = Color(0x69F0AEFF)
198 | val GreenA400 = Color(0x00E676FF)
199 | val GreenA700 = Color(0x00C853FF)
200 | val Green50010percent = Color(0x1A4CAF50FF)
201 | val Green50020percent = Color(0x334CAF50FF)
202 | val Green50050percent = Color(0x804CAF50FF)
203 | val LightGreen500 = Color(0x8BC34AFF)
204 | val LightGreen50 = Color(0xF1F8E9FF)
205 | val LightGreen100 = Color(0xDCEDC8FF)
206 | val LightGreen200 = Color(0xC5E1A5FF)
207 | val LightGreen300 = Color(0xAED581FF)
208 | val LightGreen400 = Color(0xAED581FF)
209 | val LightGreen600 = Color(0x7CB342FF)
210 | val LightGreen700 = Color(0x689F38FF)
211 | val LightGreen800 = Color(0x558B2FFF)
212 | val LightGreen900 = Color(0x33691EFF)
213 | val LightGreenA100 = Color(0xB2FF59FF)
214 | val LightGreenA200 = Color(0xB2FF59FF)
215 | val LightGreenA400 = Color(0xB2FF59FF)
216 | val LightGreenA700 = Color(0xB2FF59FF)
217 | val LightGreen50010percent = Color(0x1A8BC34AFF)
218 | val LightGreen50020percent = Color(0x338BC34AFF)
219 | val LightGreen50050percent = Color(0x808BC34AFF)
220 | val Lime500 = Color(0xCDDC39FF)
221 | val Lime50 = Color(0xF9FBE7FF)
222 | val Lime100 = Color(0xF0F4C3FF)
223 | val Lime200 = Color(0xE6EE9CFF)
224 | val Lime300 = Color(0xDCE775FF)
225 | val Lime400 = Color(0xD4E157FF)
226 | val Lime600 = Color(0xC0CA33FF)
227 | val Lime700 = Color(0xAFB42BFF)
228 | val Lime800 = Color(0x9E9D24FF)
229 | val Lime900 = Color(0x827717FF)
230 | val LimeA100 = Color(0xF4FF81FF)
231 | val LimeA200 = Color(0xEEFF41FF)
232 | val LimeA400 = Color(0xC6FF00FF)
233 | val LimeA700 = Color(0xAEEA00FF)
234 | val Lime50010percent = Color(0x1ACDDC39FF)
235 | val Lime50020percent = Color(0x33CDDC39FF)
236 | val Lime50050percent = Color(0x80CDDC39FF)
237 | val Yellow500 = Color(0xFFEB3BFF)
238 | val Yellow50 = Color(0xFFFDE7FF)
239 | val Yellow100 = Color(0xFFF9C4FF)
240 | val Yellow200 = Color(0xFFF59DFF)
241 | val Yellow300 = Color(0xFFF176FF)
242 | val Yellow400 = Color(0xFFEE58FF)
243 | val Yellow600 = Color(0xFDD835FF)
244 | val Yellow700 = Color(0xFBC02DFF)
245 | val Yellow800 = Color(0xF9A825FF)
246 | val Yellow900 = Color(0xF57F17FF)
247 | val YellowA100 = Color(0xFFFF8DFF)
248 | val YellowA200 = Color(0xFFFF00FF)
249 | val YellowA400 = Color(0xFFEA00FF)
250 | val YellowA700 = Color(0xFFD600FF)
251 | val Yellow50010percent = Color(0x1AFFEB3BFF)
252 | val Yellow50020percent = Color(0x33FFEB3BFF)
253 | val Yellow50050percent = Color(0x80FFEB3BFF)
254 | val Amber500 = Color(0xFFC107FF)
255 | val Amber50 = Color(0xFFF8E1FF)
256 | val Amber100 = Color(0xFFECB3FF)
257 | val Amber200 = Color(0xFFE082FF)
258 | val Amber300 = Color(0xFFD54FFF)
259 | val Amber400 = Color(0xFFCA28FF)
260 | val Amber600 = Color(0xFFB300FF)
261 | val Amber700 = Color(0xFFA000FF)
262 | val Amber800 = Color(0xFF8F00FF)
263 | val Amber900 = Color(0xFF6F00FF)
264 | val AmberA100 = Color(0xFFE57FFF)
265 | val AmberA200 = Color(0xFFD740FF)
266 | val AmberA400 = Color(0xFFC400FF)
267 | val AmberA700 = Color(0xFFAB00FF)
268 | val Amber50010percent = Color(0x1AFFC107FF)
269 | val Amber50020percent = Color(0x33FFC107FF)
270 | val Amber50050percent = Color(0x80FFC107FF)
271 | val Orange500 = Color(0xFF9800FF)
272 | val Orange50 = Color(0xFFF3E0FF)
273 | val Orange100 = Color(0xFFE0B2FF)
274 | val Orange200 = Color(0xFFCC80FF)
275 | val Orange300 = Color(0xFFB74DFF)
276 | val Orange400 = Color(0xFFA726FF)
277 | val Orange600 = Color(0xFB8C00FF)
278 | val Orange700 = Color(0xF57C00FF)
279 | val Orange800 = Color(0xEF6C00FF)
280 | val Orange900 = Color(0xE65100FF)
281 | val OrangeA100 = Color(0xFFD180FF)
282 | val OrangeA200 = Color(0xFFAB40FF)
283 | val OrangeA400 = Color(0xFF9100FF)
284 | val OrangeA700 = Color(0xFF6D00FF)
285 | val Orange50010percent = Color(0x1AFF9800FF)
286 | val Orange50020percent = Color(0x33FF9800FF)
287 | val Orange50050percent = Color(0x80FF9800FF)
288 | val DeepOrange500 = Color(0xFF5722FF)
289 | val DeepOrange50 = Color(0xFBE9E7FF)
290 | val DeepOrange100 = Color(0xFFCCBCFF)
291 | val DeepOrange200 = Color(0xFFAB91FF)
292 | val DeepOrange300 = Color(0xFF8A65FF)
293 | val DeepOrange400 = Color(0xFF7043FF)
294 | val DeepOrange600 = Color(0xF4511EFF)
295 | val DeepOrange700 = Color(0xE64A19FF)
296 | val DeepOrange800 = Color(0xD84315FF)
297 | val DeepOrange900 = Color(0xBF360CFF)
298 | val DeepOrangeA100 = Color(0xFF9E80FF)
299 | val DeepOrangeA200 = Color(0xFF6E40FF)
300 | val DeepOrangeA400 = Color(0xFF3D00FF)
301 | val DeepOrangeA700 = Color(0xDD2C00FF)
302 | val DeepOrange50010percent = Color(0x1AFF5722FF)
303 | val DeepOrange50020percent = Color(0x33FF5722FF)
304 | val DeepOrange50050percent = Color(0x80FF5722FF)
305 | val Brown500 = Color(0x795548FF)
306 | val Brown50 = Color(0xEFEBE9FF)
307 | val Brown100 = Color(0xD7CCC8FF)
308 | val Brown200 = Color(0xBCAAA4FF)
309 | val Brown300 = Color(0xA1887FFF)
310 | val Brown400 = Color(0x8D6E63FF)
311 | val Brown600 = Color(0x6D4C41FF)
312 | val Brown700 = Color(0x5D4037FF)
313 | val Brown800 = Color(0x4E342EFF)
314 | val Brown900 = Color(0x3E2723FF)
315 | val Brown50010percent = Color(0x1A795548FF)
316 | val Brown50020percent = Color(0x33795548FF)
317 | val Brown50050percent = Color(0x80795548FF)
318 | val Grey500 = Color(0x9E9E9EFF)
319 | val Grey50 = Color(0xFAFAFAFF)
320 | val Grey100 = Color(0xF5F5F5FF)
321 | val Grey200 = Color(0xEEEEEEFF)
322 | val Grey300 = Color(0xE0E0E0FF)
323 | val Grey400 = Color(0xBDBDBDFF)
324 | val Grey600 = Color(0x757575FF)
325 | val Grey700 = Color(0x616161FF)
326 | val Grey800 = Color(0x424242FF)
327 | val Grey900 = Color(0x212121FF)
328 | val Grey50010percent = Color(0x1A9E9E9EFF)
329 | val Grey50020percent = Color(0x339E9E9EFF)
330 | val Grey50050percent = Color(0x809E9E9EFF)
331 | val Grey300100percent = Color(0xFFE0E0E0FF)
332 | val BlueGrey500 = Color(0x607D8BFF)
333 | val BlueGrey50 = Color(0xECEFF1FF)
334 | val BlueGrey100 = Color(0xCFD8DCFF)
335 | val BlueGrey200 = Color(0xB0BEC5FF)
336 | val BlueGrey300 = Color(0x90A4AEFF)
337 | val BlueGrey400 = Color(0x78909CFF)
338 | val BlueGrey600 = Color(0x546E7AFF)
339 | val BlueGrey700 = Color(0x455A64FF)
340 | val BlueGrey800 = Color(0x37474FFF)
341 | val BlueGrey900 = Color(0x263238FF)
342 | val BlueGrey50010percent = Color(0x1A607D8BFF)
343 | val BlueGrey50020percent = Color(0x33607D8BFF)
344 | val BlueGrey50050percent = Color(0x80607D8BFF)
345 | val BlueGrey600100percent = Color(0x546E7AFF)
346 | val black = Color(0x000000FF)
347 | val Black10percent = Color(0x19000000FF)
348 | val Black12percent = Color(0x1E000000FF)
349 | val Black15percent = Color(0x26000000FF)
350 | val Black20percent = Color(0x33000000FF)
351 | val Black26percent = Color(0x42000000FF)
352 | val Black30percent = Color(0x4D000000FF)
353 | val Black50percent = Color(0x80000000FF)
354 | val Black54percent = Color(0x89000000FF)
355 | val Black65percent = Color(0xA5000000FF)
356 | val Black87percent = Color(0xDD000000FF)
357 | val white = Color(0xFFFFFFFF)
358 | val White10percent = Color(0x19FFFFFFFF)
359 | val White11percent = Color(0x1AFFFFFFFF)
360 | val White12percent = Color(0x1EFFFFFFFF)
361 | val White15percent = Color(0x26FFFFFFFF)
362 | val White20percent = Color(0x33FFFFFFFF)
363 | val White26percent = Color(0x42FFFFFFFF)
364 | val White30percent = Color(0x4DFFFFFFFF)
365 | val White50percent = Color(0x80FFFFFFFF)
366 | val White54percent = Color(0x89FFFFFFFF)
367 | val White87percent = Color(0xDDFFFFFFFF)
--------------------------------------------------------------------------------