├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── bin │ │ ├── all │ │ │ └── websocket.sh │ │ ├── arm │ │ │ ├── busybox │ │ │ ├── dd │ │ │ ├── e2fsck │ │ │ ├── mke2fs │ │ │ ├── pkgdetails │ │ │ ├── qemu-i386-static │ │ │ └── ssl_helper │ │ ├── arm_64 │ │ │ ├── busybox │ │ │ ├── qemu-x86_64-static │ │ │ └── ssl_helper │ │ ├── x86 │ │ │ ├── busybox │ │ │ ├── dd │ │ │ ├── e2fsck │ │ │ ├── mke2fs │ │ │ ├── pkgdetails │ │ │ ├── qemu-arm-static │ │ │ └── ssl_helper │ │ └── x86_64 │ │ │ ├── busybox │ │ │ ├── qemu-aarch64-static │ │ │ └── ssl_helper │ └── web │ │ ├── cgi-bin │ │ ├── resize │ │ ├── sync │ │ └── terminal │ │ ├── css │ │ ├── style.css │ │ └── xterm.css │ │ ├── favicon.png │ │ ├── index.html │ │ ├── js │ │ ├── main.js │ │ ├── xterm-addon-fit.js │ │ └── xterm.js │ │ ├── logo.png │ │ ├── manifest.json │ │ └── terminal.html │ ├── ic_launcher-web.png │ ├── java │ └── ru │ │ └── meefik │ │ └── linuxdeploy │ │ ├── App.java │ │ ├── EnvUtils.java │ │ ├── ExecService.java │ │ ├── Logger.java │ │ ├── ParamUtils.java │ │ ├── PrefStore.java │ │ ├── PropertiesStore.java │ │ ├── RemoveEnvTask.java │ │ ├── SettingsStore.java │ │ ├── UpdateEnvTask.java │ │ ├── activity │ │ ├── AboutActivity.java │ │ ├── FullscreenActivity.java │ │ ├── MainActivity.java │ │ ├── MountsActivity.java │ │ ├── ProfilesActivity.java │ │ ├── PropertiesActivity.java │ │ ├── RepositoryActivity.java │ │ └── SettingsActivity.java │ │ ├── adapter │ │ ├── MountAdapter.java │ │ └── RepositoryProfileAdapter.java │ │ ├── fragment │ │ ├── PropertiesFragment.java │ │ └── SettingsFragment.java │ │ ├── model │ │ ├── Mount.java │ │ └── RepositoryProfile.java │ │ └── receiver │ │ ├── ActionReceiver.java │ │ ├── BootReceiver.java │ │ ├── NetworkReceiver.java │ │ └── PowerReceiver.java │ └── res │ ├── drawable │ ├── ic_add_24dp.xml │ ├── ic_close_24dp.xml │ ├── ic_computer_24dp.xml │ ├── ic_delete_24dp.xml │ ├── ic_edit_24dp.xml │ ├── ic_exit_to_app_24dp.xml │ ├── ic_help_24dp.xml │ ├── ic_play_arrow_24dp.xml │ ├── ic_refresh_24dp.xml │ ├── ic_search_24dp.xml │ ├── ic_settings_24dp.xml │ ├── ic_stop_24dp.xml │ ├── ic_tune_24dp.xml │ └── ic_warning_24dp.xml │ ├── layout-land │ ├── activity_about.xml │ └── content_main.xml │ ├── layout │ ├── activity_about.xml │ ├── activity_fullscreen.xml │ ├── activity_main.xml │ ├── activity_mounts.xml │ ├── activity_preference.xml │ ├── activity_profiles.xml │ ├── activity_repository.xml │ ├── content_main.xml │ ├── edit_text_dialog.xml │ ├── mounts_row.xml │ ├── properties_mounts.xml │ └── repository_row.xml │ ├── menu │ ├── activity_main_drawer.xml │ ├── activity_main_landscape.xml │ ├── activity_main_portrait.xml │ ├── activity_mounts.xml │ ├── activity_profiles.xml │ └── activity_repository.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── raw │ ├── alpine.png │ ├── archlinux.png │ ├── centos.png │ ├── debian.png │ ├── fedora.png │ ├── kali.png │ ├── linux.png │ ├── slackware.png │ ├── tux_about.png │ └── ubuntu.png │ ├── values-de │ ├── arrays.xml │ └── strings.xml │ ├── values-es │ ├── arrays.xml │ └── strings.xml │ ├── values-fr │ ├── arrays.xml │ └── strings.xml │ ├── values-in │ ├── arrays.xml │ └── strings.xml │ ├── values-it │ ├── arrays.xml │ └── strings.xml │ ├── values-ko │ ├── arrays.xml │ └── strings.xml │ ├── values-pl │ ├── arrays.xml │ └── strings.xml │ ├── values-pt │ ├── arrays.xml │ └── strings.xml │ ├── values-ru │ ├── arrays.xml │ └── strings.xml │ ├── values-sk │ ├── arrays.xml │ └── strings.xml │ ├── values-vi │ ├── arrays.xml │ └── strings.xml │ ├── values-zh │ ├── arrays.xml │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── attrs.xml │ ├── colors.xml │ ├── preferences.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── properties.xml │ ├── properties_fb.xml │ ├── properties_pulse.xml │ ├── properties_run_parts.xml │ ├── properties_ssh.xml │ ├── properties_sysv.xml │ ├── properties_vnc.xml │ ├── properties_x11.xml │ └── settings.xml ├── build.gradle ├── contrib ├── README.md ├── dd │ └── README.md ├── e2fsprogs │ ├── README.md │ └── ismounted.patch └── pkgdetails │ ├── README.md │ └── jni │ ├── Android.mk │ ├── Application.mk │ └── pkgdetails.c ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | gen/ 13 | out/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | release/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/ 38 | 39 | # Keystore files 40 | *.jks 41 | 42 | # Mac OS X clutter 43 | *.DS_Store 44 | 45 | # Windows clutter 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "app/src/main/assets/env"] 2 | path = app/src/main/assets/env 3 | url = https://github.com/meefik/linuxdeploy-cli 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linux Deploy 2 | 3 | Copyright (C) 2012-2019 Anton Skshidlevsky, [GPLv3](https://github.com/meefik/linuxdeploy/blob/master/LICENSE) 4 | 5 | This application is open source software for quick and easy installation of the operating system (OS) GNU/Linux on your Android device. 6 | 7 | The application creates a disk image or a directory on a flash card or uses a partition or RAM, mounts it and installs an OS distribution. Applications of the new system are run in a chroot environment and working together with the Android platform. All changes made on the device are reversible, i.e. the application and components can be removed completely. Installation of a distribution is done by downloading files from official mirrors online over the internet. The application can run better with superuser rights (root). 8 | 9 | The program supports multi language interface. You can manage the process of installing the OS, and after installation, you can start and stop services of the new system (there is support for running your scripts) through the UI. The installation process is reported as text in the main application window. During the installation, the program will adjust the environment, which includes the base system, SSH server, VNC server and desktop environment. The program interface can also manage SSH and VNC settings. 10 | 11 | Installing a new operating system takes about 15 minutes. The recommended minimum size of a disk image is 1024 MB (with LXDE), and without a GUI - 512 MB. When you install Linux on the flash card with the FAT32 file system, the image size should not exceed 4095 MB! After the initial setup the password for SSH and VNC generated automatically. The password can be changed through "Properties -> User password" or standard OS tools (passwd, vncpasswd). 12 | 13 | The app is available for download in Google Play and GitHub. 14 | 15 | Get it on Google Play 16 | Get it on Github 17 | 18 | ## Features 19 | 20 | - Bootstrap: Alpine, Arch, CentOS, Debian, Fedora, Kali, Slackware, Ubuntu, Docker or from rootfs.tar 21 | - Installation type: image file, directory, disk partition, RAM 22 | - Supported file systems: ext2, ext3, ext4 23 | - Supported architectures: arm, arm64, x86, x86_64, emulation mode (ARM ~ x86) 24 | - Control interface: CLI, SSH, VNC, X11, Framebuffer 25 | - Desktop environment: XTerm, LXDE, Xfce, MATE, other (manual configuration) 26 | - Supported languages: multilingual interface 27 | 28 | ## FAQ 29 | 30 | > Do not work update operating environment or errors appear in debug mode: "Permission denied", "Socket operation on non-socket" or other. 31 | 32 | Install compatible [BusyBox](https://github.com/meefik/busybox/releases) in /system/xbin, add path /system/xbin in "Settings -> PATH variable", update the operating environment "Settings -> Update ENV". Before upgrading the environment, it is desirable restart the device. After that, the container options must be selected "Properties -> File system -> Auto" and "Propetries -> Image size (MB) -> 2000", because "busybox mke2fs" is not supperted an option "-t" to specify type of file system and not supperted image greater 2 GB. Now you can start a new installation "Menu -> Install". 33 | 34 | > Making an image on sdcard return an error "Read-only file system". 35 | 36 | If you are using SuperSU utility you need to uncheck "mount namespace separation" in SuperSU settings. See [documentation](https://su.chainfire.eu/#how-mount). 37 | 38 | > Installing an application on Google Play fails with the message "Unknown error code during application installation: -24". 39 | 40 | You need to remove the application directory: /data/data/ru.meefik.linuxdeploy 41 | 42 | ## Performance 43 | 44 | SD card read / write speed (10 class) on Android (Samsung Galaxy S II) for file systems vfat, ext2, ext4: 45 | - **vfat**: read speed 14.1 MB/s; write speed 12.0 MB/s 46 | - **ext2**: read speed 14.9 MB/s; write speed 3.9 MB/s 47 | - **ext4**: read speed 14.9 MB/s; write speed 16.6 MB/s 48 | - **ext2 (loop)**: read speed 17.0 MB/s; write speed 7.4 MB/s 49 | - **ext4 (loop)**: read speed 17.2 MB/s; write speed 8.8 MB/s 50 | 51 | Installation time and use space on disk (Debian wheezy/armhf on Samsung Galaxy S II): 52 | - **Without GUI** ~ 0:12 / 260 MB 53 | - **XTerm** ~ 0:14 / 290 MB 54 | - **LXDE** ~ 0:19 / 450 MB 55 | - **XFCE** ~ 0:20 / 495 MB 56 | - **GNOME** ~ 0:55 / 1.3 GB 57 | - **KDE** ~ 1:20 / 1.3 GB 58 | 59 | ## Links 60 | 61 | Source code: 62 | 63 | - Linux Deploy App: 64 | - Linux Deploy CLI: 65 | 66 | Donations: 67 | 68 | - E-Money: 69 | - Google Play: 70 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion '29.0.2' 6 | 7 | defaultConfig { 8 | applicationId 'ru.meefik.linuxdeploy' 9 | minSdkVersion 21 10 | // API 28 must be frozen for binary execution 11 | targetSdkVersion 28 12 | versionCode 259 13 | versionName "2.6.0" 14 | vectorDrawables.useSupportLibrary true 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled true 19 | shrinkResources true 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | lintOptions { 24 | disable 'MissingTranslation' 25 | disable 'ExtraTranslation' 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation 'com.google.android.material:material:1.1.0' 35 | implementation 'androidx.appcompat:appcompat:1.1.0' 36 | implementation 'androidx.browser:browser:1.2.0' 37 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 38 | implementation 'com.squareup.okhttp3:okhttp:4.3.1' 39 | implementation 'androidx.preference:preference:1.1.0' 40 | } 41 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/anton/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 46 | 47 | 50 | 53 | 54 | 58 | 61 | 62 | 66 | 69 | 70 | 74 | 77 | 78 | 82 | 85 | 86 | 87 | 88 | 92 | 93 | 94 | 95 | 96 | 97 | 101 | 102 | 103 | 104 | 105 | 109 | 110 | 114 | 115 | 116 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /app/src/main/assets/bin/all/websocket.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | # websocket.sh 3 | # (C) 2016-2018 Anton Skshidlevsky , MIT 4 | # The cross platform WebSocket implementation for SH. 5 | # https://github.com/meefik/websocket.sh 6 | 7 | [ -n "$WS_SHELL" ] || WS_SHELL="sh" 8 | export LANG="C" 9 | 10 | # read pipe as hex without separating and add \x for each byte 11 | split_hex() 12 | { 13 | local hex code 14 | while read -n 2 code 15 | do 16 | if [ -n "$code" ] 17 | then 18 | hex="$hex\x$code" 19 | fi 20 | done 21 | echo -n "$hex" 22 | } 23 | 24 | # get arguments, first argument - 2 25 | get_arg() 26 | { 27 | eval "echo -n \$$1" 28 | } 29 | 30 | # check contains a byte 81 (text data flag) or 82 (binary data flag) 31 | is_packet() 32 | { 33 | echo -n "$1" | grep -q -e $(printf '\x81') -e $(printf '\x82') 34 | } 35 | 36 | # read N bytes from pipe and convert to unsigned decimal 1-byte units (space seporated) 37 | read_dec() 38 | { 39 | dd bs=$1 count=1 2>/dev/null | od -A n -t u1 -w$1 40 | } 41 | 42 | # read pipe and convert to websocket frame 43 | # see RFC6455 "Base Framing Protocol" https://tools.ietf.org/html/rfc6455 44 | ws_send() 45 | { 46 | local data length 47 | while true 48 | do 49 | # Binary frame: 0x82 [length] [data] 50 | # Max length: 00-7D -> 125; 0000-FFFF -> 65535 51 | data=$(dd bs=65535 count=1 2>/dev/null) 52 | length=$(echo -n "$data" | wc -c) 53 | # exit if received 0 bytes 54 | [ "$length" -gt 0 ] || break 55 | if [ "$length" -gt 125 ] 56 | then 57 | printf "\x82\x7E$(printf '%04x' ${length} | split_hex)" 58 | else 59 | printf "\x82\x$(printf '%02x' ${length})" 60 | fi 61 | echo -n "$data" 62 | done 63 | } 64 | 65 | # initialize websocket connection 66 | ws_connect() 67 | { 68 | local line outkey 69 | while read line 70 | do 71 | if printf "%s" "$line" | grep -q $'^\r$'; then 72 | outkey=$(printf "%s" "$sec_websocket_key" | tr '\r' '\n') 73 | outkey="${outkey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 74 | outkey=$(printf "%s" "$outkey" | sha1sum | cut -d ' ' -f 1 | printf $(split_hex) | base64) 75 | #outkey=$(printf %s "$outkey" | openssl dgst -binary -sha1 | openssl base64) 76 | printf "HTTP/1.1 101 Switching Protocols\r\n" 77 | printf "Upgrade: websocket\r\n" 78 | printf "Connection: Upgrade\r\n" 79 | printf "Sec-WebSocket-Accept: %s\r\n" "$outkey" 80 | printf "\r\n" 81 | break 82 | else 83 | case "$line" in 84 | Sec-WebSocket-Key*) 85 | sec_websocket_key=$(get_arg 3 $line) 86 | ;; 87 | esac 88 | fi 89 | done 90 | } 91 | 92 | # main loop 93 | ws_server() 94 | { 95 | local flag header length byte i 96 | while IFS= read -n 1 flag 97 | do 98 | # each packet starts at byte 81 or 82 99 | is_packet "$flag" || continue 100 | # read next 5 bytes: 101 | # 1 -> length 102 | # 2-5 -> encoding bytes 103 | header=$(read_dec 5) 104 | # get packet length 105 | let length=$(get_arg 2 $header)-128 106 | [ "$length" -gt 0 -a "$length" -le 125 ] || continue 107 | # read packet 108 | let i=0 109 | for byte in $(read_dec $length) 110 | do 111 | # decoding byte: byte ^ encoding_bytes[i % 4] 112 | let byte=byte^$(get_arg $(($i % 4 + 3)) $header) 113 | printf "\x$(printf '%02x' $byte)" 114 | let i=i+1 115 | done 116 | done | $WS_SHELL 2>&1 | ws_send 117 | } 118 | 119 | # start 120 | ws_connect && 121 | ws_server 122 | -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm/busybox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm/busybox -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm/dd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm/dd -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm/e2fsck: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm/e2fsck -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm/mke2fs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm/mke2fs -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm/pkgdetails: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm/pkgdetails -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm/qemu-i386-static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm/qemu-i386-static -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm/ssl_helper: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm/ssl_helper -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm_64/busybox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm_64/busybox -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm_64/qemu-x86_64-static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm_64/qemu-x86_64-static -------------------------------------------------------------------------------- /app/src/main/assets/bin/arm_64/ssl_helper: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/arm_64/ssl_helper -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86/busybox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86/busybox -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86/dd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86/dd -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86/e2fsck: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86/e2fsck -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86/mke2fs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86/mke2fs -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86/pkgdetails: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86/pkgdetails -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86/qemu-arm-static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86/qemu-arm-static -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86/ssl_helper: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86/ssl_helper -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86_64/busybox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86_64/busybox -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86_64/qemu-aarch64-static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86_64/qemu-aarch64-static -------------------------------------------------------------------------------- /app/src/main/assets/bin/x86_64/ssl_helper: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/bin/x86_64/ssl_helper -------------------------------------------------------------------------------- /app/src/main/assets/web/cgi-bin/resize: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | 3 | echo "Content-type: text/html" 4 | echo "" 5 | 6 | for param in ${QUERY_STRING//&/ } 7 | do 8 | key="${param%=*}" 9 | value="${param#*=}" 10 | eval ${key//[^a-zA-Z0-9_]/}=\"$value\" 11 | done 12 | 13 | if [ "$dev" -a "$rows" -a "$cols" ] 14 | then 15 | if [ -n "$refresh" ] 16 | then 17 | stty -F $dev rows $(($rows-1)) cols $(($cols-1)) 18 | fi 19 | stty -F $dev rows $rows cols $cols 20 | fi 21 | 22 | echo "" 23 | echo "" 24 | -------------------------------------------------------------------------------- /app/src/main/assets/web/cgi-bin/sync: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | 3 | echo 'Content-Type: application/octet-stream' 4 | echo 'Content-Transfer-Encoding: binary' 5 | echo 'Content-Disposition: attachment; filename="env.tgz"' 6 | echo '' 7 | 8 | tar czf - -C "$ENV_DIR" . 9 | -------------------------------------------------------------------------------- /app/src/main/assets/web/cgi-bin/terminal: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | 3 | echo "Content-type: text/html" 4 | echo "" 5 | 6 | let PORT=${HTTP_HOST##*:}+1 7 | nc -l -p ${PORT} -e websocket.sh /dev/null & 8 | cat ../terminal.html 9 | 10 | echo "" 11 | echo "" 12 | -------------------------------------------------------------------------------- /app/src/main/assets/web/css/style.css: -------------------------------------------------------------------------------- 1 | #terminal { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | margin: 0; 8 | padding: 5px; 9 | background-color: black; 10 | } 11 | 12 | /* Custom scroll bar */ 13 | 14 | ::-webkit-scrollbar { 15 | width: 10px; 16 | } 17 | 18 | /* Track */ 19 | 20 | ::-webkit-scrollbar-track { 21 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 22 | -webkit-border-radius: 10px; 23 | border-radius: 10px; 24 | } 25 | 26 | /* Handle */ 27 | 28 | ::-webkit-scrollbar-thumb { 29 | -webkit-border-radius: 10px; 30 | border-radius: 10px; 31 | background: rgba(128, 128, 128, 0.8); 32 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5); 33 | } 34 | 35 | ::-webkit-scrollbar-thumb:window-inactive { 36 | background: rgba(128, 128, 128, 0.4); 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/assets/web/css/xterm.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014 The xterm.js authors. All rights reserved. 3 | * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) 4 | * https://github.com/chjj/term.js 5 | * @license MIT 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | * Originally forked from (with the author's permission): 26 | * Fabrice Bellard's javascript vt100 for jslinux: 27 | * http://bellard.org/jslinux/ 28 | * Copyright (c) 2011 Fabrice Bellard 29 | * The original design remains. The terminal itself 30 | * has been extended to include xterm CSI codes, among 31 | * other features. 32 | */ 33 | 34 | /** 35 | * Default styles for xterm.js 36 | */ 37 | 38 | .xterm { 39 | font-feature-settings: "liga" 0; 40 | position: relative; 41 | user-select: none; 42 | -ms-user-select: none; 43 | -webkit-user-select: none; 44 | } 45 | 46 | .xterm.focus, 47 | .xterm:focus { 48 | outline: none; 49 | } 50 | 51 | .xterm .xterm-helpers { 52 | position: absolute; 53 | top: 0; 54 | /** 55 | * The z-index of the helpers must be higher than the canvases in order for 56 | * IMEs to appear on top. 57 | */ 58 | z-index: 5; 59 | } 60 | 61 | .xterm .xterm-helper-textarea { 62 | /* 63 | * HACK: to fix IE's blinking cursor 64 | * Move textarea out of the screen to the far left, so that the cursor is not visible. 65 | */ 66 | position: absolute; 67 | opacity: 0; 68 | left: -9999em; 69 | top: 0; 70 | width: 0; 71 | height: 0; 72 | z-index: -5; 73 | /** Prevent wrapping so the IME appears against the textarea at the correct position */ 74 | white-space: nowrap; 75 | overflow: hidden; 76 | resize: none; 77 | } 78 | 79 | .xterm .composition-view { 80 | /* TODO: Composition position got messed up somewhere */ 81 | background: #000; 82 | color: #FFF; 83 | display: none; 84 | position: absolute; 85 | white-space: nowrap; 86 | z-index: 1; 87 | } 88 | 89 | .xterm .composition-view.active { 90 | display: block; 91 | } 92 | 93 | .xterm .xterm-viewport { 94 | /* On OS X this is required in order for the scroll bar to appear fully opaque */ 95 | background-color: #000; 96 | overflow-y: scroll; 97 | cursor: default; 98 | position: absolute; 99 | right: 0; 100 | left: 0; 101 | top: 0; 102 | bottom: 0; 103 | } 104 | 105 | .xterm .xterm-screen { 106 | position: relative; 107 | } 108 | 109 | .xterm .xterm-screen canvas { 110 | position: absolute; 111 | left: 0; 112 | top: 0; 113 | } 114 | 115 | .xterm .xterm-scroll-area { 116 | visibility: hidden; 117 | } 118 | 119 | .xterm-char-measure-element { 120 | display: inline-block; 121 | visibility: hidden; 122 | position: absolute; 123 | top: 0; 124 | left: -9999em; 125 | line-height: normal; 126 | } 127 | 128 | .xterm { 129 | cursor: text; 130 | } 131 | 132 | .xterm.enable-mouse-events { 133 | /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ 134 | cursor: default; 135 | } 136 | 137 | .xterm.xterm-cursor-pointer { 138 | cursor: pointer; 139 | } 140 | 141 | .xterm.column-select.focus { 142 | /* Column selection mode */ 143 | cursor: crosshair; 144 | } 145 | 146 | .xterm .xterm-accessibility, 147 | .xterm .xterm-message { 148 | position: absolute; 149 | left: 0; 150 | top: 0; 151 | bottom: 0; 152 | right: 0; 153 | z-index: 10; 154 | color: transparent; 155 | } 156 | 157 | .xterm .live-region { 158 | position: absolute; 159 | left: -9999px; 160 | width: 1px; 161 | height: 1px; 162 | overflow: hidden; 163 | } 164 | 165 | .xterm-dim { 166 | opacity: 0.5; 167 | } 168 | 169 | .xterm-underline { 170 | text-decoration: underline; 171 | } 172 | -------------------------------------------------------------------------------- /app/src/main/assets/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/web/favicon.png -------------------------------------------------------------------------------- /app/src/main/assets/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Redirecting, please wait...
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/assets/web/js/main.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | 3 | function resizePty(pty, rows, cols, refresh) { 4 | if (!pty) return; 5 | var xhr = new XMLHttpRequest(); 6 | var uri = 'resize?dev=' + pty + '&rows=' + rows + '&cols=' + cols; 7 | if (refresh) uri += "&refresh=1"; 8 | xhr.open('GET', uri); 9 | xhr.send(); 10 | } 11 | 12 | function blobToText(data, callback) { 13 | var textDecoder = new TextDecoder(); 14 | var fileReader = new FileReader(); 15 | fileReader.addEventListener('load', function () { 16 | var str = textDecoder.decode(fileReader.result); 17 | callback(str); 18 | }); 19 | fileReader.readAsArrayBuffer(data); 20 | } 21 | 22 | function textToBlob(str) { 23 | return new Blob([str]); 24 | } 25 | 26 | function getQueryParams(key, qs) { 27 | qs = qs || window.location.search; 28 | qs = qs.split("+").join(" "); 29 | 30 | var params = {}; 31 | var re = /[?&]?([^=]+)=([^&]*)/g; 32 | var tokens = re.exec(qs); 33 | 34 | while (tokens) { 35 | params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]); 36 | tokens = re.exec(qs); 37 | } 38 | 39 | return key ? params[key] : params; 40 | } 41 | 42 | var pty; 43 | 44 | var protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; 45 | var port = parseInt(location.port) + 1; 46 | var socketURL = protocol + location.hostname + ((port) ? (':' + port) : ''); 47 | var socket = new WebSocket(socketURL); 48 | 49 | var terminal = new Terminal({ 50 | cursorBlink: true, 51 | fontSize: getQueryParams('size') || 16 52 | }); 53 | var fitAddon = new FitAddon.FitAddon(); 54 | terminal.loadAddon(fitAddon); 55 | terminal.open(document.getElementById('terminal')); 56 | fitAddon.fit(); 57 | 58 | socket.addEventListener('message', function (ev) { 59 | blobToText(ev.data, function (str) { 60 | if (!pty) { 61 | var match = str.match(/\/dev\/pts\/\d+/); 62 | if (match) { 63 | pty = match[0]; 64 | resizePty(pty, terminal.rows, terminal.cols); 65 | } 66 | } 67 | str = str.replace(/([^\r])\n|\r$/g, '\r\n'); 68 | terminal.write(str); 69 | }); 70 | }); 71 | 72 | terminal.onData(function (data) { 73 | socket.send(textToBlob(data)); 74 | }); 75 | 76 | terminal.onResize(function (e) { 77 | resizePty(pty, e.rows, e.cols); 78 | }); 79 | 80 | window.addEventListener('resize', function () { 81 | fitAddon.fit(); 82 | }); 83 | 84 | // Hot key for resize: Ctrl + Alt + r 85 | window.addEventListener('keydown', function (e) { 86 | if (e.ctrlKey && e.altKey && e.keyCode == 82) { 87 | resizePty(pty, terminal.rows, terminal.cols, true); 88 | } 89 | }); 90 | }; 91 | -------------------------------------------------------------------------------- /app/src/main/assets/web/js/xterm-addon-fit.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(window,function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){function e(){}return e.prototype.activate=function(e){this._terminal=e},e.prototype.dispose=function(){},e.prototype.fit=function(){var e=this.proposeDimensions();if(e&&this._terminal){var t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}},e.prototype.proposeDimensions=function(){if(this._terminal&&this._terminal.element&&this._terminal.element.parentElement){var e=this._terminal._core,t=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(t.getPropertyValue("height")),n=Math.max(0,parseInt(t.getPropertyValue("width"))),o=window.getComputedStyle(this._terminal.element),i=r-(parseInt(o.getPropertyValue("padding-top"))+parseInt(o.getPropertyValue("padding-bottom"))),a=n-(parseInt(o.getPropertyValue("padding-right"))+parseInt(o.getPropertyValue("padding-left")))-e.viewport.scrollBarWidth;return{cols:Math.max(2,Math.floor(a/e._renderService.dimensions.actualCellWidth)),rows:Math.max(1,Math.floor(i/e._renderService.dimensions.actualCellHeight))}}},e}();t.FitAddon=n}])}); 2 | //# sourceMappingURL=xterm-addon-fit.js.map -------------------------------------------------------------------------------- /app/src/main/assets/web/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/assets/web/logo.png -------------------------------------------------------------------------------- /app/src/main/assets/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Linux Deploy Terminal", 3 | "icons": [ 4 | { 5 | "src": "/logo.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | } 9 | ], 10 | "display": "standalone" 11 | } -------------------------------------------------------------------------------- /app/src/main/assets/web/terminal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Linux Deploy Terminal 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meefik/linuxdeploy/d2f1af07a2f4880eae7da0d33b79691716ff9883/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/App.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy; 2 | 3 | import android.app.Application; 4 | import android.app.NotificationChannel; 5 | import android.app.NotificationManager; 6 | import android.os.Build; 7 | 8 | public class App extends Application { 9 | 10 | public static final String SERVICE_CHANNEL_ID = "SERVICE_CHANNEL"; 11 | 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | 16 | // Create notification channels for Oreo and newer 17 | createNotificationChannels(); 18 | } 19 | 20 | private void createNotificationChannels() { 21 | // Create the NotificationChannel, but only on API 26+ because 22 | // the NotificationChannel class is new and not in the support library 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 24 | CharSequence name = getString(R.string.service_notification_channel_name); 25 | String description = getString(R.string.service_notification_channel_description); 26 | int importance = NotificationManager.IMPORTANCE_LOW; 27 | NotificationChannel channel = new NotificationChannel(SERVICE_CHANNEL_ID, name, importance); 28 | channel.setDescription(description); 29 | // Register the channel with the system; you can't change the importance 30 | // or other notification behaviors after this 31 | NotificationManager notificationManager = getSystemService(NotificationManager.class); 32 | notificationManager.createNotificationChannel(channel); 33 | } 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/ExecService.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.core.app.JobIntentService; 8 | 9 | public class ExecService extends JobIntentService { 10 | 11 | public static final int JOB_ID = 1; 12 | 13 | public static void enqueueWork(Context context, Intent work) { 14 | enqueueWork(context, ExecService.class, JOB_ID, work); 15 | } 16 | 17 | @Override 18 | protected void onHandleWork(@NonNull Intent intent) { 19 | final String cmd = intent.getStringExtra("cmd"); 20 | final String args = intent.getStringExtra("args"); 21 | Thread thread = new Thread(() -> { 22 | switch (cmd) { 23 | case "telnetd": 24 | EnvUtils.telnetd(getBaseContext(), args); 25 | break; 26 | case "httpd": 27 | EnvUtils.httpd(getBaseContext(), args); 28 | break; 29 | default: 30 | PrefStore.showNotification(getBaseContext(), null); 31 | EnvUtils.cli(getApplicationContext(), cmd, args); 32 | } 33 | }); 34 | thread.start(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/Logger.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.Closeable; 7 | import java.io.File; 8 | import java.io.FileWriter; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.text.SimpleDateFormat; 13 | import java.util.ArrayList; 14 | import java.util.Date; 15 | import java.util.List; 16 | import java.util.Locale; 17 | 18 | import ru.meefik.linuxdeploy.activity.MainActivity; 19 | 20 | public class Logger { 21 | 22 | private static volatile List protocol = new ArrayList<>(); 23 | private static char lastChar = '\n'; 24 | private static String lastLine = ""; 25 | 26 | /** 27 | * Generate timestamp 28 | * 29 | * @return timestamp 30 | */ 31 | private static String getTimeStamp() { 32 | return "[" + new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH).format(new Date()) + "] "; 33 | } 34 | 35 | /** 36 | * Append the message to protocol and show 37 | * 38 | * @param c context 39 | * @param msg message 40 | */ 41 | private static synchronized void appendMessage(Context c, final String msg) { 42 | if (msg.length() == 0) return; 43 | String out = msg; 44 | boolean timestamp = PrefStore.isTimestamp(c); 45 | int maxLines = PrefStore.getMaxLines(c); 46 | int protocolSize = protocol.size(); 47 | if (protocolSize > 0 && lastChar != '\n') { 48 | protocol.remove(protocolSize - 1); 49 | out = lastLine + out; 50 | } 51 | lastChar = out.charAt(out.length() - 1); 52 | String[] lines = out.split("\\n"); 53 | for (int i = 0, l = lines.length; i < l; i++) { 54 | lastLine = lines[i]; 55 | if (timestamp) protocol.add(getTimeStamp() + lastLine); 56 | else protocol.add(lastLine); 57 | if (protocolSize + i >= maxLines) { 58 | protocol.remove(0); 59 | } 60 | } 61 | // show protocol 62 | show(); 63 | } 64 | 65 | /** 66 | * Clear protocol 67 | * 68 | * @param c context 69 | * @return true if success 70 | */ 71 | public static boolean clear(Context c) { 72 | protocol.clear(); 73 | File logFile = new File(PrefStore.getLogFile(c)); 74 | return logFile.delete(); 75 | } 76 | 77 | /** 78 | * Size of protocol 79 | * 80 | * @return size 81 | */ 82 | public static int size() { 83 | return protocol.size(); 84 | } 85 | 86 | /** 87 | * Show log on main activity 88 | */ 89 | public static void show() { 90 | MainActivity.showLog(get()); 91 | } 92 | 93 | /** 94 | * Get protocol 95 | * 96 | * @return protocol as text 97 | */ 98 | private static String get() { 99 | return android.text.TextUtils.join("\n", protocol); 100 | } 101 | 102 | /** 103 | * Append message to protocol 104 | * 105 | * @param c context 106 | * @param msg message 107 | */ 108 | static void log(Context c, String msg) { 109 | appendMessage(c, msg); 110 | } 111 | 112 | /** 113 | * Closeable helper 114 | * 115 | * @param c closable object 116 | */ 117 | private static void close(Closeable c) { 118 | if (c != null) { 119 | try { 120 | c.close(); 121 | } catch (IOException e) { 122 | // e.printStackTrace(); 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * Append stream messages to protocol 129 | * 130 | * @param c context 131 | * @param stream stream 132 | */ 133 | static void log(Context c, InputStream stream) { 134 | FileWriter writer = null; 135 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))){ 136 | if (PrefStore.isLogger(c)) { 137 | writer = new FileWriter(PrefStore.getLogFile(c)); 138 | } 139 | int n; 140 | char[] buffer = new char[1024]; 141 | while ((n = reader.read(buffer)) != -1) { 142 | String msg = String.valueOf(buffer, 0, n); 143 | appendMessage(c, msg); 144 | if (writer != null) writer.write(msg); 145 | } 146 | } catch (IOException e) { 147 | e.printStackTrace(); 148 | } finally { 149 | close(writer); 150 | close(stream); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/ParamUtils.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.BufferedWriter; 8 | import java.io.File; 9 | import java.io.FileReader; 10 | import java.io.FileWriter; 11 | import java.io.IOException; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.TreeMap; 16 | 17 | class ParamUtils { 18 | 19 | private String name; 20 | private List params; 21 | 22 | ParamUtils(String name, String[] params) { 23 | this.name = name; 24 | this.params = Arrays.asList(params); 25 | } 26 | 27 | private static Map readConf(File confFile) { 28 | TreeMap map = new TreeMap<>(); 29 | 30 | try (BufferedReader br = new BufferedReader(new FileReader(confFile))) { 31 | String line; 32 | while ((line = br.readLine()) != null) { 33 | if (!line.startsWith("#") && !line.isEmpty()) { 34 | String[] pair = line.split("="); 35 | String key = pair[0]; 36 | String value = pair[1]; 37 | map.put(key, value.replaceAll("\"", "")); 38 | } 39 | } 40 | } catch (IOException e) { 41 | // Error! 42 | } 43 | 44 | return map; 45 | } 46 | 47 | private static boolean writeConf(Map map, File confFile) { 48 | try (BufferedWriter bw = new BufferedWriter(new FileWriter(confFile))) { 49 | for (Map.Entry entry : map.entrySet()) { 50 | String key = entry.getKey(); 51 | String value = entry.getValue(); 52 | bw.write(key + "=\"" + value + "\""); 53 | bw.newLine(); 54 | } 55 | return true; 56 | } catch (IOException e) { 57 | return false; 58 | } 59 | } 60 | 61 | public String fixOutputParam(Context c, String key, String value) { 62 | return value; 63 | } 64 | 65 | public String get(Context c, String key) { 66 | SharedPreferences pref = c.getSharedPreferences(this.name, Context.MODE_PRIVATE); 67 | int resourceId = PrefStore.getResourceId(c, key, "string"); 68 | Map source = pref.getAll(); 69 | String defaultValue = ""; 70 | if (resourceId > 0) defaultValue = c.getString(resourceId); 71 | Object value = source.get(key); 72 | if (value == null) value = defaultValue; 73 | return fixOutputParam(c, key, value.toString()); 74 | } 75 | 76 | public Map get(Context c) { 77 | SharedPreferences pref = c.getSharedPreferences(this.name, Context.MODE_PRIVATE); 78 | Map source = pref.getAll(); 79 | Map target = new TreeMap<>(); 80 | for (String key : this.params) { 81 | int resourceId = PrefStore.getResourceId(c, key, "string"); 82 | String defaultValue = ""; 83 | if (resourceId > 0) defaultValue = c.getString(resourceId); 84 | Object value = source.get(key); 85 | if (value == null) value = defaultValue; 86 | target.put(key.toUpperCase(), fixOutputParam(c, key, value.toString())); 87 | } 88 | for (Map.Entry entry : source.entrySet()) { 89 | String key = entry.getKey(); 90 | if (!key.matches("^[A-Z0-9_]+$")) continue; 91 | if (!target.containsKey(key)) target.put(key, entry.getValue().toString()); 92 | } 93 | return target; 94 | } 95 | 96 | public String fixInputParam(Context c, String key, String value) { 97 | return value; 98 | } 99 | 100 | public void set(Context c, String key, String value) { 101 | SharedPreferences pref = c.getSharedPreferences(this.name, Context.MODE_PRIVATE); 102 | SharedPreferences.Editor prefEditor = pref.edit(); 103 | if (value.equals("true") || value.equals("false")) { 104 | prefEditor.putBoolean(key, fixInputParam(c, key, value).equals("true")); 105 | } else { 106 | prefEditor.putString(key, fixInputParam(c, key, value)); 107 | } 108 | prefEditor.apply(); 109 | } 110 | 111 | public void set(Context c, Map source) { 112 | SharedPreferences pref = c.getSharedPreferences(this.name, Context.MODE_PRIVATE); 113 | SharedPreferences.Editor prefEditor = pref.edit(); 114 | for (Map.Entry entry : source.entrySet()) { 115 | String key = entry.getKey(); 116 | if (!key.matches("^[A-Z0-9_]+$")) continue; 117 | String value = entry.getValue(); 118 | String lowerKey = key.toLowerCase(); 119 | if (params.contains(lowerKey)) { 120 | if (value.equals("true") || value.equals("false")) { 121 | prefEditor.putBoolean(lowerKey, fixInputParam(c, lowerKey, value).equals("true")); 122 | } else { 123 | prefEditor.putString(lowerKey, fixInputParam(c, lowerKey, value)); 124 | } 125 | } else { 126 | prefEditor.putString(key, value); 127 | } 128 | } 129 | prefEditor.apply(); 130 | } 131 | 132 | boolean dump(Context c, File f) { 133 | return writeConf(get(c), f); 134 | } 135 | 136 | boolean restore(Context c, File f) { 137 | clear(c, false); 138 | if (f.exists()) { 139 | set(c, readConf(f)); 140 | return true; 141 | } 142 | return false; 143 | } 144 | 145 | void clear(Context c, boolean all) { 146 | SharedPreferences pref = c.getSharedPreferences(this.name, Context.MODE_PRIVATE); 147 | SharedPreferences.Editor prefEditor = pref.edit(); 148 | if (all) 149 | prefEditor.clear(); 150 | else { 151 | for (Map.Entry entry : pref.getAll().entrySet()) { 152 | String key = entry.getKey(); 153 | if (!key.matches("^[A-Z0-9_]+$")) continue; 154 | prefEditor.remove(key); 155 | } 156 | } 157 | prefEditor.apply(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/PropertiesStore.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.TreeSet; 10 | 11 | class PropertiesStore extends ParamUtils { 12 | 13 | public static final String name = "properties_conf"; 14 | private static final String[] params = {"distrib", "arch", "suite", "source_path", 15 | "target_type", "target_path", "disk_size", "fs_type", "user_name", "user_password", 16 | "privileged_users", "locale", "dns", "net_trigger", "power_trigger", "init", "init_path", "init_level", 17 | "init_user", "init_async", "ssh_port", "ssh_args", "pulse_host", "pulse_port", "graphics", 18 | "vnc_display", "vnc_depth", "vnc_dpi", "vnc_width", "vnc_height", "vnc_args", 19 | "x11_display", "x11_host", "x11_sdl", "x11_sdl_delay", "fb_display", "fb_dev", 20 | "fb_input", "fb_args", "fb_refresh", "fb_freeze", "desktop", "mounts", "include"}; 21 | 22 | PropertiesStore() { 23 | super(name, params); 24 | } 25 | 26 | @Override 27 | public String fixOutputParam(Context c, String key, String value) { 28 | switch (key) { 29 | case "user_password": 30 | if (value.isEmpty()) value = PrefStore.generatePassword(); 31 | break; 32 | case "vnc_width": 33 | if (value.isEmpty()) 34 | value = String.valueOf(Math.max(PrefStore.getScreenWidth(c), PrefStore.getScreenHeight(c))); 35 | break; 36 | case "vnc_height": 37 | if (value.isEmpty()) 38 | value = String.valueOf(Math.min(PrefStore.getScreenWidth(c), PrefStore.getScreenHeight(c))); 39 | break; 40 | case "mounts": 41 | if (!get(c, "is_mounts").equals("true")) value = ""; 42 | break; 43 | case "include": 44 | Set includes = new TreeSet<>(); 45 | includes.add("bootstrap"); 46 | if (get(c, "is_init").equals("true")) { 47 | includes.add("init"); 48 | } else { 49 | includes.remove("init"); 50 | } 51 | if (get(c, "is_ssh").equals("true")) { 52 | includes.add("extra/ssh"); 53 | } else { 54 | includes.remove("extra/ssh"); 55 | } 56 | if (get(c, "is_pulse").equals("true")) { 57 | includes.add("extra/pulse"); 58 | } else { 59 | includes.remove("extra/pulse"); 60 | } 61 | if (get(c, "is_gui").equals("true")) { 62 | includes.add("graphics"); 63 | includes.add("desktop"); 64 | } else { 65 | includes.remove("graphics"); 66 | includes.remove("desktop"); 67 | } 68 | value = TextUtils.join(" ", includes); 69 | break; 70 | } 71 | return value; 72 | } 73 | 74 | @Override 75 | public String fixInputParam(Context c, String key, String value) { 76 | if (value != null) { 77 | switch (key) { 78 | case "mounts": 79 | if (!value.isEmpty()) set(c, "is_mounts", "true"); 80 | break; 81 | case "include": 82 | List includes = Arrays.asList(value.split(" ")); 83 | if (includes.contains("init")) set(c, "is_init", "true"); 84 | if (includes.contains("extra/ssh")) set(c, "is_ssh", "true"); 85 | if (includes.contains("extra/pulse")) set(c, "is_pulse", "true"); 86 | if (includes.contains("graphics")) set(c, "is_gui", "true"); 87 | break; 88 | } 89 | } 90 | return value; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/RemoveEnvTask.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy; 2 | 3 | import android.app.ProgressDialog; 4 | import android.content.Context; 5 | import android.os.AsyncTask; 6 | 7 | import java.lang.ref.WeakReference; 8 | 9 | public class RemoveEnvTask extends AsyncTask { 10 | 11 | private ProgressDialog dialog; 12 | private WeakReference contextWeakReference; 13 | 14 | public RemoveEnvTask(Context c) { 15 | contextWeakReference = new WeakReference<>(c); 16 | } 17 | 18 | @Override 19 | protected void onPreExecute() { 20 | Context context = contextWeakReference.get(); 21 | if (context != null) { 22 | dialog = new ProgressDialog(context); 23 | dialog.setMessage(context.getString(R.string.removing_env_message)); 24 | dialog.show(); 25 | } 26 | } 27 | 28 | @Override 29 | protected Boolean doInBackground(String... params) { 30 | Context context = contextWeakReference.get(); 31 | return context != null ? EnvUtils.removeEnv(context) : null; 32 | } 33 | 34 | @Override 35 | protected void onPostExecute(Boolean success) { 36 | Context context = contextWeakReference.get(); 37 | if (context != null) { 38 | if (dialog.isShowing()) dialog.dismiss(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/SettingsStore.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy; 2 | 3 | import android.content.Context; 4 | 5 | class SettingsStore extends ParamUtils { 6 | 7 | public static final String name = "settings_conf"; 8 | private static final String[] params = {"chroot_dir", "profile"}; 9 | 10 | SettingsStore() { 11 | super(name, params); 12 | } 13 | 14 | @Override 15 | public String fixOutputParam(Context c, String key, String value) { 16 | return value; 17 | } 18 | 19 | @Override 20 | public String fixInputParam(Context c, String key, String value) { 21 | return value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/UpdateEnvTask.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy; 2 | 3 | import android.app.ProgressDialog; 4 | import android.content.Context; 5 | import android.os.AsyncTask; 6 | import android.widget.Toast; 7 | 8 | import java.lang.ref.WeakReference; 9 | 10 | public class UpdateEnvTask extends AsyncTask { 11 | 12 | private ProgressDialog dialog; 13 | private WeakReference contextWeakReference; 14 | 15 | public UpdateEnvTask(Context c) { 16 | contextWeakReference = new WeakReference<>(c); 17 | } 18 | 19 | @Override 20 | protected void onPreExecute() { 21 | Context context = contextWeakReference.get(); 22 | if (context != null) { 23 | dialog = new ProgressDialog(context); 24 | dialog.setMessage(context.getString(R.string.updating_env_message)); 25 | dialog.show(); 26 | } 27 | } 28 | 29 | @Override 30 | protected Boolean doInBackground(String... params) { 31 | Context context = contextWeakReference.get(); 32 | return context != null ? EnvUtils.updateEnv(context) : null; 33 | } 34 | 35 | @Override 36 | protected void onPostExecute(Boolean success) { 37 | Context context = contextWeakReference.get(); 38 | if (context != null) { 39 | if (dialog.isShowing()) dialog.dismiss(); 40 | if (!success) { 41 | Toast.makeText(context, R.string.toast_updating_env_error, Toast.LENGTH_SHORT).show(); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/activity/AboutActivity.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.activity; 2 | 3 | import android.os.Bundle; 4 | import android.text.method.LinkMovementMethod; 5 | import android.widget.TextView; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | import ru.meefik.linuxdeploy.PrefStore; 10 | import ru.meefik.linuxdeploy.R; 11 | 12 | public class AboutActivity extends AppCompatActivity { 13 | 14 | @Override 15 | public void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | PrefStore.setLocale(this); 18 | setContentView(R.layout.activity_about); 19 | TextView atv = findViewById(R.id.aboutTextView); 20 | atv.setMovementMethod(LinkMovementMethod.getInstance()); 21 | TextView vtv = findViewById(R.id.versionView); 22 | vtv.setText(getString(R.string.app_version, PrefStore.getVersion())); 23 | } 24 | 25 | @Override 26 | public void setTheme(int resId) { 27 | super.setTheme(PrefStore.getTheme(this)); 28 | } 29 | 30 | @Override 31 | public void onResume() { 32 | super.onResume(); 33 | setTitle(R.string.title_activity_about); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/activity/FullscreenActivity.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.activity; 2 | 3 | import android.content.pm.ActivityInfo; 4 | import android.os.Bundle; 5 | import android.view.Surface; 6 | import android.view.Window; 7 | import android.view.WindowManager; 8 | 9 | import androidx.appcompat.app.AppCompatActivity; 10 | 11 | import ru.meefik.linuxdeploy.PrefStore; 12 | import ru.meefik.linuxdeploy.R; 13 | 14 | public class FullscreenActivity extends AppCompatActivity { 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | 20 | // remove title 21 | supportRequestWindowFeature(Window.FEATURE_NO_TITLE); 22 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 23 | WindowManager.LayoutParams.FLAG_FULLSCREEN); 24 | 25 | setContentView(R.layout.activity_fullscreen); 26 | } 27 | 28 | @Override 29 | public void onResume() { 30 | super.onResume(); 31 | 32 | // Screen lock 33 | if (PrefStore.isScreenLock(this)) 34 | this.getWindow().addFlags( 35 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 36 | else this.getWindow().clearFlags( 37 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 38 | 39 | // Set screen orientation (freeze) 40 | int rotation = getWindowManager().getDefaultDisplay().getRotation(); 41 | switch (rotation) { 42 | case Surface.ROTATION_180: 43 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); 44 | break; 45 | case Surface.ROTATION_270: 46 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); 47 | break; 48 | case Surface.ROTATION_0: 49 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 50 | break; 51 | case Surface.ROTATION_90: 52 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 53 | break; 54 | } 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/activity/MountsActivity.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.activity; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | import android.view.View; 8 | import android.widget.EditText; 9 | 10 | import androidx.appcompat.app.AlertDialog; 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import androidx.recyclerview.widget.DividerItemDecoration; 13 | import androidx.recyclerview.widget.LinearLayoutManager; 14 | import androidx.recyclerview.widget.RecyclerView; 15 | 16 | import ru.meefik.linuxdeploy.PrefStore; 17 | import ru.meefik.linuxdeploy.R; 18 | import ru.meefik.linuxdeploy.adapter.MountAdapter; 19 | import ru.meefik.linuxdeploy.model.Mount; 20 | 21 | public class MountsActivity extends AppCompatActivity { 22 | 23 | private MountAdapter adapter; 24 | 25 | private void addDialog() { 26 | View view = LayoutInflater.from(this).inflate(R.layout.properties_mounts, null); 27 | EditText inputSrc = view.findViewById(R.id.editTextSrc); 28 | EditText inputTarget = view.findViewById(R.id.editTextTarget); 29 | 30 | new AlertDialog.Builder(this) 31 | .setTitle(R.string.new_mount_title) 32 | .setView(view) 33 | .setPositiveButton(android.R.string.ok, 34 | (dialog, whichButton) -> { 35 | String src = inputSrc.getText().toString() 36 | .replaceAll("[ :]", "_"); 37 | String target = inputTarget.getText().toString() 38 | .replaceAll("[ :]", "_"); 39 | if (!src.isEmpty()) { 40 | adapter.addMount(new Mount(src, target)); 41 | } 42 | }) 43 | .setNegativeButton(android.R.string.cancel, 44 | (dialog, whichButton) -> dialog.cancel()).show(); 45 | } 46 | 47 | private void editDialog(Mount mount) { 48 | View view = LayoutInflater.from(this).inflate(R.layout.properties_mounts, null); 49 | EditText inputSrc = view.findViewById(R.id.editTextSrc); 50 | EditText inputTarget = view.findViewById(R.id.editTextTarget); 51 | 52 | inputSrc.setText(mount.getSource()); 53 | inputSrc.setSelection(mount.getSource().length()); 54 | 55 | inputTarget.setText(mount.getTarget()); 56 | inputTarget.setSelection(mount.getTarget().length()); 57 | 58 | new AlertDialog.Builder(this) 59 | .setTitle(R.string.edit_mount_title) 60 | .setView(view) 61 | .setPositiveButton(android.R.string.ok, 62 | (dialog, whichButton) -> { 63 | String src = inputSrc.getText().toString() 64 | .replaceAll("[ :]", "_"); 65 | String target = inputTarget.getText().toString() 66 | .replaceAll("[ :]", "_"); 67 | if (!src.isEmpty()) { 68 | mount.setSource(src); 69 | mount.setTarget(target); 70 | adapter.notifyDataSetChanged(); 71 | } 72 | }) 73 | .setNegativeButton(android.R.string.cancel, 74 | (dialog, whichButton) -> dialog.cancel()) 75 | .show(); 76 | } 77 | 78 | private void deleteDialog(Mount mount) { 79 | new AlertDialog.Builder(this) 80 | .setTitle(R.string.confirm_mount_discard_title) 81 | .setMessage(R.string.confirm_mount_discard_message) 82 | .setIcon(R.drawable.ic_warning_24dp) 83 | .setPositiveButton(android.R.string.yes, 84 | (dialog, whichButton) -> adapter.removeMount(mount)) 85 | .setNegativeButton(android.R.string.no, 86 | (dialog, whichButton) -> dialog.cancel()) 87 | .show(); 88 | } 89 | 90 | @Override 91 | protected void onCreate(Bundle savedInstanceState) { 92 | super.onCreate(savedInstanceState); 93 | PrefStore.setLocale(this); 94 | setContentView(R.layout.activity_mounts); 95 | 96 | // RecyclerView Adapter 97 | RecyclerView recyclerView = findViewById(R.id.recycler_view); 98 | adapter = new MountAdapter(); 99 | adapter.setOnItemClickListener(this::editDialog); 100 | adapter.setOnItemDeleteListener(this::deleteDialog); 101 | 102 | recyclerView.setAdapter(adapter); 103 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 104 | recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); 105 | } 106 | 107 | @Override 108 | public void setTheme(int resId) { 109 | super.setTheme(PrefStore.getTheme(this)); 110 | } 111 | 112 | @Override 113 | public boolean onCreateOptionsMenu(Menu menu) { 114 | PrefStore.setLocale(this); 115 | getMenuInflater().inflate(R.menu.activity_mounts, menu); 116 | return super.onCreateOptionsMenu(menu); 117 | } 118 | 119 | @Override 120 | public boolean onOptionsItemSelected(MenuItem item) { 121 | if (item.getItemId() == R.id.menu_add) { 122 | addDialog(); 123 | return true; 124 | } 125 | 126 | return false; 127 | } 128 | 129 | @Override 130 | public void onResume() { 131 | super.onResume(); 132 | 133 | String titleMsg = getString(R.string.title_activity_mounts) + ": " 134 | + PrefStore.getProfileName(this); 135 | setTitle(titleMsg); 136 | 137 | adapter.setMounts(PrefStore.getMountsList(this)); 138 | } 139 | 140 | @Override 141 | public void onPause() { 142 | super.onPause(); 143 | 144 | PrefStore.setMountsList(this, adapter.getMounts()); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/activity/ProfilesActivity.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.activity; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.view.GestureDetector; 6 | import android.view.LayoutInflater; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.view.MotionEvent; 10 | import android.view.View; 11 | import android.view.View.OnTouchListener; 12 | import android.widget.ArrayAdapter; 13 | import android.widget.EditText; 14 | import android.widget.ListView; 15 | 16 | import androidx.appcompat.app.AlertDialog; 17 | import androidx.appcompat.app.AppCompatActivity; 18 | 19 | import java.io.File; 20 | import java.util.ArrayList; 21 | import java.util.Collections; 22 | import java.util.List; 23 | 24 | import ru.meefik.linuxdeploy.PrefStore; 25 | import ru.meefik.linuxdeploy.R; 26 | 27 | public class ProfilesActivity extends AppCompatActivity implements OnTouchListener { 28 | 29 | private ListView listView; 30 | private List listItems = new ArrayList<>(); 31 | private ArrayAdapter adapter; 32 | private GestureDetector gd; 33 | 34 | /** 35 | * Rename conf file associated with the profile 36 | * 37 | * @param c context 38 | * @param oldName old profile name 39 | * @param newName new profile name 40 | * @return true if success 41 | */ 42 | public static boolean renameConf(Context c, String oldName, String newName) { 43 | File oldFile = new File(PrefStore.getEnvDir(c) + "/config/" + oldName + ".conf"); 44 | File newFile = new File(PrefStore.getEnvDir(c) + "/config/" + newName + ".conf"); 45 | return oldFile.renameTo(newFile); 46 | } 47 | 48 | /** 49 | * Remove conf file associated with the profile 50 | * 51 | * @param c context 52 | * @param name profile name 53 | * @return true if success 54 | */ 55 | public static boolean removeConf(Context c, String name) { 56 | File confFile = new File(PrefStore.getEnvDir(c) + "/config/" + name + ".conf"); 57 | return confFile.exists() && confFile.delete(); 58 | } 59 | 60 | /** 61 | * Get list of profiles 62 | * 63 | * @param c context 64 | * @return list of profiles 65 | */ 66 | public static List getProfiles(Context c) { 67 | List profiles = new ArrayList<>(); 68 | File confDir = new File(PrefStore.getEnvDir(c) + "/config"); 69 | File[] profileFiles = confDir.listFiles(); 70 | 71 | if (profileFiles != null) { 72 | for (File profileFile : profileFiles) { 73 | if (profileFile.isFile()) { 74 | String filename = profileFile.getName(); 75 | int index = filename.lastIndexOf('.'); 76 | if (index != -1) filename = filename.substring(0, index); 77 | profiles.add(filename); 78 | } 79 | } 80 | } 81 | 82 | return profiles; 83 | } 84 | 85 | /** 86 | * Get position by key 87 | * 88 | * @param key 89 | * @return position 90 | */ 91 | private int getPosition(String key) { 92 | for (int i = 0; i < listItems.size(); i++) { 93 | if (listItems.get(i).equals(key)) 94 | return i; 95 | } 96 | 97 | return -1; 98 | } 99 | 100 | private void addDialog() { 101 | View view = LayoutInflater.from(this).inflate(R.layout.edit_text_dialog, null); 102 | EditText input = view.findViewById(R.id.edit_text); 103 | 104 | new AlertDialog.Builder(this) 105 | .setTitle(R.string.new_profile_title) 106 | .setView(view) 107 | .setPositiveButton(android.R.string.ok, 108 | (dialog, whichButton) -> { 109 | String text = input.getText().toString(); 110 | if (!text.isEmpty()) { 111 | listItems.add(text.replaceAll("[^A-Za-z0-9_\\-]", "_")); 112 | adapter.notifyDataSetChanged(); 113 | } 114 | }) 115 | .setNegativeButton(android.R.string.cancel, 116 | (dialog, whichButton) -> dialog.cancel()) 117 | .show(); 118 | } 119 | 120 | private void editDialog() { 121 | int pos = listView.getCheckedItemPosition(); 122 | if (pos >= 0 && pos < listItems.size()) { 123 | String profileOld = listItems.get(pos); 124 | 125 | View view = LayoutInflater.from(this).inflate(R.layout.edit_text_dialog, null); 126 | EditText input = view.findViewById(R.id.edit_text); 127 | input.setText(profileOld); 128 | input.setSelection(input.getText().length()); 129 | 130 | new AlertDialog.Builder(this) 131 | .setTitle(R.string.edit_profile_title) 132 | .setView(view) 133 | .setPositiveButton(android.R.string.ok, 134 | (dialog, whichButton) -> { 135 | String text = input.getText().toString(); 136 | if (!text.isEmpty()) { 137 | String profileNew = text.replaceAll("[^A-Za-z0-9_\\-]", "_"); 138 | if (!profileOld.equals(profileNew)) { 139 | renameConf(getApplicationContext(), profileOld, profileNew); 140 | listItems.set(pos, profileNew); 141 | adapter.notifyDataSetChanged(); 142 | } 143 | } 144 | }) 145 | .setNegativeButton(android.R.string.cancel, 146 | (dialog, whichButton) -> dialog.cancel()) 147 | .show(); 148 | } 149 | } 150 | 151 | private void deleteDialog() { 152 | final int pos = listView.getCheckedItemPosition(); 153 | if (pos >= 0 && pos < listItems.size()) { 154 | new AlertDialog.Builder(this) 155 | .setTitle(R.string.confirm_profile_discard_title) 156 | .setMessage(R.string.confirm_profile_discard_message) 157 | .setIcon(R.drawable.ic_warning_24dp) 158 | .setPositiveButton(android.R.string.yes, 159 | (dialog, whichButton) -> { 160 | String key = listItems.remove(pos); 161 | int last = listItems.size() - 1; 162 | if (last < 0) listItems.add(getString(R.string.profile)); 163 | if (last >= 0 && pos > last) 164 | listView.setItemChecked(last, true); 165 | adapter.notifyDataSetChanged(); 166 | removeConf(getApplicationContext(), key); 167 | }) 168 | .setNegativeButton(android.R.string.no, 169 | (dialog, whichButton) -> dialog.cancel()) 170 | .show(); 171 | } 172 | } 173 | 174 | @Override 175 | public void onCreate(Bundle savedInstanceState) { 176 | super.onCreate(savedInstanceState); 177 | PrefStore.setLocale(this); 178 | setContentView(R.layout.activity_profiles); 179 | 180 | // ListView Adapter 181 | listView = findViewById(R.id.profilesView); 182 | adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_single_choice, listItems); 183 | listView.setAdapter(adapter); 184 | 185 | // Initialize the Gesture Detector 186 | listView.setOnTouchListener(this); 187 | gd = new GestureDetector(this, 188 | new GestureDetector.SimpleOnGestureListener() { 189 | @Override 190 | public boolean onDoubleTap(MotionEvent e) { 191 | finish(); 192 | return false; 193 | } 194 | }); 195 | } 196 | 197 | @Override 198 | public void setTheme(int resId) { 199 | super.setTheme(PrefStore.getTheme(this)); 200 | } 201 | 202 | @Override 203 | public boolean onCreateOptionsMenu(Menu menu) { 204 | PrefStore.setLocale(this); 205 | getMenuInflater().inflate(R.menu.activity_profiles, menu); 206 | return super.onCreateOptionsMenu(menu); 207 | } 208 | 209 | @Override 210 | public boolean onOptionsItemSelected(MenuItem item) { 211 | switch (item.getItemId()) { 212 | case R.id.menu_add: 213 | addDialog(); 214 | break; 215 | case R.id.menu_edit: 216 | editDialog(); 217 | break; 218 | case R.id.menu_delete: 219 | deleteDialog(); 220 | break; 221 | default: 222 | return super.onOptionsItemSelected(item); 223 | } 224 | 225 | return true; 226 | } 227 | 228 | @Override 229 | public void onPause() { 230 | super.onPause(); 231 | 232 | int pos = listView.getCheckedItemPosition(); 233 | if (pos >= 0 && pos < listItems.size()) { 234 | String profile = listItems.get(pos); 235 | PrefStore.changeProfile(this, profile); 236 | } 237 | } 238 | 239 | @Override 240 | public void onResume() { 241 | super.onResume(); 242 | setTitle(R.string.title_activity_profiles); 243 | listItems.clear(); 244 | listItems.addAll(getProfiles(this)); 245 | Collections.sort(listItems); 246 | String profile = PrefStore.getProfileName(this); 247 | if (listItems.size() == 0) listItems.add(profile); 248 | adapter.notifyDataSetChanged(); 249 | listView.setItemChecked(getPosition(profile), true); 250 | } 251 | 252 | @Override 253 | public boolean onTouch(View v, MotionEvent event) { 254 | gd.onTouchEvent(event); 255 | return false; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/activity/PropertiesActivity.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.activity; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.annotation.Nullable; 6 | import androidx.appcompat.app.AppCompatActivity; 7 | 8 | import ru.meefik.linuxdeploy.PrefStore; 9 | import ru.meefik.linuxdeploy.R; 10 | import ru.meefik.linuxdeploy.fragment.PropertiesFragment; 11 | 12 | public class PropertiesActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(@Nullable Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | PrefStore.setLocale(this); 18 | setContentView(R.layout.activity_preference); 19 | 20 | getSupportFragmentManager() 21 | .beginTransaction() 22 | .replace(R.id.frame_layout, new PropertiesFragment()) 23 | .commit(); 24 | 25 | // Restore from conf file if open from main activity 26 | if (getIntent().getBooleanExtra("restore", false)) { 27 | PrefStore.restoreProperties(this); 28 | } 29 | } 30 | 31 | @Override 32 | public void setTheme(int resId) { 33 | super.setTheme(PrefStore.getTheme(this)); 34 | } 35 | 36 | @Override 37 | protected void onResume() { 38 | super.onResume(); 39 | 40 | String titleMsg = getString(R.string.title_activity_properties) 41 | + ": " + PrefStore.getProfileName(this); 42 | setTitle(titleMsg); 43 | } 44 | 45 | @Override 46 | protected void onPause() { 47 | super.onPause(); 48 | 49 | // Update configuration file 50 | PrefStore.dumpProperties(this); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/activity/RepositoryActivity.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.activity; 2 | 3 | import android.app.ProgressDialog; 4 | import android.content.Intent; 5 | import android.content.pm.PackageManager; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.view.LayoutInflater; 9 | import android.view.Menu; 10 | import android.view.MenuItem; 11 | import android.view.View; 12 | import android.widget.EditText; 13 | import android.widget.Toast; 14 | 15 | import androidx.appcompat.app.AlertDialog; 16 | import androidx.appcompat.app.AppCompatActivity; 17 | import androidx.recyclerview.widget.DividerItemDecoration; 18 | import androidx.recyclerview.widget.LinearLayoutManager; 19 | import androidx.recyclerview.widget.RecyclerView; 20 | 21 | import org.jetbrains.annotations.NotNull; 22 | 23 | import java.io.BufferedReader; 24 | import java.io.FileOutputStream; 25 | import java.io.IOException; 26 | import java.io.InputStreamReader; 27 | import java.io.OutputStream; 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | import java.util.zip.GZIPInputStream; 31 | 32 | import okhttp3.Call; 33 | import okhttp3.Callback; 34 | import okhttp3.OkHttpClient; 35 | import okhttp3.Request; 36 | import okhttp3.Response; 37 | import ru.meefik.linuxdeploy.PrefStore; 38 | import ru.meefik.linuxdeploy.R; 39 | import ru.meefik.linuxdeploy.adapter.RepositoryProfileAdapter; 40 | import ru.meefik.linuxdeploy.model.RepositoryProfile; 41 | 42 | public class RepositoryActivity extends AppCompatActivity { 43 | 44 | private RepositoryProfileAdapter adapter; 45 | 46 | private boolean isDonated() { 47 | return getPackageManager().checkSignatures(getPackageName(), "ru.meefik.donate") 48 | == PackageManager.SIGNATURE_MATCH; 49 | } 50 | 51 | private void importDialog(final RepositoryProfile repositoryProfile) { 52 | final String name = repositoryProfile.getProfile(); 53 | final String message = getString(R.string.repository_import_message, 54 | repositoryProfile.getDescription(), 55 | repositoryProfile.getSize()); 56 | 57 | AlertDialog.Builder dialog = new AlertDialog.Builder(this) 58 | .setTitle(name) 59 | .setMessage(message) 60 | .setCancelable(false) 61 | .setNegativeButton(android.R.string.no, (dialog13, which) -> dialog13.cancel()); 62 | 63 | if (isDonated()) { 64 | dialog.setPositiveButton(R.string.repository_import_button, 65 | (dialog1, whichButton) -> importProfile(name)); 66 | } else { 67 | dialog.setPositiveButton(R.string.repository_purchase_button, 68 | (dialog12, whichButton) -> startActivity(new Intent(Intent.ACTION_VIEW, 69 | Uri.parse("https://play.google.com/store/apps/details?id=ru.meefik.donate")))); 70 | } 71 | 72 | dialog.show(); 73 | } 74 | 75 | private void changeUrlDialog() { 76 | View view = LayoutInflater.from(this).inflate(R.layout.edit_text_dialog, null); 77 | EditText input = view.findViewById(R.id.edit_text); 78 | input.setText(PrefStore.getRepositoryUrl(this)); 79 | input.setSelection(input.getText().length()); 80 | 81 | new AlertDialog.Builder(this) 82 | .setTitle(R.string.repository_change_url_title) 83 | .setView(view) 84 | .setPositiveButton(android.R.string.ok, 85 | (dialog, whichButton) -> { 86 | String text = input.getText().toString(); 87 | if (text.isEmpty()) 88 | text = getString(R.string.repository_url); 89 | PrefStore.setRepositoryUrl(getApplicationContext(), text); 90 | retrieveIndex(); 91 | }) 92 | .setNegativeButton(android.R.string.cancel, 93 | (dialog, whichButton) -> dialog.cancel()) 94 | .show(); 95 | } 96 | 97 | private void retrieveIndex() { 98 | String url = PrefStore.getRepositoryUrl(this); 99 | 100 | OkHttpClient client = new OkHttpClient.Builder() 101 | .followRedirects(true) 102 | .build(); 103 | Request request = new Request.Builder() 104 | .url(url + "/index.gz") 105 | .build(); 106 | 107 | ProgressDialog dialog = new ProgressDialog(this); 108 | dialog.setMessage(getString(R.string.loading_message)); 109 | dialog.setCancelable(false); 110 | dialog.show(); 111 | 112 | client.newCall(request).enqueue(new Callback() { 113 | @Override 114 | public void onFailure(@NotNull Call call, @NotNull IOException e) { 115 | onFailure(); 116 | } 117 | 118 | @Override 119 | public void onResponse(@NotNull Call call, @NotNull Response response) { 120 | if (response.isSuccessful()) { 121 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(response.body().byteStream())))) { 122 | List repositoryProfiles = new ArrayList<>(); 123 | String line; 124 | RepositoryProfile repositoryProfile = null; 125 | while ((line = reader.readLine()) != null) { 126 | if (line.startsWith("PROFILE")) { 127 | repositoryProfile = new RepositoryProfile(); 128 | repositoryProfile.setProfile(line.split("=")[1]); 129 | } else if (line.startsWith("DESC")) { 130 | repositoryProfile.setDescription(line.split("=")[1]); 131 | } else if (line.startsWith("TYPE")) { 132 | repositoryProfile.setType(line.split("=")[1]); 133 | } else if (line.startsWith("SIZE")) { 134 | repositoryProfile.setSize(line.split("=")[1]); 135 | repositoryProfiles.add(repositoryProfile); 136 | } 137 | } 138 | 139 | runOnUiThread(() -> { 140 | adapter.setRepositoryProfiles(repositoryProfiles); 141 | dialog.dismiss(); 142 | }); 143 | } catch (IOException e) { 144 | onFailure(); 145 | } 146 | } else { 147 | onFailure(); 148 | } 149 | } 150 | 151 | private void onFailure() { 152 | runOnUiThread(() -> { 153 | dialog.dismiss(); 154 | Toast.makeText(RepositoryActivity.this, R.string.toast_loading_error, Toast.LENGTH_SHORT).show(); 155 | }); 156 | } 157 | }); 158 | } 159 | 160 | private void importProfile(String name) { 161 | String url = PrefStore.getRepositoryUrl(this); 162 | 163 | OkHttpClient client = new OkHttpClient.Builder() 164 | .followRedirects(true) 165 | .build(); 166 | Request request = new Request.Builder() 167 | .url(url + "/index.gz") 168 | .build(); 169 | 170 | ProgressDialog dialog = new ProgressDialog(this); 171 | dialog.setMessage(getString(R.string.loading_message)); 172 | dialog.setCancelable(false); 173 | dialog.show(); 174 | 175 | client.newCall(request).enqueue(new Callback() { 176 | @Override 177 | public void onFailure(@NotNull Call call, @NotNull IOException e) { 178 | onFailure(); 179 | } 180 | 181 | @Override 182 | public void onResponse(@NotNull Call call, @NotNull Response response) { 183 | if (response.isSuccessful()) { 184 | String conf = PrefStore.getEnvDir(RepositoryActivity.this) + "/config/" + name + ".conf"; 185 | try (OutputStream os = new FileOutputStream(conf)) { 186 | os.write(response.body().bytes()); 187 | 188 | runOnUiThread(dialog::dismiss); 189 | PrefStore.changeProfile(RepositoryActivity.this, name); 190 | finish(); 191 | } catch (IOException e) { 192 | onFailure(); 193 | } 194 | } else { 195 | onFailure(); 196 | } 197 | } 198 | 199 | private void onFailure() { 200 | runOnUiThread(() -> { 201 | dialog.dismiss(); 202 | Toast.makeText(RepositoryActivity.this, R.string.toast_loading_error, Toast.LENGTH_SHORT).show(); 203 | }); 204 | } 205 | }); 206 | } 207 | 208 | @Override 209 | protected void onCreate(Bundle savedInstanceState) { 210 | super.onCreate(savedInstanceState); 211 | PrefStore.setLocale(this); 212 | setContentView(R.layout.activity_repository); 213 | 214 | // RecyclerView Adapter 215 | RecyclerView recyclerView = findViewById(R.id.repositoryView); 216 | adapter = new RepositoryProfileAdapter(); 217 | adapter.setOnItemClickListener(this::importDialog); 218 | recyclerView.setAdapter(adapter); 219 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 220 | recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); 221 | 222 | // Load list 223 | retrieveIndex(); 224 | } 225 | 226 | @Override 227 | public void setTheme(int resId) { 228 | super.setTheme(PrefStore.getTheme(this)); 229 | } 230 | 231 | @Override 232 | public void onResume() { 233 | super.onResume(); 234 | setTitle(R.string.title_activity_repository); 235 | } 236 | 237 | @Override 238 | public boolean onCreateOptionsMenu(Menu menu) { 239 | PrefStore.setLocale(this); 240 | getMenuInflater().inflate(R.menu.activity_repository, menu); 241 | return super.onCreateOptionsMenu(menu); 242 | } 243 | 244 | @Override 245 | public boolean onOptionsItemSelected(MenuItem item) { 246 | switch (item.getItemId()) { 247 | case R.id.menu_refresh: 248 | retrieveIndex(); 249 | break; 250 | case R.id.menu_change_url: 251 | changeUrlDialog(); 252 | break; 253 | default: 254 | return super.onOptionsItemSelected(item); 255 | } 256 | 257 | return true; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/activity/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.activity; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.appcompat.app.AppCompatActivity; 6 | 7 | import ru.meefik.linuxdeploy.PrefStore; 8 | import ru.meefik.linuxdeploy.R; 9 | import ru.meefik.linuxdeploy.fragment.SettingsFragment; 10 | 11 | public class SettingsActivity extends AppCompatActivity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | PrefStore.setLocale(this); 17 | setContentView(R.layout.activity_preference); 18 | 19 | getSupportFragmentManager() 20 | .beginTransaction() 21 | .replace(R.id.frame_layout, new SettingsFragment()) 22 | .commit(); 23 | 24 | // Restore from conf file 25 | PrefStore.restoreSettings(this); 26 | } 27 | 28 | @Override 29 | public void setTheme(int resId) { 30 | super.setTheme(PrefStore.getTheme(this)); 31 | } 32 | 33 | @Override 34 | public void onResume() { 35 | super.onResume(); 36 | 37 | setTitle(R.string.title_activity_settings); 38 | } 39 | 40 | @Override 41 | public void onPause() { 42 | super.onPause(); 43 | 44 | // update configuration file 45 | PrefStore.dumpSettings(this); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/adapter/MountAdapter.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.adapter; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.ImageView; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.recyclerview.widget.RecyclerView; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import ru.meefik.linuxdeploy.R; 16 | import ru.meefik.linuxdeploy.model.Mount; 17 | 18 | public class MountAdapter extends RecyclerView.Adapter { 19 | 20 | private List mounts; 21 | private OnItemClickListener clickListener; 22 | private OnItemDeleteListener deleteListener; 23 | 24 | public MountAdapter() { 25 | this.mounts = new ArrayList<>(); 26 | } 27 | 28 | @NonNull 29 | @Override 30 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 31 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.mounts_row, parent, false); 32 | return new ViewHolder(view); 33 | } 34 | 35 | @Override 36 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 37 | holder.setMount(mounts.get(position)); 38 | } 39 | 40 | @Override 41 | public int getItemCount() { 42 | return mounts == null ? 0 : mounts.size(); 43 | } 44 | 45 | public void addMount(Mount mount) { 46 | mounts.add(mount); 47 | notifyDataSetChanged(); 48 | } 49 | 50 | public void removeMount(Mount mount) { 51 | mounts.remove(mount); 52 | notifyDataSetChanged(); 53 | } 54 | 55 | public void setMounts(List mounts) { 56 | this.mounts.clear(); 57 | for (String mount : mounts) { 58 | String[] tmp = mount.split(":", 2); 59 | if (tmp.length > 1) { 60 | this.mounts.add(new Mount(tmp[0], tmp[1])); 61 | } else { 62 | this.mounts.add(new Mount(tmp[0], "")); 63 | } 64 | } 65 | notifyDataSetChanged(); 66 | } 67 | 68 | public List getMounts() { 69 | List mounts = new ArrayList<>(); 70 | for (Mount mount : this.mounts) { 71 | if (mount.getTarget().isEmpty()) { 72 | mounts.add(mount.getSource()); 73 | } else { 74 | mounts.add(mount.getSource() + ":" + mount.getTarget()); 75 | } 76 | } 77 | return mounts; 78 | } 79 | 80 | public void setOnItemClickListener(OnItemClickListener clickListener) { 81 | this.clickListener = clickListener; 82 | } 83 | 84 | public void setOnItemDeleteListener(OnItemDeleteListener deleteListener) { 85 | this.deleteListener = deleteListener; 86 | } 87 | 88 | public interface OnItemClickListener { 89 | void onItemClick(Mount mount); 90 | } 91 | 92 | public interface OnItemDeleteListener { 93 | void onItemDelete(Mount mount); 94 | } 95 | 96 | class ViewHolder extends RecyclerView.ViewHolder { 97 | 98 | private View view; 99 | private TextView mountPoint; 100 | private ImageView delete; 101 | 102 | ViewHolder(@NonNull View itemView) { 103 | super(itemView); 104 | 105 | view = itemView; 106 | mountPoint = itemView.findViewById(R.id.mount_point); 107 | delete = itemView.findViewById(R.id.delete_mount); 108 | } 109 | 110 | void setMount(Mount mount) { 111 | if (mount.getTarget().isEmpty()) { 112 | mountPoint.setText(mount.getSource()); 113 | } else { 114 | mountPoint.setText(mount.getSource() + " - " + mount.getTarget()); 115 | } 116 | 117 | view.setOnClickListener(v -> { 118 | if (clickListener != null) 119 | clickListener.onItemClick(mount); 120 | }); 121 | 122 | delete.setOnClickListener(v -> { 123 | if (deleteListener != null) 124 | deleteListener.onItemDelete(mount); 125 | }); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/adapter/RepositoryProfileAdapter.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.adapter; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.ImageView; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.recyclerview.widget.RecyclerView; 11 | 12 | import java.util.List; 13 | 14 | import ru.meefik.linuxdeploy.R; 15 | import ru.meefik.linuxdeploy.model.RepositoryProfile; 16 | 17 | public class RepositoryProfileAdapter extends RecyclerView.Adapter { 18 | 19 | private List repositoryProfiles; 20 | private OnItemClickListener listener; 21 | 22 | @NonNull 23 | @Override 24 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 25 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.repository_row, parent, false); 26 | return new ViewHolder(view); 27 | } 28 | 29 | @Override 30 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 31 | holder.setRepository(repositoryProfiles.get(position)); 32 | } 33 | 34 | @Override 35 | public int getItemCount() { 36 | return repositoryProfiles != null ? repositoryProfiles.size() : 0; 37 | } 38 | 39 | public void setRepositoryProfiles(List repositoryProfiles) { 40 | this.repositoryProfiles = repositoryProfiles; 41 | notifyDataSetChanged(); 42 | } 43 | 44 | public void setOnItemClickListener(OnItemClickListener listener) { 45 | this.listener = listener; 46 | } 47 | 48 | class ViewHolder extends RecyclerView.ViewHolder { 49 | 50 | private View view; 51 | private TextView title; 52 | private TextView subTitle; 53 | private ImageView icon; 54 | 55 | ViewHolder(@NonNull View itemView) { 56 | super(itemView); 57 | 58 | view = itemView; 59 | title = itemView.findViewById(R.id.repo_entry_title); 60 | subTitle = itemView.findViewById(R.id.repo_entry_subtitle); 61 | icon = itemView.findViewById(R.id.repo_entry_icon); 62 | } 63 | 64 | public void setRepository(RepositoryProfile repositoryProfile) { 65 | int iconRes = R.raw.linux; 66 | if (repositoryProfile.getType() != null) { 67 | switch (repositoryProfile.getType()) { 68 | case "alpine": 69 | iconRes = R.raw.alpine; 70 | break; 71 | case "archlinux": 72 | iconRes = R.raw.archlinux; 73 | break; 74 | case "centos": 75 | iconRes = R.raw.centos; 76 | break; 77 | case "debian": 78 | iconRes = R.raw.debian; 79 | break; 80 | case "fedora": 81 | iconRes = R.raw.fedora; 82 | break; 83 | case "kali": 84 | iconRes = R.raw.kali; 85 | break; 86 | case "slackware": 87 | iconRes = R.raw.slackware; 88 | break; 89 | case "ubuntu": 90 | iconRes = R.raw.ubuntu; 91 | break; 92 | } 93 | } 94 | 95 | icon.setImageResource(iconRes); 96 | title.setText(repositoryProfile.getProfile()); 97 | if (repositoryProfile.getDescription() != null && !repositoryProfile.getDescription().isEmpty()) 98 | subTitle.setText(repositoryProfile.getDescription()); 99 | else 100 | subTitle.setText(view.getContext().getString(R.string.repository_default_description)); 101 | 102 | view.setOnClickListener(v -> { 103 | if (listener != null) 104 | listener.onClick(repositoryProfile); 105 | }); 106 | } 107 | } 108 | 109 | public interface OnItemClickListener { 110 | void onClick(RepositoryProfile repositoryProfile); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/fragment/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.fragment; 2 | 3 | import android.Manifest; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.SharedPreferences; 7 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 8 | import android.content.pm.PackageManager; 9 | import android.os.Bundle; 10 | 11 | import androidx.appcompat.app.AlertDialog; 12 | import androidx.core.app.ActivityCompat; 13 | import androidx.core.content.ContextCompat; 14 | import androidx.preference.CheckBoxPreference; 15 | import androidx.preference.EditTextPreference; 16 | import androidx.preference.ListPreference; 17 | import androidx.preference.Preference; 18 | import androidx.preference.PreferenceFragmentCompat; 19 | import androidx.preference.PreferenceGroup; 20 | import androidx.preference.PreferenceScreen; 21 | 22 | import ru.meefik.linuxdeploy.EnvUtils; 23 | import ru.meefik.linuxdeploy.PrefStore; 24 | import ru.meefik.linuxdeploy.R; 25 | import ru.meefik.linuxdeploy.RemoveEnvTask; 26 | import ru.meefik.linuxdeploy.UpdateEnvTask; 27 | import ru.meefik.linuxdeploy.receiver.BootReceiver; 28 | 29 | public class SettingsFragment extends PreferenceFragmentCompat implements 30 | OnSharedPreferenceChangeListener, Preference.OnPreferenceClickListener { 31 | 32 | @Override 33 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 34 | getPreferenceManager().setSharedPreferencesName(PrefStore.getSettingsSharedName()); 35 | setPreferencesFromResource(R.xml.settings, rootKey); 36 | initSummaries(getPreferenceScreen()); 37 | } 38 | 39 | @Override 40 | public void onResume() { 41 | super.onResume(); 42 | 43 | getPreferenceScreen().getSharedPreferences() 44 | .registerOnSharedPreferenceChangeListener(this); 45 | } 46 | 47 | @Override 48 | public void onPause() { 49 | super.onPause(); 50 | 51 | getPreferenceScreen().getSharedPreferences() 52 | .unregisterOnSharedPreferenceChangeListener(this); 53 | } 54 | 55 | @Override 56 | public boolean onPreferenceClick(Preference preference) { 57 | switch (preference.getKey()) { 58 | case "installenv": 59 | updateEnvDialog(); 60 | return true; 61 | case "removeenv": 62 | removeEnvDialog(); 63 | return true; 64 | default: 65 | return false; 66 | } 67 | } 68 | 69 | @Override 70 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 71 | Preference pref = findPreference(key); 72 | setSummary(pref, true); 73 | switch (key) { 74 | case "is_telnet": 75 | // start/stop telnetd 76 | EnvUtils.execService(getContext(), "telnetd", null); 77 | break; 78 | case "telnet_port": 79 | // restart telnetd 80 | EnvUtils.execService(getContext(), "telnetd", "restart"); 81 | // restart httpd 82 | EnvUtils.execService(getContext(), "httpd", "restart"); 83 | break; 84 | case "telnet_localhost": 85 | // restart telnetd 86 | EnvUtils.execService(getContext(), "telnetd", "restart"); 87 | break; 88 | case "is_http": 89 | // start/stop httpd 90 | EnvUtils.execService(getContext(), "httpd", null); 91 | break; 92 | case "http_port": 93 | case "http_conf": 94 | // restart httpd 95 | EnvUtils.execService(getContext(), "httpd", "restart"); 96 | break; 97 | case "autostart": 98 | // set autostart settings 99 | int autostartFlag = (PrefStore.isAutostart(getContext()) ? 100 | PackageManager.COMPONENT_ENABLED_STATE_ENABLED 101 | : PackageManager.COMPONENT_ENABLED_STATE_DISABLED); 102 | ComponentName bootComponent = new ComponentName(getContext(), BootReceiver.class); 103 | getContext().getPackageManager().setComponentEnabledSetting(bootComponent, autostartFlag, 104 | PackageManager.DONT_KILL_APP); 105 | break; 106 | case "stealth": 107 | // set stealth mode 108 | // Run app without launcher: am start -n ru.meefik.linuxdeploy/.MainActivity 109 | int stealthFlag = PrefStore.isStealth(getContext()) ? 110 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED 111 | : PackageManager.COMPONENT_ENABLED_STATE_ENABLED; 112 | ComponentName mainComponent = new ComponentName(getContext().getPackageName(), getContext().getPackageName() + ".Launcher"); 113 | getContext().getPackageManager().setComponentEnabledSetting(mainComponent, stealthFlag, 114 | PackageManager.DONT_KILL_APP); 115 | break; 116 | } 117 | } 118 | 119 | private void initSummaries(PreferenceGroup pg) { 120 | for (int i = 0; i < pg.getPreferenceCount(); ++i) { 121 | Preference p = pg.getPreference(i); 122 | if (p instanceof PreferenceGroup) 123 | initSummaries((PreferenceGroup) p); 124 | else 125 | setSummary(p, false); 126 | if (p instanceof PreferenceScreen) 127 | p.setOnPreferenceClickListener(this); 128 | } 129 | } 130 | 131 | private void setSummary(Preference pref, boolean init) { 132 | if (pref instanceof EditTextPreference) { 133 | EditTextPreference editPref = (EditTextPreference) pref; 134 | pref.setSummary(editPref.getText()); 135 | 136 | switch (editPref.getKey()) { 137 | case "env_dir": 138 | if (!init) { 139 | editPref.setText(PrefStore.getEnvDir(getContext())); 140 | pref.setSummary(editPref.getText()); 141 | } 142 | break; 143 | case "http_conf": 144 | if (editPref.getText().isEmpty()) { 145 | editPref.setText(PrefStore.getHttpConf(getContext())); 146 | pref.setSummary(editPref.getText()); 147 | } 148 | break; 149 | case "logfile": 150 | if (!init) { 151 | editPref.setText(PrefStore.getLogFile(getContext())); 152 | pref.setSummary(editPref.getText()); 153 | } 154 | break; 155 | } 156 | } 157 | 158 | if (pref instanceof ListPreference) { 159 | ListPreference listPref = (ListPreference) pref; 160 | pref.setSummary(listPref.getEntry()); 161 | } 162 | 163 | if (pref instanceof CheckBoxPreference) { 164 | CheckBoxPreference checkPref = (CheckBoxPreference) pref; 165 | 166 | if (checkPref.getKey().equals("logger") && checkPref.isChecked() && init) { 167 | requestWritePermissions(); 168 | } 169 | } 170 | } 171 | 172 | private void updateEnvDialog() { 173 | final Context context = getContext(); 174 | new AlertDialog.Builder(getContext()) 175 | .setTitle(R.string.title_installenv_preference) 176 | .setMessage(R.string.message_installenv_confirm_dialog) 177 | .setIcon(android.R.drawable.ic_dialog_alert) 178 | .setCancelable(false) 179 | .setPositiveButton(android.R.string.yes, 180 | (dialog, id) -> new UpdateEnvTask(context).execute()) 181 | .setNegativeButton(android.R.string.no, 182 | (dialog, id) -> dialog.cancel()).show(); 183 | } 184 | 185 | private void removeEnvDialog() { 186 | final Context context = getContext(); 187 | new AlertDialog.Builder(getContext()) 188 | .setTitle(R.string.title_removeenv_preference) 189 | .setMessage(R.string.message_removeenv_confirm_dialog) 190 | .setIcon(android.R.drawable.ic_dialog_alert) 191 | .setCancelable(false) 192 | .setPositiveButton(android.R.string.yes, 193 | (dialog, id) -> new RemoveEnvTask(context).execute()) 194 | .setNegativeButton(android.R.string.no, 195 | (dialog, id) -> dialog.cancel()).show(); 196 | } 197 | 198 | /** 199 | * Request permission for write to storage 200 | */ 201 | private void requestWritePermissions() { 202 | int REQUEST_WRITE_STORAGE = 112; 203 | boolean hasPermission = (ContextCompat.checkSelfPermission(getContext(), 204 | Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED); 205 | if (!hasPermission) { 206 | ActivityCompat.requestPermissions(getActivity(), 207 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WRITE_STORAGE); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/model/Mount.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.model; 2 | 3 | public class Mount { 4 | private String source; 5 | private String target; 6 | 7 | public Mount() { 8 | } 9 | 10 | public Mount(String source, String target) { 11 | this.source = source; 12 | this.target = target; 13 | } 14 | 15 | public String getSource() { 16 | return source; 17 | } 18 | 19 | public void setSource(String source) { 20 | this.source = source; 21 | } 22 | 23 | public String getTarget() { 24 | return target; 25 | } 26 | 27 | public void setTarget(String target) { 28 | this.target = target; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/model/RepositoryProfile.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.model; 2 | 3 | public class RepositoryProfile { 4 | private String profile; 5 | private String description; 6 | private String type; 7 | private String size; 8 | 9 | public RepositoryProfile() { 10 | // Empty constructor 11 | } 12 | 13 | public String getProfile() { 14 | return profile; 15 | } 16 | 17 | public void setProfile(String profile) { 18 | this.profile = profile; 19 | } 20 | 21 | public String getDescription() { 22 | return description; 23 | } 24 | 25 | public void setDescription(String description) { 26 | this.description = description; 27 | } 28 | 29 | public String getType() { 30 | return type; 31 | } 32 | 33 | public void setType(String type) { 34 | this.type = type; 35 | } 36 | 37 | public String getSize() { 38 | return size; 39 | } 40 | 41 | public void setSize(String size) { 42 | this.size = size; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/receiver/ActionReceiver.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.receiver; 2 | 3 | import android.app.NotificationManager; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | 8 | import androidx.core.app.NotificationCompat; 9 | 10 | import ru.meefik.linuxdeploy.EnvUtils; 11 | import ru.meefik.linuxdeploy.R; 12 | import ru.meefik.linuxdeploy.activity.MainActivity; 13 | 14 | import static ru.meefik.linuxdeploy.App.SERVICE_CHANNEL_ID; 15 | 16 | public class ActionReceiver extends BroadcastReceiver { 17 | 18 | final static int NOTIFY_ID = 2; 19 | static long attemptTime = 0; 20 | static long attemptNumber = 1; 21 | 22 | private void showNotification(Context c, int icon, String text) { 23 | NotificationManager mNotificationManager = (NotificationManager) c 24 | .getSystemService(Context.NOTIFICATION_SERVICE); 25 | NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(c, SERVICE_CHANNEL_ID) 26 | .setSmallIcon(icon) 27 | .setContentTitle(c.getString(R.string.app_name)) 28 | .setContentText(text); 29 | mBuilder.setWhen(0); 30 | mNotificationManager.notify(NOTIFY_ID, mBuilder.build()); 31 | } 32 | 33 | private void hideNotification(Context c) { 34 | NotificationManager mNotificationManager = (NotificationManager) c 35 | .getSystemService(Context.NOTIFICATION_SERVICE); 36 | mNotificationManager.cancel(NOTIFY_ID); 37 | } 38 | 39 | @Override 40 | public void onReceive(final Context context, Intent intent) { 41 | // am broadcast -a ru.meefik.linuxdeploy.BROADCAST_ACTION --user 0 --esn "hide" 42 | if (intent.hasExtra("hide")) { 43 | hideNotification(context); 44 | return; 45 | } 46 | // am broadcast -a ru.meefik.linuxdeploy.BROADCAST_ACTION --user 0 --es "info" "Hello World!" 47 | if (intent.hasExtra("info")) { 48 | showNotification(context, android.R.drawable.ic_dialog_info, intent.getStringExtra("info")); 49 | return; 50 | } 51 | // am broadcast -a ru.meefik.linuxdeploy.BROADCAST_ACTION --user 0 --es "alert" "Hello World!" 52 | if (intent.hasExtra("alert")) { 53 | showNotification(context, android.R.drawable.ic_dialog_alert, intent.getStringExtra("alert")); 54 | return; 55 | } 56 | // am broadcast -a ru.meefik.linuxdeploy.BROADCAST_ACTION --user 0 --esn "start" 57 | if (intent.hasExtra("start")) { 58 | System.out.println("START"); 59 | EnvUtils.execService(context, "start", "-m"); 60 | return; 61 | } 62 | // am broadcast -a ru.meefik.linuxdeploy.BROADCAST_ACTION --user 0 --esn "stop" 63 | if (intent.hasExtra("stop")) { 64 | System.out.println("STOP"); 65 | EnvUtils.execService(context, "stop", "-u"); 66 | return; 67 | } 68 | // am broadcast -a ru.meefik.linuxdeploy.BROADCAST_ACTION --user 0 --esn "show" 69 | if (intent.hasExtra("show")) { 70 | if (attemptTime > System.currentTimeMillis() - 5000) { 71 | attemptNumber++; 72 | } else { 73 | attemptNumber = 1; 74 | } 75 | attemptTime = System.currentTimeMillis(); 76 | if (attemptNumber >= 5) { 77 | attemptNumber = 1; 78 | Intent mainIntent = new Intent(context.getApplicationContext(), MainActivity.class); 79 | mainIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 80 | context.startActivity(mainIntent); 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/receiver/BootReceiver.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.receiver; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | import ru.meefik.linuxdeploy.EnvUtils; 8 | import ru.meefik.linuxdeploy.PrefStore; 9 | 10 | public class BootReceiver extends BroadcastReceiver { 11 | 12 | @Override 13 | public void onReceive(final Context context, Intent intent) { 14 | String action = intent.getAction(); 15 | if (action == null) return; 16 | switch (action) { 17 | case Intent.ACTION_BOOT_COMPLETED: 18 | try { // Autostart delay 19 | Integer delay_s = PrefStore.getAutostartDelay(context); 20 | Thread.sleep(delay_s * 1000); 21 | } catch (InterruptedException e) { 22 | e.printStackTrace(); 23 | } 24 | EnvUtils.execServices(context, new String[]{"telnetd", "httpd"}, "start"); 25 | EnvUtils.execService(context, "start", "-m"); 26 | break; 27 | case Intent.ACTION_SHUTDOWN: 28 | EnvUtils.execService(context, "stop", "-u"); 29 | EnvUtils.execServices(context, new String[]{"telnetd", "httpd"}, "stop"); 30 | try { // Shutdown delay 31 | Integer delay_s = PrefStore.getAutostartDelay(context); 32 | Thread.sleep(delay_s * 1000); 33 | } catch (InterruptedException e) { 34 | e.printStackTrace(); 35 | } 36 | break; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/receiver/NetworkReceiver.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.receiver; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.ConnectivityManager; 7 | import android.net.Network; 8 | import android.net.NetworkCapabilities; 9 | import android.net.NetworkInfo; 10 | 11 | import ru.meefik.linuxdeploy.EnvUtils; 12 | 13 | public class NetworkReceiver extends BroadcastReceiver { 14 | 15 | @Override 16 | public void onReceive(final Context context, Intent intent) { 17 | if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 18 | ConnectivityManager cm = 19 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 20 | 21 | boolean isConnected; 22 | 23 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { 24 | Network activeNetwork = cm.getActiveNetwork(); 25 | NetworkCapabilities networkCapabilities = cm.getNetworkCapabilities(activeNetwork); 26 | 27 | isConnected = networkCapabilities != null 28 | && (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) 29 | || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) 30 | || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)); 31 | } else { 32 | NetworkInfo activeNetworkInfo = cm.getActiveNetworkInfo(); 33 | isConnected = activeNetworkInfo != null && activeNetworkInfo.isConnected(); 34 | } 35 | 36 | if (isConnected) { 37 | EnvUtils.execService(context, "start", "core/net"); 38 | } else { 39 | EnvUtils.execService(context, "stop", "core/net"); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/meefik/linuxdeploy/receiver/PowerReceiver.java: -------------------------------------------------------------------------------- 1 | package ru.meefik.linuxdeploy.receiver; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | import ru.meefik.linuxdeploy.EnvUtils; 8 | 9 | public class PowerReceiver extends BroadcastReceiver { 10 | 11 | @Override 12 | public void onReceive(final Context context, Intent intent) { 13 | if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { 14 | EnvUtils.execService(context, "stop", "core/power"); 15 | } else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { 16 | EnvUtils.execService(context, "start", "core/power"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_computer_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_exit_to_app_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_help_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tune_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_warning_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/activity_about.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 22 | 23 | 30 | 31 | 39 | 40 | 47 | 48 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/content_main.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 24 | 25 | 34 | 35 | 42 | 43 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_fullscreen.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_mounts.xml: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_preference.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_profiles.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_repository.xml: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 32 | 33 | 34 | 35 | 45 | 46 |