├── perspectived
├── MODULE_LICENSE_APACHE2
├── perspectived.rc
├── Android.bp
├── PerspectiveService.cpp
├── LXCContainerManager.h
├── LXCContainerManager.cpp
└── NOTICE
├── app
├── app
│ ├── .gitignore
│ ├── src
│ │ └── main
│ │ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher_foreground.webp
│ │ │ ├── mipmap-anydpi
│ │ │ │ └── ic_launcher.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_warning.xml
│ │ │ │ └── ic_help.xml
│ │ │ ├── layout
│ │ │ │ ├── progressdialog.xml
│ │ │ │ └── launcher.xml
│ │ │ └── drawable-anydpi
│ │ │ │ └── ic_cute_penguin.xml
│ │ │ ├── java
│ │ │ └── org
│ │ │ │ └── lindroid
│ │ │ │ └── ui
│ │ │ │ ├── MainApplication.java
│ │ │ │ ├── NativeLib.java
│ │ │ │ ├── Constants.java
│ │ │ │ ├── HardwareService.java
│ │ │ │ ├── ContainerAdapter.kt
│ │ │ │ ├── AudioSocketServer.java
│ │ │ │ ├── ContainerManager.java
│ │ │ │ ├── LauncherActivity.kt
│ │ │ │ └── DisplayActivity.java
│ │ │ ├── Android.bp
│ │ │ ├── AndroidManifest.xml
│ │ │ └── cpp
│ │ │ ├── InputDevice.h
│ │ │ ├── ComposerImpl.h
│ │ │ ├── native-lib.cpp
│ │ │ ├── ComposerImpl.cpp
│ │ │ └── InputDevice.cpp
│ ├── proguard-rules.pro
│ └── build.gradle.kts
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── .gitignore
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
├── gradlew.bat
└── gradlew
├── configs
└── disabled.idc
├── interfaces
├── .clang-format
├── Android.bp
├── composer
│ ├── vendor
│ │ └── lindroid
│ │ │ └── composer
│ │ │ ├── IComposerCallback.aidl
│ │ │ ├── IComposer.aidl
│ │ │ └── DisplayConfiguration.aidl
│ └── Android.bp
└── perspective
│ ├── Android.bp
│ └── vendor
│ └── lindroid
│ └── perspective
│ └── IPerspective.aidl
├── sepolicy
├── system_app.te
├── lindroid_composer_service.te
├── service.te
├── service_contexts
├── file_contexts
├── lindroid_file.te
└── perspectived.te
├── prebuilt
├── arm64
│ └── libc.so
└── x86_64
│ └── libc.so
├── lxc
├── default.conf
├── Android.bp
├── init.lindroid.rc
└── template.sh
├── lindroid.mk
└── README.md
/perspectived/MODULE_LICENSE_APACHE2:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /system_libs
3 |
--------------------------------------------------------------------------------
/configs/disabled.idc:
--------------------------------------------------------------------------------
1 | device.disabled = 1
2 |
--------------------------------------------------------------------------------
/interfaces/.clang-format:
--------------------------------------------------------------------------------
1 | ../../../build/soong/scripts/system-clang-format
--------------------------------------------------------------------------------
/sepolicy/system_app.te:
--------------------------------------------------------------------------------
1 | allow system_app uhid_device:chr_file rw_file_perms;
2 |
--------------------------------------------------------------------------------
/sepolicy/lindroid_composer_service.te:
--------------------------------------------------------------------------------
1 | allow system_app lindroid_composer_service:service_manager add;
2 |
--------------------------------------------------------------------------------
/prebuilt/arm64/libc.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Linux-on-droid/vendor_lindroid/HEAD/prebuilt/arm64/libc.so
--------------------------------------------------------------------------------
/prebuilt/x86_64/libc.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Linux-on-droid/vendor_lindroid/HEAD/prebuilt/x86_64/libc.so
--------------------------------------------------------------------------------
/interfaces/Android.bp:
--------------------------------------------------------------------------------
1 | hidl_package_root {
2 | name: "vendor.lindroid",
3 | path: "vendor/lindroid/interfaces",
4 | }
5 |
--------------------------------------------------------------------------------
/lxc/default.conf:
--------------------------------------------------------------------------------
1 | lxc.net.0.type = veth
2 | lxc.net.0.link = lxcbr0
3 | lxc.net.0.flags = up
4 | lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx
5 |
--------------------------------------------------------------------------------
/app/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Linux-on-droid/vendor_lindroid/HEAD/app/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/perspectived/perspectived.rc:
--------------------------------------------------------------------------------
1 | service perspectived /system_ext/bin/perspectived
2 | class late_start
3 | user root
4 | group root system
5 |
--------------------------------------------------------------------------------
/sepolicy/service.te:
--------------------------------------------------------------------------------
1 | type perspectived_service, service_manager_type;
2 | type lindroid_composer_service, service_manager_type;
3 |
--------------------------------------------------------------------------------
/sepolicy/service_contexts:
--------------------------------------------------------------------------------
1 | perspective u:object_r:perspectived_service:s0
2 | vendor.lindroid.composer u:object_r:lindroid_composer_service:s0
3 |
--------------------------------------------------------------------------------
/sepolicy/file_contexts:
--------------------------------------------------------------------------------
1 | /system_ext/bin/perspectived u:object_r:perspectived_exec:s0
2 | /data/lindroid(/.*)? u:object_r:lindroid_file:s0
3 |
4 |
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Linux-on-droid/vendor_lindroid/HEAD/app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Linux-on-droid/vendor_lindroid/HEAD/app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Linux-on-droid/vendor_lindroid/HEAD/app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Linux-on-droid/vendor_lindroid/HEAD/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Linux-on-droid/vendor_lindroid/HEAD/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | keystore.properties
12 | *.jks
13 |
--------------------------------------------------------------------------------
/app/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu May 23 00:04:57 IRST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id("com.android.application") version "8.7.2" apply false
4 | id("org.jetbrains.kotlin.android") version "2.0.21" apply false
5 | }
6 |
7 | tasks.withType(JavaCompile::class.java) {
8 | options.compilerArgs.add("-Xlint:all")
9 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/interfaces/composer/vendor/lindroid/composer/IComposerCallback.aidl:
--------------------------------------------------------------------------------
1 | package vendor.lindroid.composer;
2 |
3 | interface IComposerCallback {
4 | oneway void onVsyncReceived(int sequenceId, long display, long timestamp);
5 | void onHotplugReceived(int sequenceId, long display, boolean connected, boolean primaryDisplay);
6 | oneway void onRefreshReceived(int sequenceId, long display);
7 | }
8 |
--------------------------------------------------------------------------------
/app/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | pluginManagement {
4 | repositories {
5 | google()
6 | mavenCentral()
7 | gradlePluginPortal()
8 | }
9 | }
10 |
11 | dependencyResolutionManagement {
12 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
13 | repositories {
14 | google()
15 | mavenCentral()
16 | maven("https://jitpack.io")
17 | }
18 | }
19 |
20 | rootProject.name = "Lindroid"
21 | include(":app")
22 |
--------------------------------------------------------------------------------
/interfaces/perspective/Android.bp:
--------------------------------------------------------------------------------
1 | aidl_interface {
2 | name: "vendor.lindroid.perspective",
3 | system_ext_specific: true,
4 | srcs: [ "vendor/lindroid/perspective/*.aidl" ],
5 |
6 | unstable: true,
7 | // cheat around setting a version
8 | owner: "lindroid",
9 | backend: {
10 | java: {
11 | enabled: true,
12 | platform_apis: true,
13 | },
14 | cpp: {
15 | enabled: false,
16 | },
17 | ndk: {
18 | enabled: true,
19 | },
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/lxc/Android.bp:
--------------------------------------------------------------------------------
1 | prebuilt_etc {
2 | name: "init.lindroid.rc",
3 | src: "init.lindroid.rc",
4 | filename_from_src: true,
5 | sub_dir: "init",
6 | system_ext_specific: true,
7 | }
8 |
9 | sh_binary {
10 | name: "lxc-lindroid",
11 | src: "template.sh",
12 | filename: "lxc-lindroid",
13 | sub_dir: "lindroid-lxc-templates",
14 | system_ext_specific: true,
15 | }
16 |
17 | prebuilt_usr_share {
18 | name: "lindroid-default.conf",
19 | src: "default.conf",
20 | filename_from_src: true,
21 | sub_dir: "lindroid/lxc",
22 | system_ext_specific: true,
23 | }
24 |
--------------------------------------------------------------------------------
/sepolicy/lindroid_file.te:
--------------------------------------------------------------------------------
1 | type lindroid_file, file_type, data_file_type, core_data_file_type;
2 |
3 | # Allow system_app to remove and add the audio_socket file
4 | allow system_app lindroid_file:dir { remove_name add_name };
5 |
6 | # Allow system_app to unlink the audio_socket
7 | allow system_app lindroid_file:sock_file unlink;
8 |
9 | # Allow system_app to create, bind, and manage the socket
10 | allow system_app lindroid_file:sock_file { create write read };
11 |
12 | # Allow system_app to access the directory containing the socket (if necessary)
13 | allow system_app lindroid_file:dir { write search open };
14 |
--------------------------------------------------------------------------------
/interfaces/composer/Android.bp:
--------------------------------------------------------------------------------
1 | aidl_interface {
2 | name: "vendor.lindroid.composer",
3 | system_ext_specific: true,
4 | srcs: [ "vendor/lindroid/composer/*.aidl" ],
5 | imports: [
6 | "android.hardware.graphics.common-V5",
7 | ],
8 |
9 | unstable: true,
10 | // cheat around setting a version
11 | owner: "lindroid",
12 | backend: {
13 | java: {
14 | enabled: true,
15 | platform_apis: true,
16 | },
17 | cpp: {
18 | enabled: false,
19 | },
20 | ndk: {
21 | enabled: true,
22 | },
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_warning.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/progressdialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/lxc/init.lindroid.rc:
--------------------------------------------------------------------------------
1 | on init
2 | mkdir /sys/fs/cgroup/devices 0750 root system
3 | mount cgroup none /sys/fs/cgroup/devices devices
4 | mkdir /sys/fs/cgroup/freezer 0750 root system
5 | mount cgroup none /sys/fs/cgroup/freezer freezer
6 | mkdir /sys/fs/cgroup/systemd 0750 root system
7 | mount cgroup none /sys/fs/cgroup/systemd none,name=systemd
8 | # Ensure /dev/dri directory exists
9 | mkdir /dev/dri 0755 root root
10 |
11 | on post-fs-data
12 | mkdir /data/lindroid 0770 system system
13 | mkdir /data/lindroid/mnt 0770 system system
14 | mkdir /data/lindroid/lxc 0770 system system
15 | mkdir /data/lindroid/lxc/log 0770 system system
16 | mkdir /data/lindroid/lxc/run 0770 system system
17 | mkdir /data/lindroid/lxc/rootfs 0770 system system
18 | mkdir /data/lindroid/lxc/rootfs_ro 0770 system system
19 | mkdir /data/lindroid/lxc/container 0770 system system
20 |
--------------------------------------------------------------------------------
/app/app/src/main/java/org/lindroid/ui/MainApplication.java:
--------------------------------------------------------------------------------
1 | package org.lindroid.ui;
2 |
3 | import static org.lindroid.ui.NativeLib.nativeInitInputDevice;
4 |
5 | import android.app.Application;
6 | import android.util.Log;
7 |
8 | import com.google.android.material.color.DynamicColors;
9 |
10 | public class MainApplication extends Application {
11 | @Override
12 | public void onCreate() {
13 | super.onCreate();
14 | if (!getFilesDir().exists() && getFilesDir().mkdir()) {
15 | Log.e("Lindroid", "failed to create files dir");
16 | }
17 |
18 | // Apply dynamic color
19 | DynamicColors.applyToActivitiesIfAvailable(this);
20 |
21 | // We're alive until we aren't, so this is fine to put here
22 | Thread composerThread = new Thread(NativeLib::nativeStartComposerService);
23 | composerThread.start();
24 | nativeInitInputDevice();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
19 |
--------------------------------------------------------------------------------
/interfaces/perspective/vendor/lindroid/perspective/IPerspective.aidl:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015-2016 Preetam J. D'Souza
3 | * Copyright 2016 The Maru OS Project
4 | * Copyright 2024-2025 Lindroid
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | package vendor.lindroid.perspective;
20 |
21 | /**
22 | * @hide
23 | */
24 | interface IPerspective {
25 | boolean start(String id, boolean capture_output);
26 | boolean stop(String id);
27 | boolean isRunning(String id);
28 | List listContainers();
29 | boolean addContainer(String id, in ParcelFileDescriptor tarball);
30 | boolean deleteContainer(String id);
31 | String fetchLogs(String id);
32 | }
33 |
--------------------------------------------------------------------------------
/interfaces/composer/vendor/lindroid/composer/IComposer.aidl:
--------------------------------------------------------------------------------
1 | package vendor.lindroid.composer;
2 |
3 | import vendor.lindroid.composer.IComposerCallback;
4 | import vendor.lindroid.composer.DisplayConfiguration;
5 | import android.hardware.graphics.common.HardwareBuffer;
6 |
7 | interface IComposer {
8 | void registerCallback(in IComposerCallback cb, int sequenceId);
9 | void onHotplug(long displayId, boolean connected);
10 |
11 | // Display
12 | void requestDisplay(long displayId);
13 | DisplayConfiguration getActiveConfig(long displayId);
14 | void acceptChanges(long displayId);
15 | @nullable ParcelFileDescriptor getReleaseFence(long displayId);
16 | @nullable ParcelFileDescriptor present(long displayId);
17 | void setPowerMode(long displayId, int mode);
18 | void setVsyncEnabled(long displayId, int enabled);
19 | boolean getUiRunning();
20 | //void validate()
21 |
22 | // Layer
23 | int setBuffer(long displayId, in HardwareBuffer buffer, in @nullable ParcelFileDescriptor fenceFd);
24 | //void setBlendMode(int mode);
25 | //setColor
26 | //setCompositionType
27 | //setDataspace
28 | //setDisplayFrame
29 | //setPlaneAlpha
30 | //setSidebandStream
31 | //setSourceCrop
32 | //setTransform
33 | //setVisibleRegion
34 | }
35 |
--------------------------------------------------------------------------------
/app/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Enables namespacing of each library's R class so that its R class includes only the
19 | # resources declared in the library itself and none from the library's dependencies,
20 | # thereby reducing the size of the R class for that library
21 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/app/src/main/Android.bp:
--------------------------------------------------------------------------------
1 | android_app {
2 | name: "LindroidUI",
3 | platform_apis: true,
4 | certificate: "platform",
5 | system_ext_specific: true,
6 |
7 | srcs: [
8 | "java/**/*.java",
9 | "java/**/*.kt",
10 | ],
11 |
12 | static_libs: [
13 | "androidx.appcompat_appcompat",
14 | "com.google.android.material_material",
15 | "vendor.lindroid.perspective-java",
16 | "kotlinx_coroutines_android",
17 | ],
18 | jni_libs: [
19 | "libjni_lindroidui",
20 | ],
21 | required: [
22 | "libjni_lindroidui",
23 | ],
24 | }
25 |
26 | cc_library {
27 | name: "libjni_lindroidui",
28 | system_ext_specific: true,
29 |
30 | srcs: [
31 | "cpp/native-lib.cpp",
32 | "cpp/ComposerImpl.cpp",
33 | "cpp/InputDevice.cpp",
34 | ],
35 | shared_libs: [
36 | "libbase",
37 | "libbinder",
38 | "libbinder_ndk",
39 | "libcutils",
40 | "libutils",
41 | "liblog",
42 | "libui",
43 | "libgui",
44 | "libnativewindow",
45 | "libandroid",
46 | "libandroid_runtime",
47 | "vendor.lindroid.composer-ndk",
48 | ],
49 | static_libs: [
50 | "libaidlcommonsupport",
51 | ],
52 | cppflags: [
53 | "-Wno-unused-parameter",
54 | "-Wno-unused-variable",
55 | "-Wno-unused-private-field",
56 | ],
57 | }
58 |
--------------------------------------------------------------------------------
/perspectived/Android.bp:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright 2015-2016 Preetam J. D'Souza
3 | // Copyright 2016-2020 The Maru OS Project
4 | // Copyright 2034 Lindroid Project
5 | //
6 | // Licensed under the Apache License, Version 2.0 (the "License");
7 | // you may not use this file except in compliance with the License.
8 | // You may obtain a copy of the License at
9 | //
10 | // http://www.apache.org/licenses/LICENSE-2.0
11 | //
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS,
14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | // See the License for the specific language governing permissions and
16 | // limitations under the License.
17 | //
18 |
19 | // -----------------------------------------------------------------------------
20 | // perspectived
21 |
22 | cc_binary {
23 | name: "perspectived",
24 | system_ext_specific: true,
25 | init_rc: ["perspectived.rc"],
26 |
27 | srcs: [
28 | "PerspectiveService.cpp",
29 | "LXCContainerManager.cpp",
30 | ],
31 |
32 | include_dirs: [
33 | "external/lxc/src",
34 | ],
35 |
36 | cflags: ["-DLOG_TAG=\"perspectived\""],
37 |
38 | shared_libs: [
39 | "libhardware_legacy",
40 | "liblog",
41 | "libbinder",
42 | "libutils",
43 | "liblxc",
44 | "libbinder_ndk",
45 | "vendor.lindroid.perspective-ndk",
46 | ],
47 | }
48 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_help.xml:
--------------------------------------------------------------------------------
1 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/perspectived/PerspectiveService.cpp:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015-2016 Preetam J. D'Souza
3 | * Copyright 2016 The Maru OS Project
4 | * Copyright 2024 Lindroid
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | #include
20 | #include
21 |
22 | #include
23 |
24 | #include
25 | #include
26 |
27 | #include "LXCContainerManager.h"
28 |
29 | using aidl::vendor::lindroid::perspective::LXCContainerManager;
30 |
31 | #define SERVICE_NAME "perspective"
32 |
33 | int main(void) {
34 | umask(0000);
35 | auto perspective = ndk::SharedRefBase::make();
36 |
37 | binder_status_t status = AServiceManager_addService(perspective->asBinder().get(), SERVICE_NAME);
38 | if (status != STATUS_OK) {
39 | ALOGE("Could not register perspective binder service");
40 | return EXIT_FAILURE;
41 | }
42 |
43 | ABinderProcess_joinThreadPool();
44 |
45 | // should never get here
46 | return EXIT_FAILURE;
47 | }
48 |
--------------------------------------------------------------------------------
/lindroid.mk:
--------------------------------------------------------------------------------
1 | # LXC Tools
2 | PRODUCT_PACKAGES += \
3 | lxc_attach \
4 | lxc_autostart \
5 | lxc_cgroup \
6 | lxc_checkpoint \
7 | lxc_config \
8 | lxc_console \
9 | lxc_copy \
10 | lxc_create \
11 | lxc_destroy \
12 | lxc_device \
13 | lxc_execute \
14 | lxc_freeze \
15 | lxc_info \
16 | lxc_ls \
17 | lxc_monitor \
18 | lxc_multicall \
19 | lxc_snapshot \
20 | lxc_start \
21 | lxc_stop \
22 | lxc_top \
23 | lxc_unfreeze \
24 | lxc_unshare
25 |
26 | # Hacked libc for lxc container
27 | ifneq ($(filter %x86 %x86_64,$(TARGET_PRODUCT)),)
28 | PRODUCT_COPY_FILES += $(LOCAL_PATH)/prebuilt/x86_64/libc.so:$(TARGET_COPY_OUT_SYSTEM_EXT)/usr/share/lindroid/libc.so
29 | else
30 | PRODUCT_COPY_FILES += $(LOCAL_PATH)/prebuilt/arm64/libc.so:$(TARGET_COPY_OUT_SYSTEM_EXT)/usr/share/lindroid/libc.so
31 | endif
32 |
33 | # Misc lindroid stuff
34 | PRODUCT_PACKAGES += \
35 | libhwc2_compat_layer \
36 | libui_compat_layer \
37 | LindroidUI \
38 | perspectived \
39 | init.lindroid.rc \
40 | lxc-lindroid \
41 | lindroid-default.conf
42 |
43 | # IDC to ignore lindroid inputs on android side
44 | PRODUCT_COPY_FILES += \
45 | $(LOCAL_PATH)/configs/disabled.idc:$(TARGET_COPY_OUT_SYSTEM)/usr/idc/Vendor_000a_Product_000a.idc \
46 | $(LOCAL_PATH)/configs/disabled.idc:$(TARGET_COPY_OUT_SYSTEM)/usr/idc/Vendor_000a_Product_000b.idc \
47 | $(LOCAL_PATH)/configs/disabled.idc:$(TARGET_COPY_OUT_SYSTEM)/usr/idc/Vendor_000a_Product_000c.idc \
48 | $(LOCAL_PATH)/configs/disabled.idc:$(TARGET_COPY_OUT_SYSTEM)/usr/idc/Vendor_000a_Product_000d.idc
49 |
50 | BOARD_SEPOLICY_DIRS += \
51 | vendor/lindroid/sepolicy
52 |
--------------------------------------------------------------------------------
/interfaces/composer/vendor/lindroid/composer/DisplayConfiguration.aidl:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023, The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package vendor.lindroid.composer;
18 |
19 | parcelable DisplayConfiguration {
20 | /**
21 | * The config id, to be used with IComposerClient.setActiveConfig.
22 | */
23 | int configId;
24 |
25 | /**
26 | * The display id.
27 | */
28 | long displayId;
29 |
30 | /**
31 | * Dimensions in pixels
32 | */
33 | int width;
34 | int height;
35 |
36 | /**
37 | * Dots per inch.
38 | * If the DPI for a configuration is unavailable or is
39 | * considered unreliable, the device may set null instead.
40 | */
41 | parcelable Dpi {
42 | float x;
43 | float y;
44 | }
45 | Dpi dpi;
46 |
47 | /**
48 | * Vsync period in nanoseconds. This period represents the internal
49 | * frequency of the display. IComposerCallback.onVsync is expected
50 | * to be called on each vsync event. For non-VRR configurations, a
51 | * frame can be presented on each vsync event.
52 | *
53 | * A present fence, retrieved from CommandResultPayload.presentFence
54 | * must be signaled on a vsync boundary.
55 | */
56 | int vsyncPeriod;
57 | }
58 |
--------------------------------------------------------------------------------
/app/app/src/main/java/org/lindroid/ui/NativeLib.java:
--------------------------------------------------------------------------------
1 | package org.lindroid.ui;
2 |
3 | import android.view.Surface;
4 |
5 | public class NativeLib {
6 | static {
7 | System.loadLibrary("jni_lindroidui");
8 | }
9 |
10 | public static native void nativeStartComposerService();
11 |
12 | public static native void nativeSurfaceCreated(long displayId, Surface surface);
13 |
14 | public static native void nativeSurfaceChanged(long displayId, Surface surface, int dpi, float refresh);
15 |
16 | public static native void nativeSurfaceDestroyed(long displayId, Surface surface);
17 |
18 | public static native void nativeDisplayDestroyed(long displayId);
19 |
20 | public static native boolean nativeGetUiRunning();
21 |
22 | public static native void nativeInitInputDevice();
23 |
24 | public static native void nativeReconfigureInputDevice(long displayId, int width, int height);
25 |
26 | public static native void nativeStopInputDevice(long displayId);
27 |
28 | public static native void nativeKeyEvent(long displayId, int keyCode, boolean isDown);
29 |
30 | public static native void nativeTouchEvent(long displayId, int pointerId, int action, int pressure, int x, int y);
31 |
32 | public static native void nativeTouchStylusButtonEvent(long displayId, int button, boolean isDown);
33 |
34 | public static native void nativeTouchStylusHoverEvent(long displayId, int action, int x, int y, int distance, int tilt_x, int tilt_y);
35 |
36 | public static native void nativeTouchStylusEvent(long displayId, int action, int pressure, int x, int y, int tilt_x, int tilt_y);
37 |
38 | public static native void nativePointerMotionEvent(long displayId, int x, int y);
39 |
40 | public static native void nativePointerButtonEvent(long displayId, int button, int x, int y, boolean isDown);
41 |
42 | public static native void nativePointerScrollEvent(long displayId, int value, boolean isVertical);
43 | }
44 |
--------------------------------------------------------------------------------
/app/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/perspectived/LXCContainerManager.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 The Maru OS Project
3 | * Copyright 2024 Lindroid
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | #pragma once
19 |
20 | #include
21 |
22 | namespace aidl {
23 | namespace vendor {
24 | namespace lindroid {
25 | namespace perspective {
26 |
27 | class LXCContainerManager : public BnPerspective {
28 | public:
29 | LXCContainerManager();
30 |
31 | // BnPerspective interface
32 | // ------------------------------------------------------------------------
33 |
34 | virtual ndk::ScopedAStatus start(const std::string &id, bool capture_output, bool *_aidl_return);
35 | virtual ndk::ScopedAStatus stop(const std::string &id, bool *_aidl_return);
36 | virtual ndk::ScopedAStatus fetchLogs(const std::string &id, std::string *_aidl_return);
37 | virtual ndk::ScopedAStatus isRunning(const std::string &id, bool *_aidl_return);
38 | virtual ndk::ScopedAStatus listContainers(std::vector *_aidl_return);
39 | virtual ndk::ScopedAStatus addContainer(const std::string &id, const ndk::ScopedFileDescriptor& fd, bool *_aidl_return);
40 | virtual ndk::ScopedAStatus deleteContainer(const std::string &id, bool *_aidl_return);
41 |
42 | // ------------------------------------------------------------------------
43 | };
44 |
45 | }; // namespace perspective
46 | }; // namespace lindroid
47 | }; // namespace vendor
48 | }; // namespace aidl
49 |
--------------------------------------------------------------------------------
/app/app/src/main/java/org/lindroid/ui/Constants.java:
--------------------------------------------------------------------------------
1 | package org.lindroid.ui;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.net.Uri;
5 | import android.os.Build;
6 |
7 | import java.lang.reflect.InvocationTargetException;
8 | import java.lang.reflect.Method;
9 | import java.net.SocketAddress;
10 |
11 | public class Constants {
12 | public static final String PERSPECTIVE_SERVICE_NAME = "perspective";
13 | public static final String SOCKET_PATH = "/data/lindroid/mnt/audio_socket";
14 | public static final String NOTIFICATION_CHANNEL_ID = "service";
15 | public static final int NOTIFICATION_ID = 1;
16 |
17 | @SuppressLint({ "PrivateApi", "BlockedPrivateApi" })
18 | public static SocketAddress createUnixSocketAddressObj(String path) {
19 | try {
20 | Class> myClass = Class.forName("android.system.UnixSocketAddress");
21 | Method method = myClass.getDeclaredMethod("createFileSystem", String.class);
22 | return (SocketAddress) method.invoke(null, path);
23 | } catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException |
24 | InvocationTargetException e) {
25 | throw new RuntimeException(e);
26 | }
27 | }
28 |
29 | public static Uri getDownloadUriForCurrentArch() {
30 | return switch (Build.SUPPORTED_ABIS[0]) {
31 | case "arm64-v8a" ->
32 | Uri.parse("https://github.com/Linux-on-droid/lindroid-rootfs/releases/download/nightly/lindroid-rootfs-arm64-plasma.zip.tar.gz");
33 | case "x86_64" ->
34 | Uri.parse("https://github.com/Linux-on-droid/lindroid-rootfs/releases/download/nightly/lindroid-rootfs-amd64-plasma.zip.tar.gz");
35 | case "armeabi-v7a" ->
36 | Uri.parse("https://github.com/Linux-on-droid/lindroid-rootfs/releases/download/nightly/lindroid-rootfs-armhf-plasma.zip.tar.gz");
37 | default ->
38 | throw new UnsupportedOperationException("Lindroid does not seem to support this architecture: " + Build.SUPPORTED_ABIS[0]);
39 | };
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/app/src/main/cpp/InputDevice.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | #include
9 |
10 | #define UINPUT_DEVICE "/dev/uinput"
11 |
12 | #define DEVICE_KEYBOARD 0
13 | #define DEVICE_TOUCH 1
14 | #define DEVICE_TABLET 2
15 | #define DEVICE_TOUCH_STYLUS 3
16 |
17 | using android::Mutex;
18 | using android::RefBase;
19 | using android::status_t;
20 |
21 | struct UInputDevice {
22 | void getFD(int32_t device, int32_t *fd);
23 | status_t start(int32_t device, int64_t displayId);
24 | status_t stop(int32_t device);
25 | status_t inject(int32_t device, uint16_t type, uint16_t code, int32_t value);
26 |
27 | int32_t mWidth;
28 | int32_t mHeight;
29 | int32_t mFDKeyboard = -1;
30 | int32_t mFDTouch = -1;
31 | int32_t mFDTouchStylus = -1;
32 | int32_t mFDTablet = -1;
33 | };
34 |
35 | class InputDevice : public RefBase {
36 | public:
37 | virtual status_t reconfigure(int64_t displayId, uint32_t width, uint32_t height);
38 | virtual status_t stop(int64_t displayId);
39 |
40 | virtual void keyEvent(int64_t displayId, uint32_t keyCode, bool isDown);
41 | virtual void touchEvent(int64_t displayId, int64_t pointerId, int32_t action, int32_t pressure, int32_t x, int32_t y);
42 | virtual void touchStylusButtonEvent(int64_t displayId, uint32_t button, bool isDown);
43 | virtual void touchStylusHoverEvent(int64_t displayId, int32_t action, int32_t x, int32_t y, int32_t distance, int32_t tilt_x, int32_t tilt_y);
44 | virtual void touchStylusEvent(int64_t displayId, int32_t action, int32_t pressure, int32_t x, int32_t y, int32_t tilt_x, int32_t tilt_y);
45 | virtual void pointerMotionEvent(int64_t displayId, int32_t x, int32_t y);
46 | virtual void pointerButtonEvent(int64_t displayId, uint32_t button, int32_t x, int32_t y, bool isDown);
47 | virtual void pointerScrollEvent(int64_t displayId, uint32_t value, bool isVertical);
48 |
49 | private:
50 | status_t start(int64_t displayId, uint32_t width, uint32_t height);
51 | status_t start_async(int64_t displayId, uint32_t width, uint32_t height);
52 |
53 | Mutex mLock;
54 | std::unordered_map mInputs;
55 | };
56 |
--------------------------------------------------------------------------------
/app/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.util.Properties
2 |
3 | plugins {
4 | id("com.android.application")
5 | id("org.jetbrains.kotlin.android")
6 | }
7 |
8 | val keystoreProperties = Properties().apply {
9 | rootProject.file("keystore.properties").inputStream().use { fis ->
10 | load(fis)
11 | }
12 | }
13 |
14 | android {
15 | namespace = "org.lindroid.ui"
16 | compileSdk = 34
17 |
18 | defaultConfig {
19 | applicationId = "org.lindroid.ui"
20 | minSdk = 33
21 | //noinspection OldTargetApi
22 | targetSdk = 34
23 | versionCode = 34
24 | versionName = "14"
25 | }
26 |
27 | buildFeatures {
28 | aidl = true
29 | }
30 | buildTypes {
31 | release {
32 | isMinifyEnabled = false
33 | proguardFiles(
34 | getDefaultProguardFile("proguard-android-optimize.txt"),
35 | "proguard-rules.pro",
36 | )
37 | signingConfig = signingConfigs["debug"]
38 | }
39 | }
40 | compileOptions {
41 | sourceCompatibility = JavaVersion.VERSION_17
42 | targetCompatibility = JavaVersion.VERSION_17
43 | }
44 | kotlinOptions {
45 | jvmTarget = "17"
46 | freeCompilerArgs += listOf(
47 | "-Xno-param-assertions",
48 | "-Xno-call-assertions",
49 | "-Xno-receiver-assertions",
50 | )
51 | }
52 | signingConfigs {
53 | named("debug") {
54 | keyAlias = keystoreProperties["keyAlias"].toString()
55 | keyPassword = keystoreProperties["keyPassword"].toString()
56 | storeFile = file("../" + keystoreProperties["storeFile"].toString())
57 | storePassword = keystoreProperties["storePassword"].toString()
58 | }
59 | }
60 | sourceSets {
61 | named("debug") {
62 | aidl.srcDirs("../../interfaces/perspective")
63 | }
64 | named("release") {
65 | aidl.srcDirs("../../interfaces/perspective")
66 | }
67 | }
68 | }
69 |
70 | dependencies {
71 | // $OUT/../../../soong/.intermediates/frameworks/base/framework/android_common/turbine-combined/framework.jar
72 | compileOnly(files("system_libs/framework.jar"))
73 | //noinspection GradleDependency - 14 QPR 3
74 | implementation("androidx.appcompat:appcompat:1.7.0-beta01")
75 | //noinspection GradleDependency - 14 QPR 3
76 | implementation("com.google.android.material:material:1.7.0-alpha03")
77 | //noinspection GradleDependency - 14 QPR 3
78 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
79 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable-anydpi/ic_cute_penguin.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
16 |
20 |
24 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sepolicy/perspectived.te:
--------------------------------------------------------------------------------
1 | # perspectived
2 | type perspectived, domain;
3 | typeattribute perspectived coredomain;
4 | type perspectived_exec, exec_type, file_type, system_file_type;
5 |
6 | init_daemon_domain(perspectived)
7 |
8 | permissive perspectived;
9 |
10 | allow perspectived perspectived_service:service_manager add;
11 |
12 | allow system_app perspectived:binder call;
13 |
14 | allow perspectived servicemanager:binder { call transfer };
15 |
16 | allow perspectived self:netlink_kobject_uevent_socket { create setopt bind read };
17 | allow perspectived self:capability { net_admin chown fsetid fowner sys_admin sys_resource kill };
18 |
19 | allow perspectived init:file r_file_perms;
20 | allow perspectived init:dir w_dir_perms;
21 |
22 | allow perspectived shell_exec:file rx_file_perms;
23 |
24 | allow perspectived cache_file:file create_file_perms;
25 | allow perspectived cache_file:dir create_dir_perms;
26 |
27 | allow perspectived tmpfs:file create_file_perms;
28 | allow perspectived tmpfs:dir create_dir_perms;
29 | allow perspectived tmpfs:lnk_file { read create setattr rename };
30 |
31 | allow perspectived lindroid_file:dir { rw_dir_perms create setattr };
32 | allow perspectived lindroid_file:file create_file_perms;
33 | allow perspectived lindroid_file:lnk_file { create setattr r_file_perms};
34 |
35 | allow perspectived system_app:fd use;
36 | allow perspectived system_app_data_file:file read;
37 |
38 | allow perspectived rootfs:dir mounton;
39 | allow perspectived system_file:file execute_no_trans;
40 |
41 |
42 | allow perspectived input_device:dir { getattr write add_name create };
43 | allow perspectived input_device:lnk_file create;
44 |
45 | allow perspectived gpu_device:chr_file { getattr write };
46 |
47 | #allow perspectived vendor_sysfs_usb_controller:dir search;
48 | allow perspectived sysfs_loop:dir { read open };
49 |
50 | allow perspectived property_socket:sock_file write;
51 | #allow perspectived init:unix_stream_socket connectto;
52 | #allow perspectived proc:filesystem remount;
53 | #allow perspectived fusectlfs:filesystem remount;
54 | #allow perspectived labeledfs:filesystem unmount;
55 | allow perspectived devpts:chr_file { read lock };
56 | #allow perspectived netlink_kobject_uevent_socket { getopt getattr write };
57 | #allow perspectived vendor_sysfs_battery_supply:dir search;
58 | #allow perspectived sysfs:file { read open getattr };
59 | allow perspectived sysfs:dir search;
60 | allow perspectived su:file { read open getattr ioctl };
61 | allow perspectived su:dir search;
62 | #allow perspectived lindroid_file:file { execmem execmod };
63 | #allow perspectived vendor_sysfs_graphics:dir search;
64 | allow perspectived lindroid_file:file link;
65 | allow perspectived cgroup:dir { read rmdir };
66 | allow perspectived cgroup_v2:file { read getattr };
67 | allow perspectived cgroup_v2:dir create;
68 | allow perspectived tmpfs:sock_file write;
69 |
70 | #allow perspectived systemd_udevd:netlink_kobject_uevent_socket { getopt getattr };
71 |
72 | allow perspectived unlabeled:file getattr;
73 |
74 | allow hwservicemanager perspectived:binder { transfer };
75 |
--------------------------------------------------------------------------------
/app/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lindroid
3 | Yes
4 | No
5 | Unsupported System
6 | This system does not support Lindroid.
7 | Container isn\'t running
8 | Lindroid container currently isn\'t running, do you want to start it?.
9 | Stop Linux subsystem
10 | Do you want to stop the Linux subsystem?
11 | Creating container…
12 | Please be patient
13 | No container found
14 | Do you want to create one?
15 | Notification channel
16 | Lindroid is running
17 | A virtual display has been put in background. Open the Lindroid app to return to the display.
18 | Failed to create container
19 | The container creation has failed. Please check logs for more information.
20 | Failed to start
21 | The container couldn\'t be started. Please check log for more details.
22 | Not running
23 | Running
24 | No file was selected
25 | Add container
26 | (Connecting…)
27 | Downloading…
28 | Download failed
29 | Downloaded %1$d of %2$d MB
30 | Cancel
31 | Duplicate name
32 | There already is a container with that name. Please choose another name.
33 | Delete container
34 | Do you really want to delete the container %s?
35 | Do you really want to stop the container %s?
36 | Stop container
37 | Container name
38 | Deleting container…
39 | Use local file...
40 | Large download
41 | To create the container, a large file has to be downloaded. Please make sure you are connected to unmetered internet before proceeding.
42 | Delete failed
43 | Deleting this container failed. Please check logcat for more information.
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vendor_lindroid
2 |
3 | ## What is it?
4 |
5 | Lindroid is an app (and patchset for AOSP) that allows you to run Linux containers using lxc with hardware acceleration powered by [libhybris](https://github.com/libhybris/libhybris).
6 |
7 | Lindroid consists of a native daemon responsible for overseeing the creation, execution and management of lxc containers. The app on the one hand is responsible for allowing the user to manage containers, and on the other hand it includes a very minimal implementation/emulation of the Android HWComposer HAL which is used by the container to display images by handing the buffers to the actual Android display stack. This design allows the container (or more specifically, the Wayland compositor like KWin inside the container) to use the same HWComposer API it uses for Halium-based mobile Linux distributions, but without interfering with the Android side of the graphics stack.
8 |
9 | This allows you to use an OpenGL ES accelerated desktop environment running on a mostly standard Linux distribution anywhere. Lindroid also supports multiple displays and input types, so you can attach your phone to a display, mouse and keyboard and enjoy the full convergence experience. Integrations between Android and the container such as network access, audio, touch/mouse/keyboard input, internal storage access and running the container in the background are working - and many more integration features are planned. You can create/start/stop/delete containers at runtime and seperate your work into multiple containers. In short: it's Linux, as an app. Without awkward restrictions like broken programs due to missing kernel support, performance limitations due to no hardware acceleration or poor GPU support (e.g. Mali or PowerVR not working) due to not being able to use Android's graphic drivers under Linux.
10 |
11 | Of course, this being an AOSP patchset also means you have to install an alternative operating system on your phone. While some proot or PC emulation-based solutions don't need this, we don't believe these are able to really provide an enjoyable desktop PC experience.
12 |
13 | ## How do I use it?
14 |
15 | Note: more detailed instructions will be added here in the future. You can also join our [Telegram chat](https://t.me/linux_on_droid) if you need help :)
16 |
17 | Patch your device kernel to enable these defconfigs:
18 |
19 | CONFIG_SYSVIPC=y
20 | CONFIG_UTS_NS=y
21 | CONFIG_PID_NS=y
22 | CONFIG_IPC_NS=y
23 | CONFIG_USER_NS=y
24 | CONFIG_NET_NS=y
25 | CONFIG_CGROUP_DEVICE=y
26 | CONFIG_CGROUP_FREEZER=y
27 | CONFIG_DRM_LINDROID_EVDI=y
28 |
29 | Clone vendor_lindroid, vendor_extra, libhybris and external_lxc repositories into an LMODroid (or LineageOS, if you pick [this patch](https://gerrit.libremobileos.com/c/LMODroid/platform_frameworks_native/+/12936)) tree and build it! You then will have a Lindroid app you can use in your app drawer.
30 |
31 | ## Community
32 |
33 | Join our [Telegram chat](https://t.me/linux_on_droid)!
34 |
35 | ## Credits
36 |
37 | The [libhybris](https://github.com/libhybris) maintainers for making this possible
38 | [Maru OS](https://github.com/maruos) for the idea, groundwork, and perspectived
39 | [Droidian](https://github.com/droidian)
40 | [UBports](https://ubports.com)
41 | [KDE](https://kde.org) for Plasma and KWin
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/app/src/main/java/org/lindroid/ui/HardwareService.java:
--------------------------------------------------------------------------------
1 | package org.lindroid.ui;
2 |
3 | import static org.lindroid.ui.NativeLib.nativeDisplayDestroyed;
4 | import static org.lindroid.ui.NativeLib.nativeStopInputDevice;
5 |
6 | import android.app.Notification;
7 | import android.app.NotificationChannel;
8 | import android.app.NotificationManager;
9 | import android.app.Service;
10 | import android.content.Intent;
11 | import android.os.Handler;
12 | import android.os.IBinder;
13 | import android.os.Looper;
14 |
15 | import androidx.annotation.Nullable;
16 |
17 | public class HardwareService extends Service {
18 |
19 | private static HardwareService instance = null;
20 | private final Handler handler = new Handler(Looper.getMainLooper());
21 | private AudioSocketServer audioSocketServer;
22 | private boolean started = false;
23 |
24 | public static HardwareService getInstance() {
25 | return instance;
26 | }
27 |
28 | @Override
29 | public void onCreate() {
30 | super.onCreate();
31 | NotificationManager nm = getSystemService(NotificationManager.class);
32 | NotificationChannel c = new NotificationChannel(Constants.NOTIFICATION_CHANNEL_ID,
33 | getString(R.string.notification_channel_name),
34 | NotificationManager.IMPORTANCE_LOW);
35 | c.setBlockable(true);
36 | c.enableLights(false);
37 | c.enableVibration(false);
38 | c.setSound(null, null);
39 | c.setShowBadge(true);
40 | nm.createNotificationChannel(c);
41 | handler.postDelayed(new Runnable() {
42 | @Override
43 | public void run() {
44 | // Activity is supposed to clean us up, but if it doesn't happen, let's not
45 | // waste user's battery life. This happens if container stops itself while
46 | // activity is not in foreground.
47 | if (ContainerManager.isAtLeastOneRunning() == null)
48 | stopSelf();
49 | else
50 | handler.postDelayed(this, 30 * 1000);
51 | }
52 | }, 30 * 1000);
53 | }
54 |
55 | @Override
56 | public int onStartCommand(Intent intent, int flags, int startId) {
57 | super.onStartCommand(intent, flags, startId);
58 | if (!started) {
59 | started = true;
60 | if (instance != null) {
61 | throw new RuntimeException("detected bug/memory leak?");
62 | }
63 | instance = this;
64 | audioSocketServer = new AudioSocketServer();
65 | }
66 | startForeground(Constants.NOTIFICATION_ID,
67 | new Notification.Builder(this, Constants.NOTIFICATION_CHANNEL_ID)
68 | .setContentTitle(getString(R.string.running_notif_title))
69 | .setContentText(getString(R.string.running_notif_message))
70 | .setSmallIcon(R.drawable.ic_cute_penguin)
71 | .build());
72 | return START_NOT_STICKY;
73 | }
74 |
75 | @Nullable
76 | @Override
77 | public IBinder onBind(Intent intent) {
78 | return null;
79 | }
80 |
81 | @Override
82 | public void onDestroy() {
83 | handler.removeCallbacksAndMessages(null);
84 | if (audioSocketServer != null) {
85 | audioSocketServer.stopServer();
86 | audioSocketServer = null;
87 | }
88 | instance = null;
89 | // these don't explode if display 0 doesn't exist
90 | nativeDisplayDestroyed(0);
91 | nativeStopInputDevice(0);
92 | started = false;
93 | super.onDestroy();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/app/src/main/cpp/ComposerImpl.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #include
13 | #include
14 | #include
15 | #include
16 |
17 | #define MAX_DEQUEUEABLE_BUFFERS 5
18 |
19 | using aidl::android::hardware::graphics::common::HardwareBuffer;
20 | using aidl::vendor::lindroid::composer::DisplayConfiguration;
21 | using aidl::vendor::lindroid::composer::IComposerCallback;
22 | using android::Mutex;
23 | using android::sp;
24 | using android::Surface;
25 | using android::SurfaceListener;
26 |
27 | namespace aidl {
28 | namespace vendor {
29 | namespace lindroid {
30 | namespace composer {
31 |
32 | typedef std::function vsync_callback_t;
33 |
34 | class VsyncThread {
35 | public:
36 | static int64_t now();
37 | static bool sleepUntil(int64_t t);
38 |
39 | void start(int64_t first, int64_t period);
40 | void stop();
41 | void setCallback(const vsync_callback_t &callback);
42 | void enableCallback(bool enable);
43 |
44 | private:
45 | void vsyncLoop();
46 | bool waitUntilNextVsync();
47 |
48 | std::thread mThread;
49 | int64_t mNextVsync{0};
50 | int64_t mPeriod{0};
51 |
52 | std::mutex mMutex;
53 | std::condition_variable mCondition;
54 | bool mStarted{false};
55 | vsync_callback_t mCallback;
56 | bool mCallbackEnabled{false};
57 | };
58 |
59 | struct ComposerDisplay {
60 | sp surface;
61 | ANativeWindow *nativeWindow;
62 | DisplayConfiguration displayConfig;
63 | bool plugged;
64 | sp listener;
65 | VsyncThread mVsyncThread;
66 | };
67 |
68 | class ComposerImpl : public BnComposer {
69 | public:
70 | virtual ndk::ScopedAStatus registerCallback(const std::shared_ptr &in_cb, int32_t sequenceId) override;
71 | virtual ndk::ScopedAStatus onHotplug(int64_t in_displayId, bool in_connected) override;
72 | virtual ndk::ScopedAStatus requestDisplay(int64_t in_displayId) override;
73 | virtual ndk::ScopedAStatus getActiveConfig(int64_t in_displayId, DisplayConfiguration *_aidl_return) override;
74 | virtual ndk::ScopedAStatus acceptChanges(int64_t in_displayId) override;
75 | virtual ndk::ScopedAStatus getReleaseFence(int64_t in_displayId, ndk::ScopedFileDescriptor *_aidl_return) override;
76 | virtual ndk::ScopedAStatus present(int64_t in_displayId, ndk::ScopedFileDescriptor *_aidl_return) override;
77 | virtual ndk::ScopedAStatus setPowerMode(int64_t in_displayId, int32_t in_mode) override;
78 | virtual ndk::ScopedAStatus setVsyncEnabled(int64_t in_displayId, int32_t in_enabled) override;
79 | virtual ndk::ScopedAStatus setBuffer(int64_t in_displayId, const HardwareBuffer &in_buffer, const ::ndk::ScopedFileDescriptor &in_fenceFd, int32_t *_aidl_return) override;
80 | virtual ndk::ScopedAStatus getUiRunning(bool *_aidl_return) override;
81 |
82 | void onSurfaceCreated(int64_t displayId, sp surface, ANativeWindow *nativeWindow);
83 | void onSurfaceChanged(int64_t displayId, sp surface, ANativeWindow *nativeWindow, int dpi, float refresh);
84 | void onSurfaceDestroyed(int64_t displayId, sp surface, ANativeWindow *nativeWindow);
85 | void onDisplayDestroyed(int64_t displayId);
86 |
87 | private:
88 | Mutex mLock;
89 |
90 | int32_t mSequenceId;
91 | std::shared_ptr mCallbacks;
92 | std::unordered_map mDisplays;
93 |
94 | bool m_ui_running = false;
95 | };
96 |
97 | } // namespace composer
98 | } // namespace lindroid
99 | } // namespace vendor
100 | } // namespace aidl
101 |
--------------------------------------------------------------------------------
/app/app/src/main/java/org/lindroid/ui/ContainerAdapter.kt:
--------------------------------------------------------------------------------
1 | package org.lindroid.ui
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.View.OnLongClickListener
8 | import android.view.ViewGroup
9 | import android.widget.TextView
10 | import androidx.appcompat.widget.AppCompatEditText
11 | import androidx.recyclerview.widget.RecyclerView
12 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
13 | import org.lindroid.ui.ContainerAdapter.ViewHolder
14 |
15 | class ContainerAdapter(
16 | val context: Context,
17 | firstData: Pair, List>,
18 | val startContainer: (String) -> Unit,
19 | val stopContainer: (String) -> Unit,
20 | val createContainer: (String) -> Unit,
21 | val deleteContainer: (String) -> Unit,
22 | ) : RecyclerView.Adapter() {
23 | var data = firstData
24 | @SuppressLint("NotifyDataSetChanged")
25 | set(value) {
26 | if (field != value) {
27 | field = value
28 | notifyDataSetChanged()
29 | }
30 | }
31 | private val containers
32 | get() = data.first
33 | private val running
34 | get() = data.second
35 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
36 | if (viewType == 1) {
37 | return AddViewHolder(
38 | LayoutInflater.from(context)
39 | .inflate(android.R.layout.simple_list_item_1, parent, false)
40 | )
41 | }
42 | return ContainerViewHolder(
43 | LayoutInflater.from(context)
44 | .inflate(android.R.layout.simple_list_item_2, parent, false)
45 | )
46 | }
47 |
48 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
49 | if (holder is ContainerViewHolder) {
50 | holder.name = containers[position]
51 | holder.running = running[position]
52 | }
53 | }
54 |
55 | override fun getItemCount(): Int {
56 | return containers.size + 1
57 | }
58 |
59 | override fun getItemViewType(position: Int): Int {
60 | return if (position == itemCount - 1) 1 else 0
61 | }
62 |
63 | abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
64 | View.OnClickListener {
65 | init {
66 | itemView.setOnClickListener(this)
67 | }
68 |
69 | abstract override fun onClick(v: View)
70 | }
71 |
72 | private inner class ContainerViewHolder(root: View) : ViewHolder(root),
73 | View.OnClickListener, OnLongClickListener {
74 | val mContainerName: TextView
75 | val mRunningIndicator: TextView
76 | var name: String? = null
77 | set(value) {
78 | if (field != value) {
79 | mContainerName.text = value
80 | field = value
81 | }
82 | }
83 | var running: Boolean = false
84 | set(value) {
85 | if (field != value) {
86 | mRunningIndicator.text =
87 | itemView.context.getString(if (value) R.string.running else R.string.not_running)
88 | field = value
89 | }
90 | }
91 |
92 | init {
93 | root.setOnLongClickListener(this)
94 | mContainerName = root.requireViewById(android.R.id.text1)
95 | mRunningIndicator = root.requireViewById(android.R.id.text2).apply {
96 | text = itemView.context.getString(R.string.not_running)
97 | }
98 | }
99 |
100 | override fun onClick(v: View) {
101 | if (running) {
102 | MaterialAlertDialogBuilder(context)
103 | .setTitle(R.string.stop_container)
104 | .setMessage(context.getString(R.string.stop_container_msg, name))
105 | .setPositiveButton(android.R.string.ok) { _, _ ->
106 | stopContainer(name!!)
107 | }
108 | .setNegativeButton(R.string.no) { _, _ -> }
109 | .setCancelable(false)
110 | .setIcon(R.drawable.ic_warning)
111 | .show()
112 | } else {
113 | startContainer(name!!)
114 | }
115 | }
116 |
117 | override fun onLongClick(v: View): Boolean {
118 | if (!running) {
119 | MaterialAlertDialogBuilder(context)
120 | .setTitle(R.string.delete_container)
121 | .setMessage(context.getString(R.string.delete_container_msg, name))
122 | .setPositiveButton(R.string.yes) { _, _ ->
123 | deleteContainer(name!!)
124 | }
125 | .setNegativeButton(R.string.no) { _, _ -> }
126 | .setCancelable(false)
127 | .setIcon(R.drawable.ic_warning)
128 | .show()
129 | }
130 | return true
131 | }
132 | }
133 |
134 | private inner class AddViewHolder(root: View) : ViewHolder(root), View.OnClickListener {
135 | init {
136 | (root as TextView).setText(R.string.add_container)
137 | }
138 |
139 | override fun onClick(v: View) {
140 | // TODO make edittext red when user enters junk that perspectived doesn't like
141 | // (it checks if name is [0-9a-zA-Z])
142 | val editText = AppCompatEditText(context)
143 | MaterialAlertDialogBuilder(context)
144 | .setTitle(R.string.set_name)
145 | .setView(editText)
146 | .setPositiveButton(android.R.string.ok) { _, _ ->
147 | createContainer(editText.text!!.toString())
148 | }
149 | .setNegativeButton(android.R.string.cancel) { _, _ -> }
150 | .setIcon(R.drawable.ic_help)
151 | .show()
152 | }
153 | }
154 | }
--------------------------------------------------------------------------------
/app/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/app/src/main/java/org/lindroid/ui/AudioSocketServer.java:
--------------------------------------------------------------------------------
1 | package org.lindroid.ui;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.media.AudioAttributes;
5 | import android.media.AudioFormat;
6 | import android.media.AudioManager;
7 | import android.media.AudioRecord;
8 | import android.media.AudioTrack;
9 | import android.media.MediaRecorder;
10 | import android.net.LocalServerSocket;
11 | import android.net.LocalSocket;
12 | import android.system.ErrnoException;
13 | import android.system.Os;
14 | import android.system.OsConstants;
15 | import android.util.Log;
16 |
17 | import java.io.File;
18 | import java.io.FileDescriptor;
19 | import java.io.IOException;
20 | import java.io.InputStream;
21 | import java.io.OutputStream;
22 | import java.util.Arrays;
23 | import java.util.concurrent.ExecutorService;
24 | import java.util.concurrent.Executors;
25 | import java.util.concurrent.TimeUnit;
26 |
27 | public class AudioSocketServer {
28 | private static final byte AUDIO_OUTPUT_PREFIX = 0x01;
29 | private static final byte AUDIO_INPUT_PREFIX = 0x02;
30 |
31 | private static final String TAG = "AudioSocketServer";
32 |
33 | private final ExecutorService clientExecutor = Executors.newCachedThreadPool();
34 | private final ExecutorService audioExecutor = Executors.newSingleThreadExecutor();
35 |
36 | private LocalServerSocket serverSocket;
37 | private boolean isRunning = false;
38 |
39 | private AudioTrack audioTrack;
40 | private AudioRecord audioRecord;
41 |
42 | public AudioSocketServer() {
43 | clientExecutor.execute(() -> {
44 | try {
45 | // Remove existing socket file if it exists
46 | File socketFile = new File(Constants.SOCKET_PATH);
47 | if (socketFile.exists()) {
48 | if (!socketFile.delete()) {
49 | Log.w(TAG, "failed to delete socket");
50 | }
51 | }
52 |
53 | try {
54 | // Create a UNIX domain socket file descriptor
55 | FileDescriptor socketFd = Os.socket(OsConstants.AF_UNIX, OsConstants.SOCK_STREAM, 0);
56 |
57 | // Bind the socket to the file path
58 | Os.bind(socketFd, Constants.createUnixSocketAddressObj(Constants.SOCKET_PATH));
59 |
60 | // Set the socket to listen for incoming connections
61 | Os.listen(socketFd, 50);
62 |
63 | serverSocket = new LocalServerSocket(socketFd);
64 | isRunning = true;
65 | Log.i(TAG, "Server started at " + Constants.SOCKET_PATH);
66 |
67 | // Initialize AudioTrack and AudioRecord
68 | initializeAudioTrack();
69 | initializeAudioRecord();
70 |
71 | while (isRunning) {
72 | LocalSocket clientSocket = serverSocket.accept();
73 | Log.i(TAG, "Accepted client");
74 |
75 | clientExecutor.execute(() -> handleClient(clientSocket));
76 | audioExecutor.execute(() -> sendMicDataToSocket(clientSocket));
77 | }
78 | } catch (ErrnoException e) {
79 | Log.e(TAG, "Error setting up server socket", e);
80 | }
81 | } catch (IOException e) {
82 | Log.e(TAG, "Error starting server", e);
83 | }
84 | });
85 | }
86 |
87 |
88 | private void sendMicDataToSocket(LocalSocket clientSocket) {
89 | try (OutputStream outputStream = clientSocket.getOutputStream()) {
90 | byte[] buffer = new byte[AudioRecord.getMinBufferSize(48000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)];
91 |
92 | while (isRunning) {
93 | // Reset buffer before reading
94 | Arrays.fill(buffer, (byte) 0);
95 |
96 | // Add the prefix at the start of the buffer
97 | buffer[0] = AUDIO_INPUT_PREFIX;
98 |
99 | // Read data from the microphone
100 | int bytesRead = audioRecord.read(buffer, 1, buffer.length - 1);
101 |
102 | if (bytesRead > 0) {
103 | // Add 1 to bytesRead to include the prefix in the output
104 | outputStream.write(buffer, 0, bytesRead + 1);
105 | } else if (bytesRead < 0) {
106 | Log.e(TAG, "Error reading microphone data: " + bytesRead);
107 | }
108 | }
109 | } catch (IOException e) {
110 | Log.e(TAG, "Error sending microphone data to client", e);
111 | }
112 | }
113 |
114 | private void initializeAudioTrack() {
115 | // Assuming audio is PCM 16-bit, 48000Hz, stereo
116 | int sampleRate = 48000;
117 | int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
118 | int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
119 |
120 | int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
121 |
122 | audioTrack = new AudioTrack(
123 | new AudioAttributes.Builder()
124 | .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
125 | .setUsage(AudioAttributes.USAGE_UNKNOWN)
126 | .setIsContentSpatialized(true)
127 | .build(),
128 | new AudioFormat.Builder()
129 | .setSampleRate(sampleRate)
130 | .setChannelMask(channelConfig)
131 | .setEncoding(audioFormat)
132 | .build(),
133 | minBufferSize,
134 | AudioTrack.MODE_STREAM,
135 | AudioManager.AUDIO_SESSION_ID_GENERATE
136 | );
137 |
138 | audioTrack.play();
139 | }
140 |
141 | @SuppressLint("MissingPermission") // we're system
142 | private void initializeAudioRecord() {
143 | int sampleRate = 48000;
144 | int channelConfig = AudioFormat.CHANNEL_IN_MONO;
145 | int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
146 | int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
147 |
148 | audioRecord = new AudioRecord(
149 | MediaRecorder.AudioSource.MIC,
150 | sampleRate,
151 | channelConfig,
152 | audioFormat,
153 | minBufferSize
154 | );
155 |
156 | audioRecord.startRecording();
157 | }
158 |
159 | private void handleClient(LocalSocket clientSocket) {
160 | try (InputStream inputStream = clientSocket.getInputStream()) {
161 | byte[] buffer = new byte[10240];
162 | int bytesRead;
163 |
164 | while (isRunning) {
165 | bytesRead = inputStream.read(buffer);
166 | if (bytesRead > 0) {
167 | byte prefix = buffer[0];
168 |
169 | if (prefix != AUDIO_OUTPUT_PREFIX)
170 | throw new IOException("got prefix " + prefix + " but wanted " + AUDIO_OUTPUT_PREFIX);
171 |
172 | // Write data to AudioTrack (if appropriate)
173 | audioTrack.write(buffer, 1, bytesRead - 1);
174 | Log.i(TAG, "Audio data written to AudioTrack: " + (bytesRead - 1) + " bytes");
175 | }
176 | }
177 | } catch (IOException e) {
178 | Log.e(TAG, "Error handling client", e);
179 | }
180 | }
181 |
182 | public void stopServer() {
183 | try {
184 | isRunning = false;
185 | audioExecutor.shutdownNow();
186 | clientExecutor.shutdownNow();
187 |
188 | if (!audioExecutor.awaitTermination(100, TimeUnit.MILLISECONDS)) {
189 | Log.w(TAG, "AudioExecutor shutdown timed out");
190 | }
191 | if (!clientExecutor.awaitTermination(100, TimeUnit.MILLISECONDS)) {
192 | Log.w(TAG, "ClientExecutor shutdown timed out");
193 | }
194 |
195 | if (serverSocket != null) {
196 | serverSocket.close();
197 | }
198 | if (audioTrack != null) {
199 | audioTrack.stop();
200 | audioTrack.release();
201 | }
202 | if (audioRecord != null) {
203 | audioRecord.stop();
204 | audioRecord.release();
205 | }
206 | } catch (InterruptedException | IOException e) {
207 | Log.e(TAG, "Error shutting down server", e);
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/app/app/src/main/cpp/native-lib.cpp:
--------------------------------------------------------------------------------
1 | #define ALOG_TAG "LindroidNative"
2 |
3 | #include
4 | #include
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | #include
12 | #include
13 |
14 | #include "ComposerImpl.h"
15 | #include "InputDevice.h"
16 |
17 | using aidl::vendor::lindroid::composer::ComposerImpl;
18 |
19 | using namespace android;
20 |
21 | static std::shared_ptr composer = nullptr;
22 | static sp inputDevice = nullptr;
23 |
24 | extern "C" void
25 | Java_org_lindroid_ui_NativeLib_nativeStartComposerService(
26 | JNIEnv *env, jclass /* clazz */) {
27 | ALOGI("Init native: Starting composer binder service...");
28 |
29 | composer = ndk::SharedRefBase::make();
30 |
31 | binder_status_t status = AServiceManager_addService(composer->asBinder().get(), "vendor.lindroid.composer");
32 | if (status != STATUS_OK) {
33 | ALOGE("Could not register composer binder service");
34 | }
35 |
36 | ABinderProcess_joinThreadPool();
37 | }
38 |
39 | extern "C" void
40 | Java_org_lindroid_ui_NativeLib_nativeSurfaceCreated(
41 | JNIEnv *env, jclass /* clazz */,
42 | jlong displayId, jobject surface) {
43 |
44 | sp sf = android_view_Surface_getSurface(env, surface);
45 | if (sf == nullptr) {
46 | ALOGE("Get Surface ERROR!");
47 | return;
48 | }
49 | ANativeWindow* nativeWindow = ANativeWindow_fromSurface(env, surface);
50 | if (nativeWindow == nullptr) {
51 | ALOGE("Get ANativeWindow ERROR!");
52 | return;
53 | }
54 | if (composer == nullptr) {
55 | int tryCount = 0;
56 | while (composer == nullptr && tryCount < 10) {
57 | ALOGE("Composer is not initialized! Try again...");
58 | usleep(1000000);
59 | tryCount++;
60 | }
61 | ALOGE("Composer is not initialized!");
62 | return;
63 | }
64 | composer->onSurfaceCreated(displayId, sf, nativeWindow);
65 | }
66 |
67 | extern "C" void
68 | Java_org_lindroid_ui_NativeLib_nativeSurfaceChanged(
69 | JNIEnv *env, jclass /* clazz */,
70 | jlong displayId, jobject surface, jint dpi, jfloat refresh) {
71 |
72 | sp sf = android_view_Surface_getSurface(env, surface);
73 | if (sf == nullptr) {
74 | ALOGE("Get Surface ERROR!");
75 | return;
76 | }
77 | ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
78 | if (nativeWindow == nullptr) {
79 | ALOGE("Get ANativeWindow ERROR!");
80 | return;
81 | }
82 | if (composer == nullptr) {
83 | int tryCount = 0;
84 | while (composer == nullptr && tryCount < 10) {
85 | ALOGE("Composer is not initialized! Try again...");
86 | usleep(1000000);
87 | tryCount++;
88 | }
89 | ALOGE("Composer is not initialized!");
90 | return;
91 | }
92 | composer->onSurfaceChanged(displayId, sf, nativeWindow, dpi, refresh);
93 | }
94 |
95 | extern "C" void
96 | Java_org_lindroid_ui_NativeLib_nativeSurfaceDestroyed(
97 | JNIEnv *env, jclass /* clazz */,
98 | jlong displayId, jobject surface) {
99 |
100 | sp sf = android_view_Surface_getSurface(env, surface);
101 | if (sf == nullptr) {
102 | ALOGE("Get Surface ERROR!");
103 | return;
104 | }
105 | ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
106 | if (nativeWindow == nullptr) {
107 | ALOGE("Get ANativeWindow ERROR!");
108 | return;
109 | }
110 | if (composer == nullptr) {
111 | int tryCount = 0;
112 | while (composer == nullptr && tryCount < 10) {
113 | ALOGE("Composer is not initialized! Try again...");
114 | usleep(1000000);
115 | tryCount++;
116 | }
117 | ALOGE("Composer is not initialized!");
118 | return;
119 | }
120 | composer->onSurfaceDestroyed(displayId, sf, nativeWindow);
121 |
122 | ALOGE("SurfaceDestroyed");
123 | }
124 |
125 | extern "C" void
126 | Java_org_lindroid_ui_NativeLib_nativeDisplayDestroyed(
127 | JNIEnv *env, jclass /* clazz */,
128 | jlong displayId) {
129 |
130 | if (composer == nullptr) {
131 | int tryCount = 0;
132 | while (composer == nullptr && tryCount < 10) {
133 | ALOGE("Composer is not initialized! Try again...");
134 | usleep(1000000);
135 | tryCount++;
136 | }
137 | ALOGE("Composer is not initialized!");
138 | return;
139 | }
140 | composer->onDisplayDestroyed(displayId);
141 |
142 | ALOGE("SurfaceDestroyed");
143 | }
144 |
145 | extern "C" jboolean
146 | Java_org_lindroid_ui_NativeLib_nativeGetUiRunning(JNIEnv *env, jclass /* clazz */) {
147 | if (composer == nullptr) {
148 | int tryCount = 0;
149 | while (composer == nullptr && tryCount < 10) {
150 | ALOGE("Composer is not initialized! Try again...");
151 | usleep(1000000);
152 | tryCount++;
153 | }
154 | ALOGE("Composer is not initialized!");
155 | return JNI_FALSE;
156 | }
157 | bool isUiRunning;
158 | composer->getUiRunning(&isUiRunning);
159 | return isUiRunning;
160 | }
161 |
162 | extern "C" void
163 | Java_org_lindroid_ui_NativeLib_nativeInitInputDevice(
164 | JNIEnv *env, jclass /* clazz */) {
165 |
166 | inputDevice = new InputDevice();
167 | }
168 |
169 | extern "C" void
170 | Java_org_lindroid_ui_NativeLib_nativeReconfigureInputDevice(
171 | JNIEnv *env, jclass /* clazz */,
172 | jlong displayId, jint width, jint height) {
173 |
174 | if (inputDevice == nullptr) {
175 | ALOGE("InputDevice is not initialized!");
176 | return;
177 | }
178 |
179 | inputDevice->reconfigure(displayId, width, height);
180 | }
181 |
182 | extern "C" void
183 | Java_org_lindroid_ui_NativeLib_nativeStopInputDevice(
184 | JNIEnv *env, jclass /* clazz */,
185 | jlong displayId) {
186 |
187 | if (inputDevice == nullptr) {
188 | ALOGE("InputDevice is not initialized!");
189 | return;
190 | }
191 |
192 | inputDevice->stop(displayId);
193 | }
194 |
195 | extern "C" void
196 | Java_org_lindroid_ui_NativeLib_nativeKeyEvent(
197 | JNIEnv *env, jclass /* clazz */,
198 | jlong displayId, jint keyCode, jboolean isDown) {
199 |
200 | if (inputDevice == nullptr) {
201 | ALOGE("InputDevice is not initialized!");
202 | return;
203 | }
204 |
205 | inputDevice->keyEvent(displayId, keyCode, isDown);
206 | }
207 |
208 | extern "C" void
209 | Java_org_lindroid_ui_NativeLib_nativeTouchEvent(
210 | JNIEnv *env, jclass /* clazz */,
211 | jlong displayId, jint pointerId, jint action, jint pressure, jint x, jint y) {
212 |
213 | if (inputDevice == nullptr) {
214 | ALOGE("InputDevice is not initialized!");
215 | return;
216 | }
217 |
218 | inputDevice->touchEvent(displayId, pointerId, action, pressure, x, y);
219 | }
220 |
221 | extern "C" void
222 | Java_org_lindroid_ui_NativeLib_nativeTouchStylusButtonEvent(
223 | JNIEnv *env, jobject /* this */,
224 | jlong displayId, jint button, jboolean isDown) {
225 |
226 | if (inputDevice == nullptr) {
227 | ALOGE("InputDevice is not initialized!");
228 | return;
229 | }
230 |
231 | inputDevice->touchStylusButtonEvent(displayId, button, isDown);
232 | }
233 |
234 | extern "C" void
235 | Java_org_lindroid_ui_NativeLib_nativeTouchStylusHoverEvent(
236 | JNIEnv *env, jobject /* this */,
237 | jlong displayId, jint action, jint x, jint y, jint distance, jint tilt_x, jint tilt_y) {
238 |
239 | if (inputDevice == nullptr) {
240 | ALOGE("InputDevice is not initialized!");
241 | return;
242 | }
243 |
244 | inputDevice->touchStylusHoverEvent(displayId, action, x, y, distance, tilt_x, tilt_y);
245 | }
246 |
247 | extern "C" void
248 | Java_org_lindroid_ui_NativeLib_nativeTouchStylusEvent(
249 | JNIEnv *env, jobject /* this */,
250 | jlong displayId, jint action, jint pressure, jint x, jint y, jint tilt_x, jint tilt_y) {
251 |
252 | if (inputDevice == nullptr) {
253 | ALOGE("InputDevice is not initialized!");
254 | return;
255 | }
256 |
257 | inputDevice->touchStylusEvent(displayId, action, pressure, x, y, tilt_x, tilt_y);
258 | }
259 |
260 | extern "C" void
261 | Java_org_lindroid_ui_NativeLib_nativePointerMotionEvent(
262 | JNIEnv *env, jclass /* clazz */,
263 | jlong displayId, jint x, jint y) {
264 |
265 | if (inputDevice == nullptr) {
266 | ALOGE("InputDevice is not initialized!");
267 | return;
268 | }
269 |
270 | inputDevice->pointerMotionEvent(displayId, x, y);
271 | }
272 |
273 | extern "C" void
274 | Java_org_lindroid_ui_NativeLib_nativePointerButtonEvent(
275 | JNIEnv *env, jclass /* clazz */,
276 | jlong displayId, jint button, jint x, jint y, jboolean isDown) {
277 |
278 | if (inputDevice == nullptr) {
279 | ALOGE("InputDevice is not initialized!");
280 | return;
281 | }
282 |
283 | inputDevice->pointerButtonEvent(displayId, button, x, y, isDown);
284 | }
285 |
286 | extern "C" void
287 | Java_org_lindroid_ui_NativeLib_nativePointerScrollEvent(
288 | JNIEnv *env, jclass /* clazz */,
289 | jlong displayId, jint value, jboolean isVertical) {
290 |
291 | if (inputDevice == nullptr) {
292 | ALOGE("InputDevice is not initialized!");
293 | return;
294 | }
295 |
296 | inputDevice->pointerScrollEvent(displayId, value, isVertical);
297 | }
298 |
--------------------------------------------------------------------------------
/app/app/src/main/java/org/lindroid/ui/ContainerManager.java:
--------------------------------------------------------------------------------
1 | package org.lindroid.ui;
2 |
3 | import android.content.ContentResolver;
4 | import android.net.Uri;
5 | import android.os.IBinder;
6 | import android.os.ParcelFileDescriptor;
7 | import android.os.RemoteException;
8 | import android.os.ServiceManager;
9 | import android.util.Log;
10 |
11 | import java.io.IOException;
12 | import java.util.ArrayList;
13 | import java.util.List;
14 | import java.util.Map;
15 | import java.util.concurrent.ConcurrentHashMap;
16 | import java.util.concurrent.CopyOnWriteArrayList;
17 |
18 | import vendor.lindroid.perspective.IPerspective;
19 |
20 | public class ContainerManager {
21 | private static final String TAG = "ContainerManager";
22 | private static IPerspective mPerspective;
23 |
24 | private static final Map logBuffers = new ConcurrentHashMap<>();
25 | private static final Map logThreads = new ConcurrentHashMap<>();
26 | private static final Map> listeners = new ConcurrentHashMap<>();
27 | private static final Map runningContainers = new ConcurrentHashMap<>();
28 |
29 | public interface LogUpdateListener {
30 | void onLogUpdated(String containerName, String latestLog);
31 | }
32 |
33 | private ContainerManager() {} // no init
34 |
35 | public static boolean isPerspectiveAvailable() {
36 | if (mPerspective != null) {
37 | //if (mPerspective.isBinderAlive()) TODO
38 | return true;
39 | //mPerspective = null;
40 | }
41 | // Fetch the Perspective service
42 | final IBinder binder = ServiceManager.getService(Constants.PERSPECTIVE_SERVICE_NAME);
43 | if (binder == null) {
44 | Log.e(TAG, "Failed to get binder from ServiceManager");
45 | return false;
46 | }
47 | mPerspective = IPerspective.Stub.asInterface(binder);
48 | return true;
49 | }
50 |
51 | public static void addLogUpdateListener(String containerName, LogUpdateListener listener) {
52 | listeners.computeIfAbsent(containerName, k -> new CopyOnWriteArrayList<>()).add(listener);
53 | }
54 |
55 | public static void removeLogUpdateListener(String containerName, LogUpdateListener listener) {
56 | List containerListeners = listeners.get(containerName);
57 | if (containerListeners != null) {
58 | containerListeners.remove(listener);
59 | }
60 | }
61 |
62 | public static boolean startFetchingLogs(String containerName) {
63 | if (logThreads.containsKey(containerName)) {
64 | Log.w(TAG, "Log fetching already running for container: " + containerName);
65 | return false;
66 | }
67 |
68 | runningContainers.put(containerName, true);
69 |
70 | logBuffers.putIfAbsent(containerName, new StringBuilder());
71 | Thread logThread = new Thread(() -> {
72 | while (Boolean.TRUE.equals(runningContainers.get(containerName))) {
73 | try {
74 | String logs = mPerspective.fetchLogs(containerName);
75 |
76 | if (logs != null && !logs.trim().isEmpty()) {
77 | // Append logs to buffer
78 | synchronized (logBuffers.get(containerName)) {
79 | logBuffers.get(containerName).append(logs).append("\n");
80 | }
81 |
82 | // Notify listeners
83 | List containerListeners = listeners.get(containerName);
84 | if (containerListeners != null) {
85 | for (LogUpdateListener listener : containerListeners) {
86 | listener.onLogUpdated(containerName, logs);
87 | }
88 | }
89 | }
90 |
91 | Thread.sleep(100);
92 |
93 | } catch (RemoteException | InterruptedException e) {
94 | Log.e(TAG, "Error fetching logs for container: " + containerName, e);
95 | }
96 | }
97 | logThreads.remove(containerName);
98 | });
99 | logThreads.put(containerName, logThread);
100 | logThread.start();
101 | return true;
102 | }
103 |
104 | public static void stopFetchingLogs(String containerName) {
105 | if (!logThreads.containsKey(containerName)) {
106 | Log.w(TAG, "Log fetching is not running for container: " + containerName);
107 | return;
108 | }
109 |
110 | runningContainers.put(containerName, false);
111 | Thread logThread = logThreads.get(containerName);
112 | if (logThread != null) {
113 | logThread.interrupt();
114 | try {
115 | logThread.join();
116 | } catch (InterruptedException e) {
117 | Log.e(TAG, "Error stopping log thread for container: " + containerName, e);
118 | }
119 | }
120 | logThreads.remove(containerName);
121 | }
122 |
123 | public static String getBufferedLogs(String containerName) {
124 | StringBuilder buffer = logBuffers.get(containerName);
125 | if (buffer == null) return "";
126 | synchronized (buffer) {
127 | return buffer.toString();
128 | }
129 | }
130 |
131 | public static void clearLogBuffer(String containerName) {
132 | StringBuilder buffer = logBuffers.get(containerName);
133 | if (buffer != null) {
134 | synchronized (buffer) {
135 | buffer.setLength(0);
136 | }
137 | }
138 | }
139 |
140 | public static boolean isRunning(String containerName) {
141 | if (!isPerspectiveAvailable())
142 | return false;
143 | try {
144 | return mPerspective.isRunning(containerName);
145 | } catch (RemoteException e) {
146 | mPerspective = null;
147 | throw new RuntimeException(e);
148 | }
149 | }
150 |
151 | public static String isAtLeastOneRunning() {
152 | for (String id : listContainers()) {
153 | if (isRunning(id))
154 | return id;
155 | }
156 | return null;
157 | }
158 |
159 | public static boolean start(String containerName) {
160 | if (!isPerspectiveAvailable())
161 | return false;
162 | try {
163 | if (mPerspective.start(containerName, true)) {
164 | Log.d(TAG, "Container " + containerName + " started successfully.");
165 | return true;
166 | } else {
167 | Log.e(TAG, "Container " + containerName + " failed to start.");
168 | return false;
169 | }
170 | } catch (RemoteException e) {
171 | mPerspective = null;
172 | throw new RuntimeException(e);
173 | }
174 | }
175 |
176 | public static boolean stop(String containerName) {
177 | if (!isPerspectiveAvailable())
178 | return false;
179 | try {
180 | if (mPerspective.stop(containerName)) {
181 | Log.d(TAG, "Container " + containerName + " stopped successfully.");
182 | return true;
183 | } else {
184 | Log.e(TAG, "Container " + containerName + " failed to stop.");
185 | return false;
186 | }
187 | } catch (RemoteException e) {
188 | mPerspective = null;
189 | throw new RuntimeException(e);
190 | }
191 | }
192 |
193 | public static String getLog(String containerName) {
194 | Log.d(TAG, "getLog called for container: " + containerName);
195 | String logs="";
196 | try {
197 | logs = mPerspective.fetchLogs("default");
198 | Log.d(TAG, "Container Logs: " + logs);
199 | } catch (RemoteException e) {
200 | e.printStackTrace(); // Handle the exception or log it
201 | Log.d(TAG, "Failed to fetch logs: " + e.getMessage());
202 | }
203 | return logs;
204 | }
205 |
206 | public static boolean addContainer(String containerName, ContentResolver cr, Uri rootfs) {
207 | if (!isPerspectiveAvailable())
208 | return false;
209 | try (ParcelFileDescriptor pfd = cr.openFileDescriptor(rootfs, "r")) {
210 | if (mPerspective.addContainer(containerName, pfd)) {
211 | Log.d(TAG, "Container " + containerName + " added successfully.");
212 | return true;
213 | } else {
214 | Log.e(TAG, "Container " + containerName + " failed to be added.");
215 | return false;
216 | }
217 | } catch (RemoteException e) {
218 | mPerspective = null;
219 | throw new RuntimeException(e);
220 | } catch (IOException e) {
221 | Log.e(TAG, "IOException in addContainer", e);
222 | return false;
223 | }
224 | }
225 |
226 | public static boolean deleteContainer(String containerName) {
227 | if (!isPerspectiveAvailable())
228 | return false;
229 | try {
230 | if (mPerspective.deleteContainer(containerName)) {
231 | Log.d(TAG, "Container " + containerName + " deleted successfully.");
232 | return true;
233 | } else {
234 | Log.e(TAG, "Container " + containerName + " failed to be deleted.");
235 | return false;
236 | }
237 | } catch (RemoteException e) {
238 | mPerspective = null;
239 | throw new RuntimeException(e);
240 | }
241 | }
242 |
243 | public static List listContainers() {
244 | if (!isPerspectiveAvailable())
245 | return new ArrayList<>();
246 | try {
247 | return mPerspective.listContainers();
248 | } catch (RemoteException e) {
249 | mPerspective = null;
250 | throw new RuntimeException(e);
251 | }
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/perspectived/LXCContainerManager.cpp:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 The Maru OS Project
3 | * Copyright 2024-2025 Lindroid
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | #include
19 | #include
20 | #include
21 | #include
22 | #include
23 | #include
24 | #include
25 |
26 | #include