├── app
├── app
│ ├── .cxx
│ │ ├── ndk_locator_record_1e2102h1.json
│ │ └── ndk_locator_record_1e2102h1_key.json
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── drawable
│ │ │ │ │ ├── logo.png
│ │ │ │ │ ├── rounded_button.xml
│ │ │ │ │ ├── rounded_dialog.xml
│ │ │ │ │ ├── ic_stop_24px.xml
│ │ │ │ │ ├── red_gradient.xml
│ │ │ │ │ ├── green_gradient.xml
│ │ │ │ │ ├── ic_play_arrow_24px.xml
│ │ │ │ │ ├── cellular.xml
│ │ │ │ │ ├── ic_done_black_18dp.xml
│ │ │ │ │ ├── ic_code_24px.xml
│ │ │ │ │ ├── ic_info_24px.xml
│ │ │ │ │ ├── ic_videocam_24px.xml
│ │ │ │ │ ├── ic_chat_24px.xml
│ │ │ │ │ ├── vpn.xml
│ │ │ │ │ ├── ic_close_24.xml
│ │ │ │ │ ├── ethernet.xml
│ │ │ │ │ ├── ic_print_24px.xml
│ │ │ │ │ ├── ic_favorite_24px.xml
│ │ │ │ │ ├── circle_drawable.xml
│ │ │ │ │ ├── ic_baseline_article_24.xml
│ │ │ │ │ ├── ic_baseline_heart_broken_24.xml
│ │ │ │ │ ├── wifi.xml
│ │ │ │ │ ├── ic_usb_24px.xml
│ │ │ │ │ ├── ic_help.xml
│ │ │ │ │ ├── ic_baseline_extension_24.xml
│ │ │ │ │ ├── ic_share_24.xml
│ │ │ │ │ └── ic_settings_24px.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ ├── ic_launcher_background.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── values
│ │ │ │ │ ├── ids.xml
│ │ │ │ │ ├── attrs.xml
│ │ │ │ │ ├── colors.xml
│ │ │ │ │ ├── arrays.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── mipmap-anydpi-v26
│ │ │ │ │ ├── ic_launcher.xml
│ │ │ │ │ └── ic_launcher_round.xml
│ │ │ │ ├── menu
│ │ │ │ │ ├── main_appbar.xml
│ │ │ │ │ └── nav_menu.xml
│ │ │ │ ├── layout
│ │ │ │ │ ├── bootstrap_spinner_view.xml
│ │ │ │ │ ├── activity_webinterface.xml
│ │ │ │ │ ├── view_ip_address.xml
│ │ │ │ │ ├── dialog_camera_preview.xml
│ │ │ │ │ ├── activity_main.xml
│ │ │ │ │ ├── extension_view.xml
│ │ │ │ │ ├── view_installation_item.xml
│ │ │ │ │ ├── fragment_extensions.xml
│ │ │ │ │ ├── view_usb_devices_item.xml
│ │ │ │ │ ├── fragment_about.xml
│ │ │ │ │ ├── fragment_terminal_sheet.xml
│ │ │ │ │ ├── fragment_server.xml
│ │ │ │ │ ├── activity_landing.xml
│ │ │ │ │ ├── view_status_card.xml
│ │ │ │ │ └── select_driver_bottom_sheet.xml
│ │ │ │ ├── navigation
│ │ │ │ │ └── navigation_graph.xml
│ │ │ │ ├── xml
│ │ │ │ │ ├── device_filter.xml
│ │ │ │ │ └── main_preferences.xml
│ │ │ │ └── drawable-v24
│ │ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── ic_launcher-playstore.png
│ │ │ ├── java
│ │ │ │ └── com
│ │ │ │ │ └── octo4a
│ │ │ │ │ ├── serial
│ │ │ │ │ ├── VSPListener.java
│ │ │ │ │ ├── VSPPty.kt
│ │ │ │ │ ├── SerialData.kt
│ │ │ │ │ └── PrinterProber.kt
│ │ │ │ │ ├── utils
│ │ │ │ │ ├── AnimationExtensions.kt
│ │ │ │ │ ├── ManualLifecycleOwner.kt
│ │ │ │ │ ├── CancelableTimer.kt
│ │ │ │ │ ├── WaitableEvent.kt
│ │ │ │ │ ├── ProgressTrackingInputStream.kt
│ │ │ │ │ ├── preferences
│ │ │ │ │ │ ├── MainPreferences.kt
│ │ │ │ │ │ └── Preferences.kt
│ │ │ │ │ ├── Octo4aWakeLock.kt
│ │ │ │ │ ├── ProcessUtils.kt
│ │ │ │ │ └── Extensions.kt
│ │ │ │ │ ├── ui
│ │ │ │ │ ├── BugReportingDialogExtension.kt
│ │ │ │ │ ├── views
│ │ │ │ │ │ ├── ExtensionView.kt
│ │ │ │ │ │ ├── IPAddressView.kt
│ │ │ │ │ │ ├── InstallationProgressItem.kt
│ │ │ │ │ │ ├── UsbDeviceView.kt
│ │ │ │ │ │ └── StatusView.kt
│ │ │ │ │ ├── fragments
│ │ │ │ │ │ ├── AboutFragment.kt
│ │ │ │ │ │ ├── ExtensionsFragment.kt
│ │ │ │ │ │ └── TerminalSheetDialog.kt
│ │ │ │ │ └── WebinterfaceActivity.kt
│ │ │ │ │ ├── Octo4aApplication.kt
│ │ │ │ │ ├── repository
│ │ │ │ │ ├── GithubRepository.kt
│ │ │ │ │ ├── SystemStatusRepository.kt
│ │ │ │ │ ├── FIFOEventRepository.kt
│ │ │ │ │ └── LoggerRepository.kt
│ │ │ │ │ ├── BootReceiver.kt
│ │ │ │ │ ├── AppModule.kt
│ │ │ │ │ ├── camera
│ │ │ │ │ ├── MJpegServer.kt
│ │ │ │ │ ├── MjpegResponse.kt
│ │ │ │ │ ├── NativeCameraUtils.kt
│ │ │ │ │ └── RotateUtils.kt
│ │ │ │ │ └── viewmodel
│ │ │ │ │ ├── StatusViewModel.kt
│ │ │ │ │ └── InstallationViewModel.kt
│ │ │ ├── cpp
│ │ │ │ ├── util.h
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── real_pty.h
│ │ │ │ ├── openpty.c
│ │ │ │ ├── ioctl-hook.c
│ │ │ │ └── yuv2rgb.cpp
│ │ │ └── AndroidManifest.xml
│ │ ├── test
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── octo4a
│ │ │ │ └── ExampleUnitTest.kt
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── com
│ │ │ └── octo4a
│ │ │ └── ExampleInstrumentedTest.kt
│ └── proguard-rules.pro
├── .gitignore
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── settings.gradle.kts
├── build.gradle.kts
├── gradle.properties
└── gradlew.bat
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── title.txt
│ ├── changelogs
│ ├── 1000003.txt
│ └── 1000002.txt
│ ├── short_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── 3.png
│ └── full_description.txt
├── .github
├── FUNDING.yml
├── OTG-connection.png
├── readme-banner.png
└── workflows
│ └── build-app.yml
├── scripts
├── setup-plugin-extras.sh
├── setup-octo4a.sh
├── setup-klipper.sh
└── comm-fix.py
├── bootstrap-builder
├── go.mod
├── fake_dpkg.sh
├── properties.sh
├── go.sum
├── replace_paths.go
└── dpkg_replacer.go
├── local.properties
├── .gitignore
└── README.md
/app/app/.cxx/ndk_locator_record_1e2102h1.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .gradle/*
3 | local.properties
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | OctoPrint for Android
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1000003.txt:
--------------------------------------------------------------------------------
1 | Fix FDroid release
2 |
--------------------------------------------------------------------------------
/app/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /.cxx
3 | /src/main/obj
4 | /debug
5 | /release
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1000002.txt:
--------------------------------------------------------------------------------
1 | First FDroid release
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [feelfreelinux]
2 | custom: ["https://www.paypal.me/feelfreelinux"]
3 |
--------------------------------------------------------------------------------
/scripts/setup-plugin-extras.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 | apk add python3-dev build-base gcc g++
5 |
--------------------------------------------------------------------------------
/.github/OTG-connection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/.github/OTG-connection.png
--------------------------------------------------------------------------------
/.github/readme-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/.github/readme-banner.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Use your old Android device as an OctoPrint server.
2 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/drawable/logo.png
--------------------------------------------------------------------------------
/app/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/app/.cxx/ndk_locator_record_1e2102h1_key.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdkFolder": "/Users/cspot/Library/Android/sdk",
3 | "sideBySideNdkFolderNames": []
4 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/serial/VSPListener.java:
--------------------------------------------------------------------------------
1 | package com.octo4a.serial;
2 |
3 | public interface VSPListener {
4 | void onDataReceived(SerialData data);
5 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feelfreelinux/octo4a/HEAD/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/bootstrap-builder/go.mod:
--------------------------------------------------------------------------------
1 | module dpkg_replacer
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
7 | github.com/mitchellh/gox v1.0.1 // indirect
8 | github.com/ulikunitz/xz v0.5.10
9 | )
10 |
--------------------------------------------------------------------------------
/app/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Mar 07 22:49:50 CET 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/app/app/src/main/cpp/util.h:
--------------------------------------------------------------------------------
1 | #ifndef _UTIL_H_
2 | #define _UTIL_H_
3 |
4 | #ifdef DEBUG_TRACE
5 | void _trace(const char* format, ...);
6 | #endif
7 |
8 | #ifdef DEBUG_TRACE
9 | #define TRACE(X) _trace X;
10 | #else /*DEBUG_TRACE*/
11 | #define TRACE(X)
12 | #endif /*DEBUG_TRACE*/
13 |
14 | #endif //_UTIL_H_
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/rounded_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/bootstrap-builder/fake_dpkg.sh:
--------------------------------------------------------------------------------
1 | #!/data/data/com.octo4a/files/usr/bin/bash
2 | for var in "$@"
3 | do
4 | if [[ "$var" == *.deb ]]
5 | then
6 | >&2 echo "Replacing deb $var"
7 | dpkg_replacer $var
8 | rm $var
9 | mv $var.replaced $var
10 |
11 | fi
12 | done
13 | /data/data/com.octo4a/files/usr/bin/realDpkg $@
14 |
--------------------------------------------------------------------------------
/app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | ## This file must *NOT* be checked into Version Control Systems,
2 | # as it contains information specific to your local configuration.
3 | #
4 | # Location of the SDK. This is only used by Gradle.
5 | # For customization when using a Version Control System, please read the
6 | # header note.
7 | #Sun Feb 05 13:08:54 UTC 2023
8 |
--------------------------------------------------------------------------------
/app/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | include(":app")
2 | rootProject.name = "OctoPrint for Android"
3 | pluginManagement {
4 | repositories {
5 | google()
6 | jcenter()
7 | gradlePluginPortal()
8 | mavenCentral()
9 | maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
10 | }
11 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/rounded_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
8 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_stop_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/red_gradient.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/green_gradient.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_play_arrow_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/cellular.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_done_black_18dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/menu/main_appbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_code_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/AnimationExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils
2 |
3 | import android.view.View
4 | import android.view.animation.LinearInterpolator
5 |
6 | var View.animatedAlpha: Float
7 | get() = alpha
8 | set(value) {
9 | animate().apply {
10 | interpolator = LinearInterpolator()
11 | duration = 300
12 | alpha(value)
13 | start()
14 | }
15 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_info_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/serial/VSPPty.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.serial
2 |
3 | class VSPPty {
4 | init {
5 | System.loadLibrary("vsp-pty")
6 | }
7 |
8 | external fun setVSPListener(listener: VSPListener)
9 | external fun writeData(data: ByteArray)
10 | external fun getBaudrate(data: Int): Int
11 | external fun runPtyThread()
12 | external fun cancelPtyThread()
13 | external fun createEventPipe()
14 |
15 | }
--------------------------------------------------------------------------------
/app/app/src/test/java/com/octo4a/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_videocam_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_chat_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/serial/SerialData.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.serial
2 |
3 | import com.octo4a.utils.isBitSet
4 |
5 | class SerialData(val data: ByteArray, val baudrate: Int, val c_iflag: Int, val c_oflag: Int, val c_cflag: Int, val c_lflag: Int) {
6 | companion object {
7 | const val TIOCPKT_DATA = 0
8 | }
9 |
10 | val isStartPacket = data[0].toInt().isBitSet(TIOCPKT_DATA)
11 | val serialData: ByteArray
12 | get() = data.copyOfRange(1, data.size)
13 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/vpn.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_close_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ethernet.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_print_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_favorite_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | project(Octo4a)
2 | cmake_minimum_required(VERSION 3.4.1)
3 |
4 | add_library(vsp-pty SHARED
5 | vsp-pty.cpp openpty.c)
6 |
7 | target_link_libraries(vsp-pty
8 | android
9 | log)
10 |
11 | add_library(yuv2rgb SHARED
12 | yuv2rgb.cpp)
13 |
14 | target_link_libraries(yuv2rgb
15 | android)
16 |
17 | add_library(ioctl-hook SHARED
18 | ioctl-hook.c)
19 |
20 | target_link_libraries(ioctl-hook
21 | android)
--------------------------------------------------------------------------------
/app/app/src/main/cpp/real_pty.h:
--------------------------------------------------------------------------------
1 | #ifdef __cplusplus
2 | extern "C" {
3 | #endif
4 | #ifndef _PTY_H
5 | #define _PTY_H 1
6 |
7 | #include
8 |
9 | #include
10 | #include
11 |
12 | /* Create pseudo tty master slave pair with NAME and set terminal
13 | attributes according to TERMP and WINP and return handles for both
14 | ends in AMASTER and ASLAVE. */
15 | int openpty (int *__amaster, int *__aslave, char *__name,
16 | struct termios *__termp, struct winsize *__winp);
17 |
18 | #ifdef __cplusplus
19 | }
20 | #endif
21 | #endif /* pty.h */
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/circle_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
9 |
12 |
17 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_baseline_article_24.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_baseline_heart_broken_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/wifi.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_usb_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_help.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_baseline_extension_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | gradlePluginPortal()
4 | jcenter()
5 | google()
6 | mavenCentral()
7 | }
8 | dependencies {
9 | classpath("com.android.tools.build:gradle:7.1.3")
10 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30")
11 | classpath("com.bugsnag:bugsnag-android-gradle-plugin:7.+")
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | google()
18 | mavenCentral()
19 | jcenter()
20 | maven { url = uri("https://jitpack.io") }
21 | maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
22 | }
23 | }
24 |
25 | tasks.register("clean", Delete::class){
26 | delete(rootProject.buildDir)
27 | }
28 |
--------------------------------------------------------------------------------
/app/app/src/androidTest/java/com/octo4a/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.octo4a", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/menu/nav_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/ManualLifecycleOwner.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.LifecycleRegistry
6 |
7 | class ManualLifecycleOwner : LifecycleOwner {
8 | private val lifecycleRegistry: LifecycleRegistry
9 |
10 | init {
11 | lifecycleRegistry = LifecycleRegistry(this)
12 | lifecycleRegistry.currentState = Lifecycle.State.CREATED
13 | }
14 |
15 | fun start() {
16 | lifecycleRegistry.currentState = Lifecycle.State.RESUMED
17 | }
18 |
19 | fun stop() {
20 | lifecycleRegistry.currentState = Lifecycle.State.CREATED
21 | }
22 |
23 | override fun getLifecycle(): Lifecycle {
24 | return lifecycleRegistry
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_share_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/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.kts.
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
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/CancelableTimer.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import java.util.Timer
6 | import java.util.TimerTask
7 |
8 | class CancelableTimer(private val handler: Handler = Handler(Looper.getMainLooper())) {
9 | private var timer: Timer? = null
10 | private var task: TimerTask? = null
11 |
12 | fun start(delay: Long, callback: () -> Unit) {
13 | timer?.cancel() // Cancel any existing timer
14 | timer = Timer()
15 | task =
16 | object : TimerTask() {
17 | override fun run() {
18 | handler.post { callback() }
19 | }
20 | }
21 | timer?.schedule(task, delay)
22 | }
23 |
24 | fun cancel() {
25 | task?.cancel()
26 | timer?.cancel()
27 | timer = null
28 | task = null
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/WaitableEvent.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils
2 |
3 | import java.util.concurrent.locks.ReentrantLock
4 | import kotlin.concurrent.withLock
5 |
6 | class WaitableEvent {
7 | private val lock = ReentrantLock()
8 | private val condition = lock.newCondition()
9 | private var isSet = false
10 |
11 | fun wait(autoreset: Boolean = false) {
12 | lock.withLock {
13 | while (!isSet) {
14 | condition.await()
15 | }
16 | if( autoreset ) {
17 | reset()
18 | }
19 | }
20 | }
21 |
22 | fun set() {
23 | lock.withLock {
24 | isSet = true
25 | condition.signalAll()
26 | }
27 | }
28 |
29 | fun reset() {
30 | lock.withLock {
31 | isSet = false
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/bootstrap_spinner_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
21 |
--------------------------------------------------------------------------------
/bootstrap-builder/properties.sh:
--------------------------------------------------------------------------------
1 | TERMUX_ANDROID_BUILD_TOOLS_VERSION=30.0.3
2 | TERMUX_NDK_VERSION_NUM=21
3 | TERMUX_NDK_REVISION="d"
4 | TERMUX_NDK_VERSION=$TERMUX_NDK_VERSION_NUM$TERMUX_NDK_REVISION
5 |
6 | if [ "${TERMUX_PACKAGES_OFFLINE-false}" = "true" ]; then
7 | export ANDROID_HOME=${TERMUX_SCRIPTDIR}/build-tools/android-sdk
8 | export NDK=${TERMUX_SCRIPTDIR}/build-tools/android-ndk
9 | else
10 | : "${ANDROID_HOME:="${HOME}/lib/android-sdk"}"
11 | : "${NDK:="${HOME}/lib/android-ndk"}"
12 | fi
13 |
14 | # Termux packages configuration.
15 | TERMUX_APP_PACKAGE="com.termux"
16 | TERMUX_BASE_DIR="/data/data/${TERMUX_APP_PACKAGE}/files"
17 | TERMUX_CACHE_DIR="/data/data/${TERMUX_APP_PACKAGE}/cache"
18 | TERMUX_ANDROID_HOME="${TERMUX_BASE_DIR}/home"
19 | TERMUX_PREFIX="${TERMUX_BASE_DIR}/usr"
20 |
21 | # Allow to override setup.
22 | if [ -f "$HOME/.termuxrc" ]; then
23 | . "$HOME/.termuxrc"
24 | fi
25 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/ProgressTrackingInputStream.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils
2 |
3 | import java.io.FilterInputStream
4 | import java.io.InputStream
5 | class ProgressTrackingInputStream(
6 | inputStream: InputStream,
7 | private val progressListener: (Long) -> Unit
8 | ) : FilterInputStream(inputStream) {
9 |
10 | private var totalBytesRead: Long = 0
11 |
12 | override fun read(): Int {
13 | val byte = super.read()
14 | if (byte >= 0) {
15 | totalBytesRead++
16 | progressListener(totalBytesRead)
17 | }
18 | return byte
19 | }
20 |
21 | override fun read(b: ByteArray, off: Int, len: Int): Int {
22 | val bytesRead = super.read(b, off, len)
23 | if (bytesRead > 0) {
24 | totalBytesRead += bytesRead
25 | progressListener(totalBytesRead)
26 | }
27 | return bytesRead
28 | }
29 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #43a047
4 | #76d275
5 | #00701a
6 | #1976d2
7 | #63a4ff
8 | #004ba0
9 | #000000
10 | #ffffff
11 |
12 | #FFE082
13 | #d35400
14 | #1FFFFFFF
15 | #000000
16 | #ff4CAF50
17 | #1D1F21
18 | #C5C8C6
19 |
20 |
--------------------------------------------------------------------------------
/bootstrap-builder/go.sum:
--------------------------------------------------------------------------------
1 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
2 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
3 | github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
4 | github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
5 | github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI=
6 | github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4=
7 | github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
8 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
9 | github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
10 | github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
11 |
--------------------------------------------------------------------------------
/app/app/src/main/cpp/openpty.c:
--------------------------------------------------------------------------------
1 | #include "pty.h"
2 |
3 | #include
4 | #include
5 | #include "util.h"
6 |
7 | #define HAVE_openpty
8 | int openpty(int *amaster, int *aslave, char *name, struct termios *termp, struct winsize *winp)
9 | {
10 | char buf[512];
11 | int master, slave;
12 |
13 | master = open("/dev/ptmx", O_RDWR);
14 | if (master == -1) return -1;
15 | if (grantpt(master) || unlockpt(master) || ptsname_r(master, buf, sizeof buf)) goto fail;
16 |
17 | slave = open(buf, O_RDWR | O_NOCTTY);
18 | if (slave == -1) goto fail;
19 |
20 | /* XXX Should we ignore errors here? */
21 | if (termp) tcsetattr(slave, TCSAFLUSH, termp);
22 | if (winp) ioctl(slave, TIOCSWINSZ, winp);
23 |
24 | *amaster = master;
25 | *aslave = slave;
26 | if (name != NULL) strcpy(name, buf);
27 | return 0;
28 |
29 | fail:
30 | close(master);
31 | return -1;
32 | }
--------------------------------------------------------------------------------
/bootstrap-builder/replace_paths.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "log"
7 | "os"
8 | "path/filepath"
9 | )
10 |
11 | var REPLACE_FROM = []byte("com.termux")
12 | var REPLACE_TO = []byte("com.octo4a")
13 |
14 | func main() {
15 | if len(os.Args) < 2 {
16 | log.Fatalf("usage: replace_path ")
17 | }
18 | err := filepath.Walk(os.Args[1],
19 | func(path string, info os.FileInfo, err error) error {
20 | if err != nil {
21 | return err
22 | }
23 | if info.IsDir() {
24 | return nil
25 | }
26 | data, err := os.ReadFile(path)
27 | if err != nil {
28 | return fmt.Errorf("failed to read file %v: %w", path, err)
29 | }
30 | out := bytes.Replace(data, REPLACE_FROM, REPLACE_TO, -1)
31 | err = os.WriteFile(path, out, info.Mode())
32 | if err != nil {
33 | return fmt.Errorf("failed to read write %v: %w", path, err)
34 | }
35 | return nil
36 | })
37 | if err != nil {
38 | log.Fatal(err)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - 90 degree
5 | - 180 degree
6 | - 270 degree
7 | - Do not rotate
8 |
9 |
10 | - 90
11 | - 180
12 | - 270
13 | - 0
14 |
15 |
16 |
17 | - 5 FPS
18 | - 10 FPS
19 | - 15 FPS
20 | - 20 FPS
21 | - 30 FPS
22 | - Do not limit
23 |
24 |
25 | - 5
26 | - 10
27 | - 15
28 | - 20
29 | - 30
30 | - -1
31 |
32 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/activity_webinterface.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/scripts/setup-octo4a.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 | # install required dependencies
5 | apk add py3-pip py3-yaml py3-regex py3-zeroconf py3-netifaces py3-cffi py3-psutil unzip py3-pillow ttyd ffmpeg
6 |
7 | mkdir -p /root/extensions/ttyd
8 | cat << EOF > /root/extensions/ttyd/manifest.json
9 | {
10 | "title": "Remote web terminal (ttyd)",
11 | "description": "Uses port 5002; User root / ssh password"
12 | }
13 | EOF
14 |
15 | echo "octoprint" > /root/.octoCredentials
16 | cat << EOF > /root/extensions/ttyd/start.sh
17 | #!/bin/sh
18 | ttyd -p 5002 --credential root:\$(cat /root/.octoCredentials) bash
19 | EOF
20 |
21 | cat << EOF > /root/extensions/ttyd/kill.sh
22 | #!/bin/sh
23 | pkill ttyd
24 | EOF
25 | chmod +x /root/extensions/ttyd/start.sh
26 | chmod +x /root/extensions/ttyd/kill.sh
27 | chmod 777 /root/extensions/ttyd/start.sh
28 | chmod 777 /root/extensions/ttyd/kill.sh
29 |
30 | pip3 install -U packaging --ignore-installed
31 | pip3 install https://github.com/feelfreelinux/octo4a-argon2-mock/archive/main.zip
32 | touch /home/octoprint/.argon-fix
33 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/BugReportingDialogExtension.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui
2 |
3 | import android.app.Activity
4 | import android.app.AlertDialog
5 | import com.octo4a.Octo4aApplication
6 | import com.octo4a.R
7 | import com.octo4a.utils.preferences.MainPreferences
8 |
9 | fun Activity.showBugReportingDialog(prefs: MainPreferences) {
10 | if (!prefs.hasAskedAboutReporting) {
11 | val builder = AlertDialog.Builder(this)
12 | builder.apply {
13 | setTitle(getString(R.string.bugreport_dialog_title))
14 | setMessage(getString(R.string.bugreport_dialog_msg))
15 | setNegativeButton(getString(R.string.bugreport_dialog_dismiss)) { dialog, id ->
16 | prefs.hasAskedAboutReporting = true
17 | prefs.enableBugReporting = false
18 | }
19 | setPositiveButton(getString(R.string.bugreport_dialog_enable)) { dialog, id ->
20 | prefs.hasAskedAboutReporting = true
21 | prefs.enableBugReporting = true
22 | (application as Octo4aApplication).startBugsnag()
23 | }
24 | }
25 | val dialog = builder.create()
26 | dialog.show()
27 | }
28 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable/ic_settings_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/app/src/main/res/navigation/navigation_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
18 |
19 |
24 |
25 |
30 |
31 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/view_ip_address.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
23 |
24 |
31 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/Octo4aApplication.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a
2 |
3 | import androidx.multidex.MultiDexApplication
4 | import com.bugsnag.android.Bugsnag
5 | import com.octo4a.utils.TLSSocketFactory
6 | import com.octo4a.utils.preferences.MainPreferences
7 | import org.koin.android.ext.koin.androidLogger
8 | import org.koin.android.ext.koin.androidContext
9 | import org.koin.core.context.startKoin
10 | import javax.net.ssl.HttpsURLConnection
11 |
12 | class Octo4aApplication : MultiDexApplication() {
13 | val preferences by lazy { MainPreferences(this) }
14 | var bugsnagStarted = false
15 |
16 | override fun onCreate() {
17 | super.onCreate()
18 | initializeSSLContext()
19 |
20 | // Start Koin
21 | startKoin {
22 | androidLogger()
23 | androidContext(this@Octo4aApplication)
24 | modules(appModule)
25 | }
26 |
27 | if (preferences.enableBugReporting) {
28 | startBugsnag()
29 | }
30 | }
31 |
32 | fun startBugsnag() {
33 | if (bugsnagStarted) return
34 |
35 | Bugsnag.start(this)
36 | bugsnagStarted = true
37 | }
38 |
39 | fun initializeSSLContext() {
40 | val noSSLv3Factory: TLSSocketFactory = TLSSocketFactory(applicationContext)
41 |
42 | HttpsURLConnection.setDefaultSSLSocketFactory(noSSLv3Factory)
43 | }
44 | }
--------------------------------------------------------------------------------
/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 -Dkotlin.daemon.jvm.options=--illegal-access=permit
10 | org.gradle.jvmargs=--illegal-access=permit -Dkotlin.daemon.jvm.options=--illegal-access=permit
11 | # When configured, Gradle will run in incubating parallel mode.
12 | # This option should only be used with decoupled projects. More details, visit
13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
14 | # org.gradle.parallel=true
15 | # AndroidX package structure to make it clearer which packages are bundled with the
16 | # Android operating system, and which are packaged with your app"s APK
17 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
18 | android.useAndroidX=true
19 | # Automatically convert third-party libraries to use AndroidX
20 | android.enableJetifier=true
21 | # Kotlin code style for this project: "official" or "obsolete":
22 | kotlin.code.style=official
23 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/repository/GithubRepository.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.repository
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.request.*
5 | import io.ktor.http.*
6 | import java.util.Date
7 |
8 | data class GithubAsset(val name: String, val url: String, val browserDownloadUrl: String)
9 |
10 | data class GithubRelease(val tagName: String, val zipballUrl: String, val body: String, val assets: List, val htmlUrl: String, val name: String, val prerelease: Boolean, val publishedAt: Date)
11 |
12 | interface GithubRepository {
13 | suspend fun getNewestRelease(repository: String): GithubRelease
14 | suspend fun getNewestReleases(repository: String): List
15 | }
16 |
17 | class GithubRepositoryImpl(val httpClient: HttpClient): GithubRepository {
18 | private val baseUrl = "https://api.github.com/"
19 |
20 | override suspend fun getNewestRelease(repository: String): GithubRelease {
21 | return httpClient.get {
22 | url("${baseUrl}repos/${repository}/releases/latest")
23 | contentType(ContentType.Application.Json)
24 | }
25 | }
26 |
27 | override suspend fun getNewestReleases(repository: String): List {
28 | return httpClient.get {
29 | url("${baseUrl}repos/${repository}/releases")
30 | contentType(ContentType.Application.Json)
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
14 |
15 |
24 |
25 |
26 |
31 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/preferences/MainPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils.preferences
2 |
3 | import android.content.Context
4 |
5 | class MainPreferences(context: Context) : Preferences(context, true) {
6 | var enableCameraServer by booleanPref()
7 | var selectedCamera by stringPref(defaultValue = null)
8 | var selectedResolution by stringPref()
9 | var selectedVideoResolution by stringPref()
10 | var enableSSH by booleanPref(defaultValue = false)
11 | var manualAF by booleanPref(defaultValue = false)
12 | var manualAFValue by floatPref(defaultValue = 0f)
13 | var changeSSHPassword by stringPref()
14 | var sshPort by stringPref(defaultValue = "8022")
15 | var flashWhenObserved by booleanPref()
16 | var currentRelease by stringPref(defaultValue = "")
17 | var defaultPrinterPid by intPref()
18 | var defaultPrinterVid by intPref()
19 | var defaultPrinterCustomDriver by stringPref()
20 | var imageRotation by stringPref(defaultValue = "0")
21 | var fpsLimit by stringPref(defaultValue = "-1")
22 | var sshPasword by stringPref()
23 | var enableBugReporting by booleanPref(defaultValue = false)
24 | var hasAskedAboutReporting by booleanPref(defaultValue = false)
25 | var extensionSettings by stringPref(defaultValue = "[]")
26 | var updateDismissed by stringPref()
27 | var startOnBoot by booleanPref(defaultValue = false)
28 | var warnDisableBatteryOptimization by booleanPref(defaultValue = true)
29 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/repository/SystemStatusRepository.kt:
--------------------------------------------------------------------------------
1 | //package com.octo4a.repository
2 | //
3 | //package com.octo4a.repository
4 | //
5 | //import android.content.Context
6 | //import com.octo4a.octoprint.BootstrapUtils
7 | //import com.octo4a.utils.*
8 | //import com.octo4a.utils.preferences.MainPreferences
9 | //import kotlinx.coroutines.flow.MutableStateFlow
10 | //import kotlinx.coroutines.flow.StateFlow
11 | //import kotlin.math.roundToInt
12 | //
13 | //enum class ServerStatus(val value: Int) {
14 | // InstallingBootstrap(0),
15 | // DownloadingOctoPrint(1),
16 | // InstallingDependencies(2),
17 | // BootingUp(3),
18 | // Running(4),
19 | // Stopped(5)
20 | //}
21 | //
22 | //data class UsbDeviceStatus(val isAttached: Boolean, val port: String = "")
23 | //
24 | //fun ServerStatus.getInstallationProgress(): Int {
25 | // return ((value.toDouble() / 4) * 100).roundToInt()
26 | //}
27 | //
28 | //fun ServerStatus.isInstallationFinished(): Boolean {
29 | // return value == ServerStatus.Running.value
30 | //}
31 | //
32 | //interface SystemStatusRepository {
33 | // val serverState: StateFlow
34 | // val usbDeviceStatus: StateFlow
35 | //
36 | // fun setServerStatus(status: ServerStatus)
37 | // fun setSSHEnabled(isSSHEnabled: Boolean)
38 | // fun usbDevicePluggedIn(port: String)
39 | // fun usbDeviceDetached()
40 | // fun setCameraServerRunning(isRunning: Boolean)
41 | //}
42 | //
43 | //class SystemStatusRepositoryImpl() : SystemStatusRepository {
44 | // private var _serverState = MutableStateFlow(ServerStatus.InstallingBootstrap)
45 | // private var _usbDeviceStatus = MutableStateFlow(UsbDeviceStatus(false))
46 | //
47 | //}
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/dialog_camera_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
22 |
27 |
31 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/repository/FIFOEventRepository.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.repository
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.google.gson.GsonBuilder
6 | import org.koin.java.KoinJavaComponent.inject
7 | import java.io.File
8 | import java.lang.Exception
9 |
10 | data class FIFOEvent(val eventType: String)
11 |
12 | interface FIFOEventRepository {
13 | val eventState: LiveData
14 | fun handleFifoEvents()
15 | }
16 |
17 | class FIFOEventRepositoryImpl(val logger: LoggerRepository) : FIFOEventRepository {
18 | private var _eventState = MutableLiveData()
19 | override val eventState = _eventState
20 |
21 | private val eventFifoPath = "/data/data/com.octo4a/files/bootstrap/bootstrap/eventPipe"
22 | private val gson by lazy {
23 | GsonBuilder()
24 | .create()
25 | }
26 |
27 | override fun handleFifoEvents() {
28 | try {
29 | val fifoFile = File(eventFifoPath)
30 | while (true) {
31 | fifoFile.inputStream().bufferedReader().forEachLine {
32 | logger.log(this) { "Got event $it" }
33 | try {
34 | val event = gson.fromJson(it.replace("\n", ""), FIFOEvent::class.java)
35 | _eventState.postValue(event)
36 | } catch (e: Exception) {
37 | logger.log(this) { "Error occured when parsing fifo event " + e.message }
38 | }
39 | }
40 | }
41 | } catch (e: Exception) {
42 | logger.log(this) { "FIFO Handler error " + e.message }
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/views/ExtensionView.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui.views
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.LayoutInflater
6 |
7 | import androidx.constraintlayout.widget.ConstraintLayout
8 | import com.octo4a.R
9 | import com.octo4a.repository.ExtensionStatus
10 | import com.octo4a.repository.ExtensionsRepository
11 | import com.octo4a.repository.RegisteredExtension
12 | import kotlinx.android.synthetic.main.extension_view.view.*
13 |
14 | class ExtensionView @JvmOverloads
15 | constructor(private val ctx: Context, private val extensionsRepository: ExtensionsRepository, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0)
16 | : ConstraintLayout(ctx, attributeSet, defStyleAttr) {
17 |
18 | var onActionClicked: () -> Unit = {}
19 |
20 | init {
21 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
22 | inflater.inflate(R.layout.extension_view, this)
23 | }
24 |
25 | fun setExtensionDescription(extensionInfo: RegisteredExtension) {
26 | titleText.text = extensionInfo.title
27 |
28 | val status = when (extensionInfo.status) {
29 | ExtensionStatus.Stopped -> " (stopped)"
30 | ExtensionStatus.Running -> " (running)"
31 | else -> " (crashed)"
32 | }
33 |
34 | subtitleText.text = extensionInfo.description + status
35 |
36 |
37 | extensionEnabled.isChecked = extensionsRepository.getExtensionSettings(extensionInfo.name)?.enabled ?: false
38 | extensionEnabled.setOnCheckedChangeListener { _, checked ->
39 | extensionsRepository.modifyExtensionSetting(extensionInfo.name, checked)
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/scripts/setup-klipper.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | COL='\033[1;32m'
4 | NC='\033[0m' # No Color
5 | echo -e "${COL}Setting up klipper"
6 |
7 | read -p "Do you have \"Plugin extras\" installed? (y/n): " -n 1 -r
8 | if [[ ! $REPLY =~ ^[Yy]$ ]]
9 | then
10 | echo -e "${COL}\nPlease go to settings and install plugin extras${NC}"
11 | [[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1
12 | fi
13 |
14 | echo -e "${COL}\nInstalling dependencies...\n${NC}"
15 | # install required dependencies
16 | apk add py3-cffi py3-greenlet linux-headers can-utils
17 | pip3 install python-can
18 |
19 | echo -e "${COL}Downloading klipper\n${NC}"
20 | curl -o klipper.zip -L https://github.com/Klipper3d/klipper/archive/refs/heads/master.zip
21 |
22 | echo -e "${COL}Extracting klipper\n${NC}"
23 | unzip klipper.zip
24 | rm -rf klipper.zip
25 | mv klipper-master /klipper
26 | echo "# replace with your config" >> /root/printer.cfg
27 |
28 | mkdir -p /root/extensions/klipper
29 | cat << EOF > /root/extensions/klipper/manifest.json
30 | {
31 | "title": "Klipper plugin",
32 | "description": "Requires OctoKlipper plugin"
33 | }
34 | EOF
35 |
36 | cat << EOF > /root/extensions/klipper/start.sh
37 | #!/bin/sh
38 | python3 /klipper/klippy/klippy.py /root/printer.cfg -l /tmp/klippy.log -a /tmp/klippy_uds
39 | EOF
40 |
41 | cat << EOF > /root/extensions/klipper/kill.sh
42 | #!/bin/sh
43 | pkill -f 'klippy\.py'
44 | EOF
45 | chmod +x /root/extensions/klipper/start.sh
46 | chmod +x /root/extensions/klipper/kill.sh
47 | chmod 777 /root/extensions/klipper/start.sh
48 | chmod 777 /root/extensions/klipper/kill.sh
49 |
50 | cat << EOF
51 | ${COL}
52 | Klipper installed!
53 | Please place your own klipper config file at /root/printer.cfg
54 | Please kill the app and restart it again to see it in extension settings${NC}
55 | EOF
56 |
--------------------------------------------------------------------------------
/app/app/src/main/res/xml/device_filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/repository/LoggerRepository.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.repository
2 |
3 | import android.graphics.Color
4 | import android.os.Looper
5 | import android.util.Log
6 | import kotlinx.coroutines.flow.MutableSharedFlow
7 | import kotlinx.coroutines.flow.SharedFlow
8 | import kotlinx.coroutines.runBlocking
9 |
10 | enum class LogType {
11 | SYSTEM,
12 | BOOTSTRAP,
13 | OCTOPRINT,
14 | EXTENSION,
15 | OTHER
16 | }
17 |
18 | data class LogEntry(val entry: String, val type: LogType = LogType.SYSTEM)
19 |
20 | fun LogType.getTypeEmoji(): String {
21 | return when (this) {
22 | LogType.BOOTSTRAP -> {
23 | "\uD83D\uDC38"
24 | }
25 | LogType.OCTOPRINT -> {
26 | "\uD83D\uDC19"
27 | }
28 | LogType.EXTENSION -> {
29 | "\uD83D\uDD0C"
30 | }
31 | LogType.SYSTEM -> {
32 | "\uD83D\uDCBB"
33 | }
34 | else ->"❓"
35 | }
36 | }
37 |
38 | interface LoggerRepository {
39 | val logHistoryFlow: SharedFlow
40 | fun log(obj: Any? = null, type: LogType = LogType.SYSTEM, getMessage: () -> String)
41 | }
42 |
43 | class LoggerRepositoryImpl: LoggerRepository {
44 | private var _logHistoryFlow = MutableSharedFlow(1000)
45 |
46 | override val logHistoryFlow: SharedFlow
47 | get() = _logHistoryFlow
48 |
49 | override fun log(obj: Any?, type: LogType, getMessage: () -> String) {
50 | var tag = "octo4a: "
51 | obj?.let {
52 | tag = obj::class.java.simpleName + ": "
53 | }
54 |
55 | Log.d(type.getTypeEmoji(), getMessage())
56 |
57 | runBlocking {
58 | Looper.getMainLooper().run {
59 | _logHistoryFlow.emit(LogEntry(getMessage(), type))
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/BootReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a
2 |
3 | import android.app.AlarmManager
4 | import android.app.PendingIntent
5 | import android.content.BroadcastReceiver
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.os.Build
9 | import android.util.Log
10 | import com.octo4a.ui.InitialActivity
11 | import com.octo4a.utils.preferences.MainPreferences
12 |
13 |
14 | class BootReceiver: BroadcastReceiver() {
15 | override fun onReceive(context: Context, intent: Intent) {
16 | val preferences = MainPreferences(context)
17 |
18 | if (preferences.startOnBoot) {
19 | restartApp(context)
20 | }
21 | }
22 |
23 | // I'm guessing android made it impossible to straight up startActivity at boot in newer sdk versions
24 | // BUT HEY OFC THERE'S A WEIRD WORKAROUND THAT BYPASSES IT ON STACKOVERFLOW
25 | private fun restartApp(context: Context) {
26 | try {
27 | val restartTime = (1000 * 5).toLong()
28 | val intents = context.packageManager.getLaunchIntentForPackage(context.packageName)
29 | val restartIntent = PendingIntent.getActivity(context, 0, intents, PendingIntent.FLAG_ONE_SHOT)
30 | val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
32 | mgr.setExactAndAllowWhileIdle(
33 | AlarmManager.RTC_WAKEUP,
34 | System.currentTimeMillis() + restartTime,
35 | restartIntent
36 | )
37 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
38 | mgr.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + restartTime, restartIntent)
39 | }
40 | } catch (e: Exception) {
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/fragments/AboutFragment.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui.fragments
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.fragment.app.Fragment
10 | import com.octo4a.R
11 | import com.octo4a.repository.BootstrapRepository
12 | import kotlinx.android.synthetic.main.fragment_about.*
13 | import org.koin.android.ext.android.inject
14 |
15 | class AboutFragment : Fragment() {
16 | private val bootstrapRepository: BootstrapRepository by inject()
17 |
18 | override fun onCreateView(
19 | inflater: LayoutInflater,
20 | container: ViewGroup?,
21 | savedInstanceState: Bundle?): View? {
22 | return inflater.inflate(R.layout.fragment_about, container, false)
23 | }
24 |
25 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
26 | super.onViewCreated(view, savedInstanceState)
27 | val pInfo = requireContext().packageManager.getPackageInfo(requireContext().packageName, 0)
28 | val version = pInfo.versionName
29 | val bootstrapVersion = bootstrapRepository.bootstrapVersion
30 |
31 | appVersionText.text = "octo4a build $version • $bootstrapVersion"
32 |
33 | joinTelegramButton.setOnClickListener {
34 | openWebsite("https://t.me/octo4achat")
35 | }
36 |
37 | donateButton.setOnClickListener {
38 | openWebsite("https://paypal.me/feelfreelinux")
39 | }
40 |
41 | appProjectButton.setOnClickListener {
42 | openWebsite("https://github.com/feelfreelinux/octo4a")
43 | }
44 | }
45 |
46 | fun openWebsite(url: String) {
47 | val i = Intent(Intent.ACTION_VIEW)
48 | i.data = Uri.parse(url)
49 | startActivity(i)
50 | }
51 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
10 |
16 |
19 |
22 |
23 |
24 |
25 |
31 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/fragments/ExtensionsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui.fragments
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.util.Log
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import androidx.fragment.app.Fragment
11 | import androidx.lifecycle.asLiveData
12 | import com.octo4a.R
13 | import com.octo4a.repository.ExtensionsRepository
14 | import com.octo4a.repository.OctoPrintHandlerRepository
15 | import com.octo4a.ui.views.ExtensionView
16 | import kotlinx.android.synthetic.main.fragment_extensions.*
17 | import org.koin.android.ext.android.inject
18 |
19 | class ExtensionsFragment : Fragment() {
20 | val octoPrintHandlerRepository: OctoPrintHandlerRepository by inject()
21 | val extensionsRepository: ExtensionsRepository by inject()
22 |
23 | override fun onCreateView(
24 | inflater: LayoutInflater,
25 | container: ViewGroup?,
26 | savedInstanceState: Bundle?): View? {
27 | return inflater.inflate(R.layout.fragment_extensions, container, false)
28 | }
29 |
30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
31 | super.onViewCreated(view, savedInstanceState)
32 | extensionsRepository.getValidExtensions()
33 | extensionsRepository.extensionsState.asLiveData().observe(viewLifecycleOwner) {
34 | extensionsList.removeAllViews()
35 | it.forEach {
36 | val extensionView = ExtensionView(requireContext(), extensionsRepository)
37 | extensionsList.addView(extensionView)
38 | extensionView.setExtensionDescription(it)
39 | }
40 | }
41 |
42 | extensionsCard.setOnClickListener {
43 | val i = Intent(Intent.ACTION_VIEW)
44 | i.data = Uri.parse("https://github.com/feelfreelinux/octo4a/wiki/Extensions-system")
45 | startActivity(i)
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/app/src/main/cpp/ioctl-hook.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | int g_obvio=0;
15 | #define DPRINTF(format, args...) if (!g_obvio) { g_obvio=1; fprintf(stderr, format, ## args); g_obvio=0; }
16 |
17 | #ifndef RTLD_NEXT
18 | #define RTLD_NEXT ((void *)-1l)
19 | #endif
20 |
21 | #define REAL_LIBC RTLD_NEXT
22 | #define request_t int
23 | #define TIOCMBIS 0x5416
24 |
25 | static char fifoPath[] = "/home/octoprint/eventPipe";
26 | static char eventJsonStart[] = "{\"eventType\": \"";
27 | static char eventJsonEnd[] = "\"}\n";
28 |
29 |
30 | int writeEventToPipe(char* event, int len) {
31 | int eventFifoFd = open(fifoPath, O_WRONLY);
32 | write(eventFifoFd, eventJsonStart, sizeof(eventJsonStart)-1);
33 | write(eventFifoFd, event, len);
34 | write(eventFifoFd, eventJsonEnd, sizeof(eventJsonEnd)-1);
35 | close(eventFifoFd);
36 | }
37 |
38 | int ioctl(int fd, int request, ...)
39 | {
40 | static int (*funcIoctl)(int, request_t, void *) = NULL;
41 | va_list args;
42 | void *argp;
43 |
44 | if (!funcIoctl)
45 | funcIoctl = (int (*)(int, request_t, void *))dlsym(REAL_LIBC, "ioctl");
46 | va_start(args, request);
47 | argp = va_arg(args, void *);
48 | va_end(args);
49 |
50 | if (request == TIOCMBIS)
51 | {
52 | writeEventToPipe("rtsDts", 6);
53 | return 0;
54 | }
55 |
56 | if (request == 0x802c542a || request == 0x402C542B)
57 | {
58 | writeEventToPipe("customBaud", 10);
59 | return 0;
60 | }
61 | //DPRINTF ("HOOK: ioctl (fd=%d, request=%p, argp=%p [%02X])\n", fd, request, argp);
62 | return funcIoctl(fd, request, argp);
63 | }
64 |
65 | int flock(int fd, int operation) {
66 | // DPRINTF ("HOOK: flock (fd=%d, operation=%p)\n", fd, operation);
67 | return 0;
68 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
19 |
27 |
28 |
36 |
37 |
38 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | You don't have a Raspberry Pi, but you want to control your 3D printer remotely? Use your phone as an octoprint host! With the Octo4a app you can install Octoprint on your android phone in minutes, without any special Linux knowledge.
2 |
3 | Usage
4 |
5 | - Enable installing 3rd-party .apk in your phone's settings.
6 | - Install the apk file downloaded from the releases page.
7 | - Open the app.
8 | - Click "Install OctoPrint" to download and install OctoPrint
9 | - Allow the app to access the storage, if asked for permission.
10 | - Wait for the installation to complete. This may take a long time, depending on your internet speed.
11 | - Click "Continue" when the installation finishes.
12 | - Optionally start the camera server to enable watching yur printer from octoprint.
13 | - Navigate to the IP address shown at the top in your browser to access and set-up OctoPrint.
14 | - Happy printing!
15 |
16 | Features
17 | - Quick and easy octoprint installation.
18 | - Printer connection via USB OTG. Thanks to our custom USB driver you can use octoprint even on phones without root access.
19 | - Built-in camera support. You can use the built-in camera in your phone to see the progress of your 3D prints, instead of buying a separate module. The app also supports octolapse.
20 | - SSH support. You can easily log-in via ssh and customize your octoprint installation.
21 |
22 | Wiki
23 | FAQ and many different topics are described in the project's wiki
24 |
25 | Contributing
26 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
27 |
28 | Disclaimer
29 |
30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 |
32 |
--------------------------------------------------------------------------------
/.github/workflows/build-app.yml:
--------------------------------------------------------------------------------
1 | name: Build app
2 | on: [pull_request, push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout the code
8 | uses: actions/checkout@v2
9 | - name: Setup ndk
10 | uses: nttld/setup-ndk@v1
11 | with:
12 | ndk-version: r21d
13 | - name: Build the app
14 | run: |
15 | ./gradlew assembleRelease
16 | working-directory: ./app
17 | - name: Setup build tool version variable
18 | shell: bash
19 | run: |
20 | BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
21 | echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
22 | echo Last build tool version is: $BUILD_TOOL_VERSION
23 | - uses: r0adkll/sign-android-release@v1
24 | name: Sign app APK
25 | id: sign_app
26 | with:
27 | releaseDirectory: app/app/build/outputs/apk/release
28 | signingKeyBase64: ${{ secrets.SIGNING_KEY }}
29 | alias: ${{ secrets.ALIAS }}
30 | keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
31 | keyPassword: ${{ secrets.KEY_PASSWORD }}
32 | env:
33 | BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
34 | - name: Upload artifacts
35 | uses: actions/upload-artifact@v2
36 | with:
37 | name: Signed app
38 | path: ${{steps.sign_app.outputs.signedReleaseFile}}
39 | - name: Get the version
40 | if: startsWith(github.ref, 'refs/tags/')
41 | id: get_version
42 | run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
43 | - name: Move and name release
44 | if: startsWith(github.ref, 'refs/tags/')
45 | run: mv ${{steps.sign_app.outputs.signedReleaseFile}} octo4a-${{steps.get_version.outputs.VERSION}}.apk
46 | - name: Create github release
47 | uses: softprops/action-gh-release@v1
48 | if: startsWith(github.ref, 'refs/tags/')
49 | with:
50 | files: octo4a-${{steps.get_version.outputs.VERSION}}.apk
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/extension_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
22 |
23 |
35 |
36 |
40 |
48 |
49 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/view_installation_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
14 |
26 |
35 |
44 |
45 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/Octo4aWakeLock.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils
2 |
3 | import android.content.ActivityNotFoundException
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.net.wifi.WifiManager
8 | import android.os.Build
9 | import android.os.PowerManager
10 | import android.provider.Settings
11 | import android.widget.Toast
12 | import androidx.lifecycle.LifecycleService
13 | import com.octo4a.R
14 | import com.octo4a.repository.LoggerRepository
15 | import java.time.Duration
16 |
17 | class Octo4aWakeLock(val context: Context, val logger: LoggerRepository) {
18 | var wakeLock: PowerManager.WakeLock? = null
19 | var wifiLock: WifiManager.WifiLock? = null
20 |
21 | fun acquire() {
22 | if (wakeLock != null && wakeLock!!.isHeld && wifiLock != null && wifiLock!!.isHeld) {
23 | return
24 | }
25 |
26 | val pm = context.getSystemService(LifecycleService.POWER_SERVICE) as PowerManager
27 | wakeLock =
28 | pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "octo4a:service-wakelock")
29 | wakeLock?.acquire(10*60*1000L /*10 minutes*/)
30 |
31 |
32 | // http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
33 | val wm = context.applicationContext.getSystemService(LifecycleService.WIFI_SERVICE) as WifiManager
34 | wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "octo4a:wifilock")
35 | wifiLock?.acquire()
36 |
37 | val packageName = context.packageName
38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !pm.isIgnoringBatteryOptimizations(packageName)) {
39 | val whitelist = Intent()
40 | whitelist.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
41 | whitelist.data = Uri.parse("package:$packageName")
42 | whitelist.flags = Intent.FLAG_ACTIVITY_NEW_TASK
43 | try {
44 | context.startActivity(whitelist)
45 | } catch (e: ActivityNotFoundException) {
46 | logger.log(this) { "failed to open battery optimization dialog" }
47 | }
48 | }
49 | }
50 |
51 | fun remove() {
52 | wakeLock?.release()
53 | wifiLock?.release()
54 | wakeLock = null
55 | wifiLock = null
56 | }
57 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a
2 |
3 | import com.google.gson.FieldNamingPolicy
4 | import com.octo4a.camera.CameraEnumerationRepository
5 | import com.octo4a.repository.*
6 | import com.octo4a.serial.VirtualSerialDriver
7 | import com.octo4a.utils.TLSSocketFactory
8 | import com.octo4a.utils.preferences.MainPreferences
9 | import com.octo4a.viewmodel.InstallationViewModel
10 | import com.octo4a.viewmodel.NetworkStatusViewModel
11 | import com.octo4a.viewmodel.StatusViewModel
12 | import io.ktor.client.*
13 | import io.ktor.client.engine.android.*
14 | import io.ktor.client.features.json.*
15 | import org.koin.android.ext.koin.androidApplication
16 | import org.koin.android.ext.koin.androidContext
17 | import org.koin.androidx.viewmodel.dsl.viewModel
18 | import org.koin.dsl.module
19 |
20 | val appModule = module {
21 | single {
22 | HttpClient(Android) {
23 | install(JsonFeature) {
24 | serializer = GsonSerializer {
25 | serializeNulls()
26 | setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
27 | }
28 | }
29 | engine {
30 | sslManager = {
31 | it.sslSocketFactory = TLSSocketFactory(androidContext())
32 | }
33 | }
34 | }
35 | }
36 |
37 | factory { MainPreferences(androidContext()) }
38 | factory { GithubRepositoryImpl(get()) }
39 |
40 | single { BootstrapRepositoryImpl(get(), get(), androidContext()) }
41 | single { FIFOEventRepositoryImpl(get()) }
42 | single { VirtualSerialDriver(androidContext(), get(), get(), get()) }
43 | single { ExtensionsRepositoryImpl(androidContext(), get(), get(), get()) }
44 | single { LoggerRepositoryImpl() }
45 | single { OctoPrintHandlerRepositoryImpl(androidContext(), get(), get(), get(), get(), get(), get()) }
46 | single { CameraEnumerationRepository(androidApplication()) }
47 |
48 | viewModel { InstallationViewModel(androidApplication(), get(), get(), get()) }
49 | viewModel { StatusViewModel(androidApplication(), get(), get()) }
50 | viewModel { NetworkStatusViewModel(androidApplication(), get()) }
51 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/camera/MJpegServer.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.camera
2 |
3 | import android.util.Log
4 | import fi.iki.elonen.NanoHTTPD
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.launch
8 | import kotlinx.coroutines.runBlocking
9 | import java.io.BufferedOutputStream
10 | import java.io.ByteArrayInputStream
11 | import java.io.PipedInputStream
12 | import java.io.PipedOutputStream
13 |
14 | interface MJpegFrameProvider {
15 | data class FrameInfo(val image: ByteArray? = null, val id: Int)
16 |
17 | suspend fun takeSnapshot(): ByteArray
18 | fun getNewFrame(prevFrame: FrameInfo?): FrameInfo
19 | fun registerListener(): Boolean
20 | fun unregisterListener()
21 | }
22 |
23 | // Simple http server hosting mjpeg stream along with
24 | class MJpegServer(port: Int, private val frameProvider: MJpegFrameProvider): NanoHTTPD(port) {
25 |
26 | override fun serve(session: IHTTPSession?): Response {
27 | when (session?.uri) {
28 | "/snapshot" -> {
29 | var res: Response? = null
30 | kotlin.runCatching {
31 | runBlocking {
32 | val data = frameProvider?.takeSnapshot()
33 | val inputStream = ByteArrayInputStream(data)
34 |
35 | res = newFixedLengthResponse(
36 | Response.Status.OK,
37 | "image/jpeg",
38 | inputStream,
39 | data.size.toLong()
40 | )
41 | }
42 | }.onFailure {
43 | }
44 |
45 | return res ?: newFixedLengthResponse(
46 | "Failed to fetch image
"
47 | )
48 | }
49 | "/mjpeg" -> {
50 | return MjpegResponse(frameProvider)
51 | }
52 | else -> return newFixedLengthResponse(
53 | ""
54 | + "GET /snapshot
GET a current JPEG image.
"
55 | + "GET /mjpeg
GET MJPEG frames.
"
56 | + ""
57 | )
58 | }
59 | }
60 |
61 | fun startServer() {
62 | start()
63 | }
64 |
65 | fun stopServer() {
66 | stop()
67 | }
68 | }
--------------------------------------------------------------------------------
/app/app/src/main/cpp/yuv2rgb.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | namespace {
13 |
14 | void yuv420toNv21(int image_width, int image_height, const int8_t* y_buffer,
15 | const int8_t* u_buffer, const int8_t* v_buffer, int y_pixel_stride,
16 | int uv_pixel_stride, int y_row_stride, int uv_row_stride,
17 | int8_t *nv21) {
18 | for(int y = 0; y < image_height; ++y) {
19 | int destOffset = image_width * y;
20 | int yOffset = y * y_row_stride;
21 | memcpy(nv21 + destOffset, y_buffer + yOffset, image_width);
22 | }
23 |
24 | int idUV = image_width * image_height;
25 | int uv_width = image_width / 2;
26 | int uv_height = image_height / 2;
27 | for(int y = 0; y < uv_height; ++y) {
28 | int uvOffset = y * uv_row_stride;
29 | for (int x = 0; x < uv_width; ++x) {
30 | int bufferIndex = uvOffset + (x * uv_pixel_stride);
31 | // V channel.
32 | nv21[idUV++] = v_buffer[bufferIndex];
33 | // U channel.
34 | nv21[idUV++] = u_buffer[bufferIndex];
35 | }
36 | }
37 | }
38 |
39 | } // namespace
40 |
41 | extern "C" {
42 |
43 | jboolean Java_com_octo4a_camera_NativeCameraUtils_yuv420toNv21(
44 | JNIEnv *env, jclass clazz,
45 | jint image_width, jint image_height, jobject y_byte_buffer,
46 | jobject u_byte_buffer, jobject v_byte_buffer, jint y_pixel_stride,
47 | jint uv_pixel_stride, jint y_row_stride, jint uv_row_stride,
48 | jbyteArray nv21_array) {
49 |
50 | auto y_buffer = static_cast(env->GetDirectBufferAddress(y_byte_buffer));
51 | auto u_buffer = static_cast(env->GetDirectBufferAddress(u_byte_buffer));
52 | auto v_buffer = static_cast(env->GetDirectBufferAddress(v_byte_buffer));
53 |
54 | jbyte* nv21 = env->GetByteArrayElements(nv21_array, nullptr);
55 | if (nv21 == nullptr || y_buffer == nullptr || u_buffer == nullptr
56 | || v_buffer == nullptr) {
57 | // Log this.
58 | return false;
59 | }
60 |
61 | yuv420toNv21(image_width, image_height, y_buffer, u_buffer, v_buffer,
62 | y_pixel_stride, uv_pixel_stride, y_row_stride, uv_row_stride,
63 | nv21);
64 |
65 | env->ReleaseByteArrayElements(nv21_array, nv21, 0);
66 | return true;
67 | }
68 |
69 | } // extern "C"
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/fragment_extensions.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
12 |
21 |
25 |
33 |
41 |
42 |
43 |
44 |
45 |
50 |
51 |
--------------------------------------------------------------------------------
/app/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/camera/MjpegResponse.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.camera
2 |
3 | import fi.iki.elonen.NanoHTTPD
4 | import java.io.OutputStream
5 | import java.io.PrintWriter
6 | import java.text.SimpleDateFormat
7 | import java.util.*
8 |
9 | class MjpegResponse(private val frameProvider: MJpegFrameProvider) :
10 | NanoHTTPD.Response(Status.OK, "multipart/x-mixed-replace; boundary=--octo4a", null, 0) {
11 | override fun send(outputStream: OutputStream) {
12 | val registered = frameProvider.registerListener()
13 | kotlin
14 | .runCatching {
15 | if (registered) {
16 | sendStream(outputStream)
17 | } else {
18 | sendError(outputStream, 500, "Internal Server Error - Camera Not Initialized")
19 | }
20 | }
21 | .onFailure { outputStream.close() }
22 | .onSuccess { outputStream.close() }
23 |
24 | if (registered) {
25 | frameProvider.unregisterListener()
26 | }
27 | }
28 |
29 | private fun sendStream(outputStream: OutputStream) {
30 | val gmtFrmt = SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US)
31 | gmtFrmt.timeZone = TimeZone.getTimeZone("GMT")
32 |
33 | val pw = PrintWriter(outputStream)
34 | pw.apply {
35 | print("HTTP/1.1 ${status.description} \r\n")
36 | print("Date: ${gmtFrmt.format(Date())}\r\n")
37 | print("Connection: close\r\n")
38 | print(
39 | "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n")
40 | print("Expires: 0\r\n")
41 | print("Max-Age: 0\r\n")
42 | print("Pragma: no-cache\r\n")
43 | print("Content-Type: ${mimeType}\r\n")
44 | print("\r\n--octo4a\r\n")
45 | flush()
46 | var frameInfo: MJpegFrameProvider.FrameInfo? = null
47 | while (true) {
48 | frameInfo = frameProvider.getNewFrame(frameInfo)
49 | val image = frameInfo.image ?: ByteArray(0)
50 | if (image.isNotEmpty()) {
51 | write("Content-type: image/jpeg\r\n")
52 | write("Content-Length: ${image?.size}\r\n")
53 | write("\r\n")
54 | flush()
55 | outputStream.write(image, 0, image.size)
56 | outputStream.flush()
57 | print("\r\n--octo4a\r\n")
58 | flush()
59 | }
60 | }
61 | }
62 | }
63 |
64 | private fun sendError(outputStream: OutputStream, statusCode: Int, message: String) {
65 | val pw = PrintWriter(outputStream)
66 | pw.apply {
67 | print("HTTP/1.1 $statusCode $message\r\n")
68 | print("Content-Length: ${message.length}\r\n")
69 | print("\r\n$message")
70 | flush()
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/viewmodel/StatusViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.viewmodel
2 |
3 | import android.app.Application
4 | import android.content.pm.PackageInfo
5 | import android.content.pm.PackageManager
6 | import androidx.lifecycle.*
7 | import com.octo4a.Octo4aApplication
8 | import com.octo4a.repository.GithubRelease
9 | import com.octo4a.repository.GithubRepository
10 | import com.octo4a.repository.OctoPrintHandlerRepository
11 | import com.octo4a.utils.SemVer
12 | import com.octo4a.utils.ipAddress
13 | import com.octo4a.utils.withIO
14 | import kotlinx.coroutines.launch
15 | import java.lang.Exception
16 |
17 |
18 | class StatusViewModel(context: Application,
19 | private val octoPrintHandlerRepository: OctoPrintHandlerRepository,
20 | private val githubRepository: GithubRepository) : AndroidViewModel(context) {
21 | val serverStatus = octoPrintHandlerRepository.serverState.asLiveData()
22 | val usbStatus = octoPrintHandlerRepository.usbDeviceStatus.asLiveData()
23 | val cameraStatus = octoPrintHandlerRepository.cameraServerStatus.asLiveData()
24 | val updateAvailable = MutableLiveData()
25 |
26 | fun startServer() {
27 | octoPrintHandlerRepository.startOctoPrint()
28 | }
29 |
30 | fun checkUpdateAvailable() {
31 | viewModelScope.launch {
32 | withIO {
33 | try {
34 | val newestRelease = githubRepository.getNewestRelease("feelfreelinux/octo4a")
35 |
36 | val app = getApplication()
37 | val pInfo: PackageInfo = app.packageManager.getPackageInfo(app.packageName, 0)
38 | val version = pInfo.versionName
39 | if (SemVer.parse(version) < SemVer.parse(newestRelease.tagName)) {
40 | // New version available, check if is built already
41 | if (newestRelease.assets.any { it.name.contains("bootstrap") }
42 | && newestRelease.assets.any { it.name.contains(".apk") }) {
43 | updateAvailable.postValue(newestRelease)
44 | }
45 | }
46 | } catch (e: Exception) {
47 | e.printStackTrace()
48 | }
49 | }
50 | }
51 | }
52 |
53 | private fun getServerPort(): String {
54 | return octoPrintHandlerRepository.getConfigValue("server.port")
55 | }
56 |
57 | fun getServerAddress(): String {
58 | return "${getApplication().applicationContext.ipAddress}:5000"
59 | }
60 |
61 | fun stopServer() {
62 | octoPrintHandlerRepository.stopOctoPrint()
63 | }
64 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/view_usb_devices_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
26 |
27 |
37 |
38 |
49 |
50 |
58 |
59 |
63 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/views/IPAddressView.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui.views
2 |
3 | import android.content.ClipData
4 | import android.content.ClipboardManager
5 | import android.content.Context
6 | import android.graphics.ColorFilter
7 | import android.os.Build
8 | import android.util.AttributeSet
9 | import android.view.LayoutInflater
10 | import android.widget.Toast
11 | import androidx.constraintlayout.widget.ConstraintLayout
12 | import com.octo4a.R
13 | import com.octo4a.viewmodel.IPAddress
14 | import com.octo4a.viewmodel.IPAddressType
15 | import kotlinx.android.synthetic.main.view_ip_address.view.*
16 |
17 | class IPAddressView@JvmOverloads
18 | constructor(private val ctx: Context, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0)
19 | : ConstraintLayout(ctx, attributeSet, defStyleAttr) {
20 | init {
21 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
22 | inflater.inflate(R.layout.view_ip_address, this)
23 | ipAddressView.setOnClickListener {
24 | // copy ipAddressTextView.text to clipboard
25 | val clipboard =
26 | ctx.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
27 | clipboard.primaryClip = ClipData.newPlainText("octo4a", ipAddressTextView.text)
28 |
29 | // Only show a toast for Android 12 and lower.
30 | if (Build.VERSION.SDK_INT <= 32) { // API 32 is Android 12
31 | Toast.makeText(
32 | context,
33 | ctx.getString(R.string.ip_address_copied_to_clipboard),
34 | Toast.LENGTH_SHORT
35 | ).show()
36 |
37 | }
38 | }
39 | }
40 |
41 | var ipAddress: IPAddress
42 | get() = IPAddress(IPAddressType.Wifi, "")
43 | set(value) {
44 | connectionTypeIcon.setImageResource(
45 | when (value.type) {
46 | IPAddressType.Wifi -> R.drawable.wifi
47 | IPAddressType.Cellular -> R.drawable.cellular
48 | IPAddressType.Ethernet -> R.drawable.ethernet
49 | IPAddressType.VPN -> R.drawable.vpn
50 |
51 | }
52 | )
53 | connectionTypeIcon.imageAlpha = when (value.type) {
54 | IPAddressType.Cellular -> 100
55 | else -> 255
56 | }
57 | ipAddressTextView.setTypeface(null, when (value.type) {
58 | IPAddressType.Cellular -> android.graphics.Typeface.NORMAL
59 | else -> android.graphics.Typeface.BOLD
60 | })
61 | ipAddressTextView.text = value.address + if (value.port != "") ":${value.port}" else ""
62 | }
63 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/serial/PrinterProber.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.serial
2 |
3 | import com.hoho.android.usbserial.driver.*
4 |
5 |
6 | // I should probably come up with a better way of handling those.
7 | // Maybe generation of VID / PID pairs from linux vid / pid db is an option?
8 | data class UsbDeviceId(val vendorId: Int, val productId: Int)
9 |
10 | val prolificDevices = listOf(
11 | UsbDeviceId(1659, 8963),
12 | )
13 |
14 | val cdcDevices = listOf(
15 | // Prusa devices
16 | UsbDeviceId(11417, 1),
17 | UsbDeviceId(11417, 2),
18 |
19 | // Bunch of Arduinos
20 | UsbDeviceId(9025, 16),
21 | UsbDeviceId(9025, 54),
22 | UsbDeviceId(9025, 61),
23 | UsbDeviceId(9025, 62),
24 | UsbDeviceId(9025, 63),
25 | UsbDeviceId(9025, 66),
26 | UsbDeviceId(9025, 67),
27 | UsbDeviceId(9025, 68),
28 | UsbDeviceId(9025, 69),
29 | UsbDeviceId(9025, 73),
30 | UsbDeviceId(9025, 32822),
31 | UsbDeviceId(9025, 32824),
32 | UsbDeviceId(9025, 32825),
33 |
34 | // Teensy (who tf builds printers with those anyways?)
35 | UsbDeviceId(1155, 5824),
36 |
37 | // Atmel LUFA
38 | UsbDeviceId(1003, 8260),
39 |
40 | // Maple printer controllers
41 | UsbDeviceId(7855, 4),
42 | UsbDeviceId(7855, 41),
43 |
44 | // NXP Armbed
45 | UsbDeviceId(3368, 516),
46 |
47 | // Marlin CDC driver
48 | UsbDeviceId(7504, 24617),
49 |
50 | // Duet 2 WiFi
51 | UsbDeviceId(7504, 24812)
52 | )
53 |
54 | val ftdiDevices = listOf(
55 | // Bunch of ftdi devices
56 | UsbDeviceId(1027, 24577),
57 | UsbDeviceId(1027, 24592),
58 | UsbDeviceId(1027, 24593),
59 | UsbDeviceId(1027, 24596),
60 | UsbDeviceId(1027, 24597)
61 | )
62 |
63 | val ch341Devices = listOf(
64 | UsbDeviceId(6790, 29987),// CH341
65 | )
66 |
67 | val cp21xxDevices = listOf(
68 | UsbDeviceId(4292, 60000),
69 | UsbDeviceId(4292, 60016),
70 | UsbDeviceId(4292, 60017),
71 | UsbDeviceId(4292, 60032),
72 | )
73 |
74 | fun getCustomPrinterProber(): ProbeTable {
75 | val probeTable = ProbeTable()
76 | prolificDevices.forEach {
77 | probeTable.addProduct(it.vendorId, it.productId, ProlificSerialDriver::class.java)
78 | }
79 |
80 | cdcDevices.forEach {
81 | probeTable.addProduct(it.vendorId, it.productId, CdcAcmSerialDriver::class.java)
82 | }
83 |
84 | ftdiDevices.forEach {
85 | probeTable.addProduct(it.vendorId, it.productId, FtdiSerialDriver::class.java)
86 | }
87 |
88 | ch341Devices.forEach {
89 | probeTable.addProduct(it.vendorId, it.productId, Ch34xSerialDriver::class.java)
90 | }
91 |
92 | cp21xxDevices.forEach {
93 | probeTable.addProduct(it.vendorId, it.productId, Cp21xxSerialDriver::class.java)
94 | }
95 |
96 | return probeTable
97 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/views/InstallationProgressItem.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui.views
2 |
3 | import android.animation.LayoutTransition
4 | import android.content.Context
5 | import android.graphics.Typeface
6 | import android.util.AttributeSet
7 | import android.util.Log
8 | import android.view.LayoutInflater
9 | import androidx.constraintlayout.widget.ConstraintLayout
10 | import androidx.core.view.isGone
11 | import com.octo4a.R
12 | import com.octo4a.repository.ServerStatus
13 | import com.octo4a.utils.animatedAlpha
14 | import com.octo4a.utils.getArchString
15 | import kotlinx.android.synthetic.main.view_installation_item.view.*
16 |
17 | class InstallationProgressItem @JvmOverloads
18 | constructor(private val ctx: Context, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0)
19 | : ConstraintLayout(ctx, attributeSet, defStyleAttr) {
20 |
21 | init {
22 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
23 | inflater.inflate(R.layout.view_installation_item, this)
24 | layoutTransition = LayoutTransition()
25 | }
26 |
27 | var status: ServerStatus
28 | get() = ServerStatus.Stopped
29 | set(value) {
30 | contentTextView.text = when (value) {
31 | ServerStatus.DownloadingBootstrap -> resources.getString(R.string.installation_step_downloading, getArchString())
32 | ServerStatus.ExtractingBootstrap -> resources.getString(R.string.installation_step_extracting, getArchString())
33 | ServerStatus.BootingUp -> resources.getString(R.string.installation_step_bootup)
34 | ServerStatus.Running -> resources.getString(R.string.installation_step_done)
35 | ServerStatus.InstallationError -> resources.getString(R.string.installation_error)
36 | else -> "Unknown status"
37 | }
38 | }
39 |
40 | var statusText: String
41 | get() = contentTextView.text.toString()
42 | set(value) {
43 | contentTextView.text = value
44 | }
45 | var isLoading: Boolean
46 | get() = false
47 | set(value) {
48 | spinnerView.isGone = !value
49 | doneIconView.isGone = value
50 | if (!value) {
51 | contentTextView.animatedAlpha = 0.4F
52 | contentTextView.typeface = Typeface.DEFAULT
53 | } else {
54 | contentTextView.animatedAlpha = 1F
55 | contentTextView.typeface = Typeface.DEFAULT_BOLD
56 | }
57 | }
58 |
59 | fun setUpcoming() {
60 | spinnerView.isGone = true
61 | doneIconView.isGone = true
62 | contentTextView.animatedAlpha = 0.4F
63 | contentTextView.typeface = Typeface.DEFAULT
64 | }
65 |
66 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .buildozer/*
2 | bin/*
3 | app/build/*
4 | app/build
5 | app/.gradle
6 | app/.gradle/*
7 | app/app/src/main/jniLibs/*
8 | .gradle
9 | .DS_STORE
10 | *.class
11 | *.lock
12 | *.log
13 | *.pyc
14 | *.swp
15 | *.o
16 | .DS_Store
17 | .atom/
18 | .buildlog/
19 | .history
20 | .svn/
21 |
22 | # IntelliJ related
23 | *.iml
24 | *.ipr
25 | *.iws
26 | .idea/
27 |
28 | # Visual Studio Code related
29 | .classpath
30 | .project
31 | .settings/
32 | .vscode/
33 |
34 | # Flutter repo-specific
35 | /bin/cache/
36 | /bin/mingit/
37 | /dev/benchmarks/mega_gallery/
38 | /dev/bots/.recipe_deps
39 | /dev/bots/android_tools/
40 | /dev/docs/doc/
41 | /dev/docs/flutter.docs.zip
42 | /dev/docs/lib/
43 | /dev/docs/pubspec.yaml
44 | /dev/integration_tests/**/xcuserdata
45 | /dev/integration_tests/**/Pods
46 | /packages/flutter/coverage/
47 | version
48 |
49 | # packages file containing multi-root paths
50 | .packages.generated
51 |
52 | # Flutter/Dart/Pub related
53 | **/doc/api/
54 | .dart_tool/
55 | .flutter-plugins
56 | .flutter-plugins-dependencies
57 | .packages
58 | .pub-cache/
59 | .pub/
60 | build/
61 | flutter_*.png
62 | linked_*.ds
63 | unlinked.ds
64 | unlinked_spec.ds
65 |
66 | # Android related
67 | **/android/**/gradle-wrapper.jar
68 | **/android/.gradle
69 | **/android/captures/
70 | **/android/gradlew
71 | **/android/gradlew.bat
72 | **/android/local.properties
73 | **/android/**/GeneratedPluginRegistrant.java
74 | **/android/key.properties
75 | *.jks
76 |
77 | # iOS/XCode related
78 | **/ios/**/*.mode1v3
79 | **/ios/**/*.mode2v3
80 | **/ios/**/*.moved-aside
81 | **/ios/**/*.pbxuser
82 | **/ios/**/*.perspectivev3
83 | **/ios/**/*sync/
84 | **/ios/**/.sconsign.dblite
85 | **/ios/**/.tags*
86 | **/ios/**/.vagrant/
87 | **/ios/**/DerivedData/
88 | **/ios/**/Icon?
89 | **/ios/**/Pods/
90 | **/ios/**/.symlinks/
91 | **/ios/**/profile
92 | **/ios/**/xcuserdata
93 | **/ios/.generated/
94 | **/ios/Flutter/App.framework
95 | **/ios/Flutter/Flutter.framework
96 | **/ios/Flutter/Flutter.podspec
97 | **/ios/Flutter/Generated.xcconfig
98 | **/ios/Flutter/app.flx
99 | **/ios/Flutter/app.zip
100 | **/ios/Flutter/flutter_assets/
101 | **/ios/Flutter/flutter_export_environment.sh
102 | **/ios/ServiceDefinitions.json
103 | **/ios/Runner/GeneratedPluginRegistrant.*
104 |
105 | # macOS
106 | **/macos/Flutter/GeneratedPluginRegistrant.swift
107 | **/macos/Flutter/Flutter-Debug.xcconfig
108 | **/macos/Flutter/Flutter-Release.xcconfig
109 | **/macos/Flutter/Flutter-Profile.xcconfig
110 |
111 | # Coverage
112 | coverage/
113 |
114 | # Exceptions to above rules.
115 | !**/ios/**/default.mode1v3
116 | !**/ios/**/default.mode2v3
117 | !**/ios/**/default.pbxuser
118 | !**/ios/**/default.perspectivev3
119 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
120 | !/dev/ci/**/Gemfile.lock
121 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/fragment_about.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
22 |
31 |
33 |
42 |
44 |
53 |
54 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/fragment_terminal_sheet.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
21 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
46 |
58 |
59 |
65 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/WebinterfaceActivity.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.ActivityManager
5 | import android.content.Intent
6 | import android.os.Build
7 | import android.os.Bundle
8 | import android.view.View
9 | import android.view.WindowManager
10 | import android.webkit.WebResourceRequest
11 | import android.webkit.WebView
12 | import android.webkit.WebViewClient
13 | import androidx.annotation.RequiresApi
14 | import androidx.appcompat.app.AppCompatActivity
15 | import com.octo4a.R
16 | import kotlinx.android.synthetic.main.activity_webinterface.*
17 |
18 | const val WEBINTERFACE_ADDRESS = "127.0.0.1:5000"
19 |
20 | class WebinterfaceActivity : AppCompatActivity() {
21 |
22 | @SuppressLint("SetJavaScriptEnabled")
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | setContentView(R.layout.activity_webinterface)
26 | webview.webViewClient = WebinterfaceClient(this)
27 | webview.settings.loadsImagesAutomatically = true
28 | webview.settings.javaScriptEnabled = true
29 | webview.settings.domStorageEnabled = true
30 | webview.settings.userAgentString = "TouchUI"
31 | webview.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
32 | webview.loadUrl("http://$WEBINTERFACE_ADDRESS")
33 | }
34 |
35 | override fun onResume() {
36 | window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
37 | WindowManager.LayoutParams.FLAG_FULLSCREEN)
38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !isPinned()) {
39 | startLockTask()
40 | }
41 | super.onResume()
42 | }
43 |
44 | @Suppress("DEPRECATION")
45 | fun isPinned(): Boolean {
46 | val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
47 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
48 | (activityManager.lockTaskModeState
49 | != ActivityManager.LOCK_TASK_MODE_NONE)
50 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
51 | activityManager.isInLockTaskMode
52 | } else {
53 | false
54 | }
55 | }
56 |
57 | override fun onBackPressed() {
58 | if (!isPinned()){
59 | super.onBackPressed()
60 | } else if (webview.canGoBack()) {
61 | webview.goBack()
62 | }
63 | }
64 |
65 | private fun webViewLoadingFinished() {
66 | webview.visibility = View.VISIBLE
67 | loadingIndicator.visibility = View.GONE
68 | }
69 |
70 | private class WebinterfaceClient(val activity: WebinterfaceActivity): WebViewClient() {
71 | override fun onPageFinished(view: WebView?, url: String?) {
72 | activity.webViewLoadingFinished()
73 | super.onPageFinished(view, url)
74 | }
75 |
76 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
77 | override fun shouldOverrideUrlLoading(
78 | view: WebView?,
79 | request: WebResourceRequest?
80 | ): Boolean {
81 | return if (request?.url?.authority == WEBINTERFACE_ADDRESS) {
82 | false
83 | } else {
84 | activity.stopLockTask()
85 | val intent = Intent(Intent.ACTION_VIEW, request?.url)
86 | activity.startActivity(intent)
87 | true
88 | }
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/viewmodel/InstallationViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.viewmodel
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.asLiveData
6 | import androidx.lifecycle.viewModelScope
7 | import com.octo4a.repository.BootstrapRepository
8 | import com.octo4a.repository.GithubRelease
9 | import com.octo4a.repository.GithubRepository
10 | import com.octo4a.repository.OctoPrintHandlerRepository
11 | import com.octo4a.utils.getArchString
12 | import com.octo4a.utils.withIO
13 | import kotlinx.coroutines.flow.MutableStateFlow
14 | import kotlinx.coroutines.launch
15 |
16 | data class BootstrapItem(val title: String, val bootstrapVersion: String, val assetUrl: String, val prerelease: Boolean, var recommended: Boolean = false)
17 | class InstallationViewModel(context: Application, octoPrintHandlerRepository: OctoPrintHandlerRepository, private val githubRepository: GithubRepository, private val bootstrapRepository: BootstrapRepository) : AndroidViewModel(context) {
18 | private var _bootstrapReleases = MutableStateFlow(listOf())
19 | private var _selectedRelease = MutableStateFlow(null)
20 |
21 | val serverStatus = octoPrintHandlerRepository.serverState.asLiveData()
22 | val installErrorDescription = octoPrintHandlerRepository.installErrorDescription.asLiveData()
23 | val bootstrapReleases = _bootstrapReleases.asLiveData()
24 | val bootstrapDownloadProgress = bootstrapRepository.downloadProgressData
25 |
26 | fun fetchBootstrapReleases() {
27 | viewModelScope.launch {
28 | withIO {
29 | try {
30 | // Fetch releases from octo4a-bootstrap-builder
31 | val newestReleases = githubRepository.getNewestReleases("feelfreelinux/octo4a-bootstrap-builder").toMutableList()
32 |
33 | // Sort for newest releases
34 | newestReleases.sortByDescending { it.publishedAt }
35 |
36 | val arch = getArchString()
37 |
38 | val bootstrapItems = newestReleases.mapNotNull {
39 | // Get matching asset
40 | val asset = it.assets.firstOrNull { asset -> asset.name.contains(arch) }
41 | val nameSplit = it.name.split("-")
42 |
43 | // Filter out invalid releases
44 | if (nameSplit.size != 2 || (asset == null)) null
45 | else BootstrapItem("OctoPrint ${nameSplit[1]}", nameSplit.first().removePrefix("v"), asset.browserDownloadUrl, it.prerelease)
46 | }
47 |
48 |
49 | // Latest non-prerelease should be the recommended bootstrap
50 | val recommendedItem = bootstrapItems.firstOrNull { !it.prerelease }
51 |
52 | recommendedItem?.recommended = true
53 | _bootstrapReleases.value = bootstrapItems
54 | _selectedRelease.value = recommendedItem
55 |
56 | recommendedItem?.apply {
57 | bootstrapRepository.selectReleaseForInstallation(this)
58 | }
59 | } catch (e: Exception) {
60 | e.printStackTrace()
61 | }
62 | }
63 | }
64 | }
65 |
66 | fun selectBootstrapRelease(bootstrapItem: BootstrapItem) {
67 | _selectedRelease.value = bootstrapItem
68 | bootstrapRepository.selectReleaseForInstallation(bootstrapItem)
69 | }
70 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/ProcessUtils.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils
2 |
3 | import com.bugsnag.android.Bugsnag
4 | import com.octo4a.repository.LogType
5 | import com.octo4a.repository.LoggerRepository
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.withContext
9 | import java.time.Duration
10 | import java.util.Date
11 |
12 | suspend fun withIO(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
13 |
14 | fun Process.waitAndPrintOutput(
15 | logger: LoggerRepository,
16 | type: LogType = LogType.BOOTSTRAP
17 | ): String {
18 | var outputStr = ""
19 | inputStream.reader().forEachLine {
20 | logger.log(this, type) { it }
21 | outputStr += it
22 | }
23 |
24 |
25 | val exitCode = waitFor()
26 | if (exitCode != 0) {
27 | val uselessWarnings = arrayListOf(
28 | "proot warning: can't sanitize binding \"/data/data/com.octo4a/files/serialpipe\": No such file or directory",
29 | "WARNING: linker: ./root/bin/proot: unused DT entry:"
30 | )
31 | val logLines = outputStr
32 | // replace useless proot warning
33 | .let {
34 | uselessWarnings.runningFold(it) { curr, valueToReplace ->
35 | curr.replace(valueToReplace, "")
36 | }
37 | }
38 | .toString()
39 | .lines()
40 | .filter { it.trim() != "" }
41 | .takeLast(2)
42 | .joinToString("\n") { it.take(50) }
43 | throw RuntimeException("Process exited with error code ${exitCode}. $logLines")
44 | }
45 |
46 | return outputStr
47 | }
48 |
49 | fun Process.getOutputAsString(): String {
50 | val log = StringBuilder()
51 | var line: String?
52 | while (inputStream.bufferedReader().readLine().also { line = it } != null) {
53 | log.append(line + "\n")
54 | }
55 |
56 | return log.toString()
57 | }
58 |
59 | fun Process.setPassword(password: String) {
60 | outputStream.bufferedWriter().apply {
61 | write("$password\n")
62 | flush()
63 | write("$password\n")
64 | flush()
65 | }
66 | }
67 |
68 | fun Process.isRunning(): Boolean {
69 | try {
70 | exitValue()
71 | } catch (_: Throwable) {
72 | return true
73 | }
74 | return false
75 | }
76 |
77 | /**
78 | * retryOperation will retry the operation until it succeeds or the maxRetries is reached
79 | * It does not retry if the operation takes less than minSeconds, to avoid retrying operations that fail instantly.
80 | * @param logger the logger to use
81 | * @param maxRetries the maximum number of retries
82 | * @param minSeconds the minimum time the operation needs to run to be retried
83 | */
84 | fun retryOperation(
85 | logger: LoggerRepository,
86 | maxRetries: Int = 2,
87 | minSeconds: Int = 6,
88 | op: () -> Unit
89 | ) {
90 | var timesLeft = maxRetries
91 |
92 | while (true) {
93 | var started = Date()
94 | try {
95 |
96 | op()
97 | return
98 | } catch (e: java.lang.Exception) {
99 | timesLeft--
100 | var now = Date()
101 | // Don't wanna use Duration.between because it's not available on API 24
102 | if (now.time - started.time < minSeconds * 1000) {
103 | throw e
104 | }
105 | if (timesLeft <= 0) {
106 | throw e
107 | }
108 | logger.log { "An error has occurred:$e" }
109 | logger.log { "Retries left: $timesLeft/$maxRetries" }
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/fragment_server.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
12 |
20 |
24 |
32 |
40 |
41 |
42 |
43 |
44 |
53 |
55 |
64 |
66 |
68 |
69 |
78 |
79 |
--------------------------------------------------------------------------------
/bootstrap-builder/dpkg_replacer.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "archive/tar"
5 | "bytes"
6 | "errors"
7 | "flag"
8 | "fmt"
9 | "io"
10 | "log"
11 | "os"
12 | "strings"
13 |
14 | "github.com/blakesmith/ar"
15 | "github.com/ulikunitz/xz"
16 | )
17 |
18 | var REPLACE_FROM = []byte("com.termux")
19 | var REPLACE_TO = []byte("com.octo4a")
20 |
21 | func ReplaceInReader(r io.Reader, w io.Writer) error {
22 | data, err := io.ReadAll(r)
23 | if err != nil {
24 | return fmt.Errorf("failed to read from reader: %w", err)
25 | }
26 | out := bytes.Replace(data, REPLACE_FROM, REPLACE_TO, -1)
27 | r2 := bytes.NewReader(out)
28 | n, err := io.Copy(w, r2)
29 | if err != nil {
30 | return fmt.Errorf("failed to write to writer: %w", err)
31 | }
32 | log.Printf("n == %v, len(out) == %v", n, len(out))
33 | return nil
34 | }
35 |
36 | func ReplaceTarFile(inFile io.Reader, outFile io.Writer) error {
37 | tarFile := tar.NewReader(inFile)
38 |
39 | outTar := tar.NewWriter(outFile)
40 | for {
41 | header, err := tarFile.Next()
42 | if errors.Is(err, io.EOF) {
43 | break
44 | }
45 | if err != nil {
46 | return fmt.Errorf("failed to read from tar file: %w", err)
47 | }
48 | header.Name = strings.ReplaceAll(header.Name, string(REPLACE_FROM), string(REPLACE_TO))
49 | if err := outTar.WriteHeader(header); err != nil {
50 | return fmt.Errorf("failed to write header to tar file: %w", err)
51 | }
52 | log.Printf("name == %v, headerSize = %v", header.Name, header.Size)
53 | if header.Size != 0 {
54 | if err := ReplaceInReader(tarFile, outTar); err != nil {
55 | return fmt.Errorf("failed to replace in reader in tar: %w", err)
56 | }
57 | }
58 |
59 | }
60 |
61 | outTar.Close()
62 | return nil
63 | }
64 |
65 | func ReplaceInArFile(inFile io.Reader, outFile io.Writer) error {
66 | arFile := ar.NewReader(inFile)
67 |
68 | outAr := ar.NewWriter(outFile)
69 | outAr.WriteGlobalHeader()
70 | for {
71 | header, err := arFile.Next()
72 | if errors.Is(err, io.EOF) {
73 | break
74 | }
75 | if err != nil {
76 | return fmt.Errorf("failed to read heade from ar file: %w", err)
77 | }
78 |
79 | buf := &bytes.Buffer{}
80 | if header.Name == "data.tar.xz/" {
81 | xzReader, err := xz.NewReader(arFile)
82 | if err != nil {
83 | return fmt.Errorf("failed to create xzReader: %w", err)
84 | }
85 | header.Name = "data.tar/"
86 | err = ReplaceTarFile(xzReader, buf)
87 | if err != nil {
88 | return fmt.Errorf("failed to replace xz compressed tar file: %w", err)
89 | }
90 |
91 | } else {
92 | log.Printf("Unknown file type %v, leaving as is", header.Name)
93 | _, err := io.Copy(buf, arFile)
94 | if err != nil {
95 | return fmt.Errorf("failed to copy file %v inside of ar archive: %w", header.Name, err)
96 | }
97 | }
98 | header.Size = int64(buf.Len())
99 | err = outAr.WriteHeader(header)
100 | if err != nil {
101 | return fmt.Errorf("failed to write ar header: %w", err)
102 | }
103 | _, err = outAr.Write(buf.Bytes())
104 | if err != nil {
105 | return fmt.Errorf("failed to write ar data: %w", err)
106 | }
107 | }
108 | return nil
109 | }
110 |
111 | func main() {
112 | fmt.Println("BRUH\n")
113 | flag.Parse()
114 | if len(flag.Args()) < 1 {
115 | log.Fatal("missing file")
116 | }
117 | fmt.Println("BRUH\n")
118 | f, err := os.Open(flag.Args()[0])
119 | if err != nil {
120 | log.Fatal("failed to open input .deb file: %v", err)
121 | }
122 | defer f.Close()
123 | outFile, err := os.Create(flag.Args()[0] + ".replaced")
124 | if err != nil {
125 | log.Fatal("failed to open output .deb file: %v", err)
126 | }
127 | defer outFile.Close()
128 | if err := ReplaceInArFile(f, outFile); err != nil {
129 | log.Fatalf("failed to replace in ar file: %v", err)
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/scripts/comm-fix.py:
--------------------------------------------------------------------------------
1 | import octoprint.plugin
2 |
3 | from octoprint.util.pip import create_pip_caller
4 | from octoprint.util.version import ( get_comparable_version, is_octoprint_compatible )
5 |
6 | # Imports from comm.py
7 | import contextlib
8 | import copy
9 | import fnmatch
10 | import glob
11 | import logging
12 | import os
13 | import queue
14 | import re
15 | import threading
16 | import time
17 | from collections import deque
18 |
19 | import serial
20 | import wrapt
21 | import octoprint.util.version
22 | import octoprint.plugin
23 | from octoprint.events import Events, eventManager
24 | from octoprint.filemanager import valid_file_type
25 | from octoprint.filemanager.destinations import FileDestinations
26 | from octoprint.settings import settings
27 | from octoprint.systemcommands import system_command_manager
28 | from octoprint.util import (
29 | CountedEvent,
30 | PrependableQueue,
31 | RepeatedTimer,
32 | ResettableTimer,
33 | TypeAlreadyInQueue,
34 | TypedQueue,
35 | chunks,
36 | filter_non_ascii,
37 | filter_non_utf8,
38 | get_bom,
39 | get_exception_string,
40 | sanitize_ascii,
41 | to_unicode,
42 | comm
43 | )
44 |
45 | from octoprint.util.platform import get_os, set_close_exec
46 |
47 | _logger = logging.getLogger(__name__)
48 |
49 | class Octo4a18Fix(octoprint.plugin.StartupPlugin):
50 | def on_startup(self, host, port):
51 | self._logger.info("Monkey patching comm.py")
52 | def patchedSerialList():
53 | if os.name == "nt":
54 | candidates = []
55 | try:
56 | key = winreg.OpenKey(
57 | winreg.HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM"
58 | )
59 | i = 0
60 | while True:
61 | candidates += [winreg.EnumValue(key, i)[1]]
62 | i += 1
63 | except Exception:
64 | pass
65 |
66 | else:
67 | candidates = []
68 | try:
69 | with os.scandir("/dev") as it:
70 | for entry in it:
71 | if regex_serial_devices.match(entry.name):
72 | candidates.append(entry.path)
73 | except Exception:
74 | pass
75 |
76 | # additional ports
77 | additionalPorts = settings().get(["serial", "additionalPorts"])
78 | if additionalPorts:
79 | for additional in additionalPorts:
80 | candidates += glob.glob(additional)
81 |
82 | hooks = octoprint.plugin.plugin_manager().get_hooks(
83 | "octoprint.comm.transport.serial.additional_port_names"
84 | )
85 | for name, hook in hooks.items():
86 | try:
87 | candidates += hook(candidates)
88 | except Exception:
89 | logging.getLogger(__name__).exception(
90 | "Error while retrieving additional "
91 | "serial port names from hook {}".format(name)
92 | )
93 |
94 | # blacklisted ports
95 | blacklistedPorts = settings().get(["serial", "blacklistedPorts"])
96 | if blacklistedPorts:
97 | for pattern in settings().get(["serial", "blacklistedPorts"]):
98 | candidates = list(
99 | filter(lambda x: not fnmatch.fnmatch(x, pattern), candidates)
100 | )
101 |
102 | # last used port = first to try, move to start
103 | prev = settings().get(["serial", "port"])
104 | if prev in candidates:
105 | candidates.remove(prev)
106 | candidates.insert(0, prev)
107 |
108 | return candidates
109 |
110 | comm.serialList = patchedSerialList
111 |
112 | __plugin_pythoncompat__ = ">=3.0,<4"
113 | __plugin_implementation__ = Octo4a18Fix()
114 | def __plugin_check__():
115 | return is_octoprint_compatible(">=1.8.0")
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/activity_landing.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
19 |
30 |
34 |
42 |
50 |
56 |
64 |
69 |
70 |
71 |
72 |
73 |
74 |
87 |
88 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/camera/NativeCameraUtils.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.camera
2 |
3 | import android.media.Image
4 | import android.util.Log
5 | import androidx.camera.core.ImageProxy
6 | import java.nio.ByteBuffer
7 | import java.nio.ReadOnlyBufferException
8 | import kotlin.experimental.inv
9 |
10 | class NativeCameraUtils {
11 | init {
12 | System.loadLibrary("yuv2rgb")
13 | }
14 |
15 | external fun yuv420toNv21(
16 | imageWidth: Int,
17 | imageHeight: Int,
18 | yByteBuffer: ByteBuffer?,
19 | uByteBuffer: ByteBuffer?,
20 | vByteBuffer: ByteBuffer?,
21 | yPixelStride: Int,
22 | uvPixelStride: Int,
23 | yRowStride: Int,
24 | uvRowStride: Int,
25 | nv21Output: ByteArray?
26 | ): Boolean
27 |
28 | fun toNv21(image: ImageProxy): ByteArray? {
29 | val nv21 = ByteArray((image.width * image.height * 1.5f).toInt())
30 | return if (!yuv420toNv21(
31 | image.width,
32 | image.height,
33 | image.planes[0].buffer, // Y buffer
34 | image.planes[1].buffer, // U buffer
35 | image.planes[2].buffer, // V buffer
36 | image.planes[0].pixelStride, // Y pixel stride
37 | image.planes[1].pixelStride, // U/V pixel stride
38 | image.planes[0].rowStride, // Y row stride
39 | image.planes[1].rowStride, // U/V row stride
40 | nv21
41 | )
42 | ) {
43 | null
44 | } else nv21
45 | }
46 |
47 |
48 | fun yuvToNv21Slow(image: ImageProxy): ByteArray {
49 | val width = image.width
50 | val height = image.height
51 | val ySize = width * height
52 | val uvSize = width * height / 4
53 | val nv21 = ByteArray(ySize + uvSize * 2)
54 | val yBuffer = image.planes[0].buffer // Y
55 | val uBuffer = image.planes[1].buffer // U
56 | val vBuffer = image.planes[2].buffer // V
57 | var rowStride = image.planes[0].rowStride
58 | assert(image.planes[0].pixelStride == 1)
59 | var pos = 0
60 | if (rowStride == width) { // likely
61 | yBuffer[nv21, 0, ySize]
62 | pos += ySize
63 | } else {
64 | var yBufferPos = -rowStride.toLong() // not an actual position
65 | while (pos < ySize) {
66 | yBufferPos += rowStride.toLong()
67 | yBuffer.position(yBufferPos.toInt())
68 | yBuffer[nv21, pos, width]
69 | pos += width
70 | }
71 | }
72 | rowStride = image.planes[2].rowStride
73 | val pixelStride = image.planes[2].pixelStride
74 | assert(rowStride == image.planes[1].rowStride)
75 | assert(pixelStride == image.planes[1].pixelStride)
76 | if (pixelStride == 2 && rowStride == width && uBuffer[0] == vBuffer[1]) {
77 | // maybe V an U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0]
78 | val savePixel = vBuffer[1]
79 | try {
80 | vBuffer.put(1, savePixel.inv() as Byte)
81 | if (uBuffer[0] == savePixel.inv() as Byte) {
82 | vBuffer.put(1, savePixel)
83 | vBuffer.position(0)
84 | uBuffer.position(0)
85 | vBuffer[nv21, ySize, 1]
86 | uBuffer[nv21, ySize + 1, uBuffer.remaining()]
87 | return nv21 // shortcut
88 | }
89 | } catch (ex: ReadOnlyBufferException) {
90 | // unfortunately, we cannot check if vBuffer and uBuffer overlap
91 | }
92 |
93 | // unfortunately, the check failed. We must save U and V pixel by pixel
94 | vBuffer.put(1, savePixel)
95 | }
96 |
97 | // other optimizations could check if (pixelStride == 1) or (pixelStride == 2),
98 | // but performance gain would be less significant
99 | for (row in 0 until height / 2) {
100 | for (col in 0 until width / 2) {
101 | val vuPos = col * pixelStride + row * rowStride
102 | nv21[pos++] = vBuffer[vuPos]
103 | nv21[pos++] = uBuffer[vuPos]
104 | }
105 | }
106 | return nv21
107 | }
108 |
109 |
110 | }
111 |
112 |
113 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/preferences/Preferences.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils.preferences
2 | import android.content.Context
3 | import android.content.SharedPreferences
4 | import android.preference.PreferenceManager
5 | import kotlin.reflect.KProperty
6 |
7 | abstract class Preferences(var context: Context? = null, useDefaultFile: Boolean = false) {
8 |
9 | private val prefs: SharedPreferences by lazy {
10 | if (context != null && useDefaultFile)
11 | PreferenceManager.getDefaultSharedPreferences(context)
12 | else if (context != null)
13 | context!!.getSharedPreferences(javaClass.simpleName, Context.MODE_PRIVATE)
14 | else
15 | throw IllegalStateException("Context was not initialized. Call Preferences.init(context) before using it")
16 | }
17 |
18 | private val listeners = mutableListOf()
19 |
20 | abstract class PrefDelegate(val prefKey: String?) {
21 | abstract operator fun getValue(thisRef: Any?, property: KProperty<*>): T
22 | abstract operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
23 | }
24 |
25 | interface SharedPrefsListener {
26 | fun onSharedPrefChanged(property: KProperty<*>)
27 | }
28 |
29 | fun stringPref(prefKey: String? = null, defaultValue: String? = null) = StringPrefDelegate(prefKey, defaultValue)
30 |
31 | inner class StringPrefDelegate(prefKey: String? = null, val defaultValue: String?) : PrefDelegate(prefKey) {
32 | override fun getValue(thisRef: Any?, property: KProperty<*>): String? = prefs.getString(prefKey ?: property.name, defaultValue)
33 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
34 | prefs.edit().putString(prefKey ?: property.name, value).apply()
35 | onPrefChanged(property)
36 | }
37 | }
38 |
39 | fun intPref(prefKey: String? = null, defaultValue: Int = 0) = IntPrefDelegate(prefKey, defaultValue)
40 |
41 | inner class IntPrefDelegate(prefKey: String? = null, val defaultValue: Int) : PrefDelegate(prefKey) {
42 | override fun getValue(thisRef: Any?, property: KProperty<*>) = prefs.getInt(prefKey ?: property.name, defaultValue)
43 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
44 | prefs.edit().putInt(prefKey ?: property.name, value).apply()
45 | onPrefChanged(property)
46 | }
47 | }
48 |
49 | fun floatPref(prefKey: String? = null, defaultValue: Float = 0.0f) = FloatPrefDelegate(prefKey, defaultValue)
50 |
51 | inner class FloatPrefDelegate(prefKey: String? = null, val defaultValue: Float) : PrefDelegate(prefKey) {
52 | override fun getValue(thisRef: Any?, property: KProperty<*>) = prefs.getFloat(prefKey ?: property.name, defaultValue)
53 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: Float) {
54 | prefs.edit().putFloat(prefKey ?: property.name, value).apply()
55 | onPrefChanged(property)
56 | }
57 | }
58 |
59 | fun booleanPref(prefKey: String? = null, defaultValue: Boolean = false) = BooleanPrefDelegate(prefKey, defaultValue)
60 |
61 | inner class BooleanPrefDelegate(prefKey: String? = null, val defaultValue: Boolean) : PrefDelegate(prefKey) {
62 | override fun getValue(thisRef: Any?, property: KProperty<*>) = prefs.getBoolean(prefKey ?: property.name, defaultValue)
63 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
64 | prefs.edit().putBoolean(prefKey ?: property.name, value).apply()
65 | onPrefChanged(property)
66 | }
67 | }
68 |
69 | fun stringSetPref(prefKey: String? = null, defaultValue: Set = HashSet()) = StringSetPrefDelegate(prefKey, defaultValue)
70 |
71 | inner class StringSetPrefDelegate(prefKey: String? = null, val defaultValue: Set) : PrefDelegate>(prefKey) {
72 | override fun getValue(thisRef: Any?, property: KProperty<*>): Set = prefs.getStringSet(prefKey ?: property.name, defaultValue)!!
73 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set) {
74 | prefs.edit().putStringSet(prefKey ?: property.name, value).apply()
75 | onPrefChanged(property)
76 | }
77 | }
78 |
79 | private fun onPrefChanged(property: KProperty<*>) {
80 | listeners.forEach { it.onSharedPrefChanged(property) }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/camera/RotateUtils.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.camera
2 |
3 |
4 | class RotateUtils {
5 | companion object {
6 | fun rotate270(nv21_data: ByteArray, width: Int, height: Int): ByteArray? {
7 | try {
8 | val y_size = width * height
9 | val buffser_size = y_size * 3 / 2
10 | val nv21_rotated = ByteArray(buffser_size)
11 | var i = 0
12 |
13 | // Rotate the Y luma
14 | for (x in width - 1 downTo 0) {
15 | var offset = 0
16 | for (y in 0 until height) {
17 | nv21_rotated[i] = nv21_data[offset + x]
18 | i++
19 | offset += width
20 | }
21 | }
22 |
23 | // Rotate the U and V color components
24 | i = y_size
25 | var x = width - 1
26 | while (x > 0) {
27 | var offset = y_size
28 | for (y in 0 until height / 2) {
29 | nv21_rotated[i] = nv21_data[offset + (x - 1)]
30 | i++
31 | nv21_rotated[i] = nv21_data[offset + x]
32 | i++
33 | offset += width
34 | }
35 | x = x - 2
36 | }
37 | return nv21_rotated
38 | } catch (e: Exception) {
39 | }
40 | return null
41 | }
42 |
43 | /**
44 | * 旋转180度
45 | */
46 | fun rotate180(nv21_data: ByteArray, width: Int, height: Int): ByteArray? {
47 | try {
48 | val y_size = width * height
49 | val buffser_size = y_size * 3 / 2
50 | val nv21_rotated = ByteArray(buffser_size)
51 | var i = 0
52 | var count = 0
53 | i = y_size - 1
54 | while (i >= 0) {
55 | nv21_rotated[count] = nv21_data[i]
56 | count++
57 | i--
58 | }
59 | i = buffser_size - 1
60 | while (i >= y_size) {
61 | nv21_rotated[count++] = nv21_data[i - 1]
62 | nv21_rotated[count++] = nv21_data[i]
63 | i -= 2
64 | }
65 | return nv21_rotated
66 | } catch (e: Exception) {
67 | }
68 | return null
69 | }
70 |
71 | /**
72 | * 旋转90度
73 | */
74 | fun rotate90(nv21_data: ByteArray, width: Int, height: Int): ByteArray? {
75 | try {
76 | val y_size = width * height
77 | val buffser_size = y_size * 3 / 2
78 | val nv21_rotated = ByteArray(buffser_size)
79 |
80 | // Rotate the Y luma
81 | var i = 0
82 | val startPos = (height - 1) * width
83 | for (x in 0 until width) {
84 | var offset = startPos
85 | for (y in height - 1 downTo 0) {
86 | nv21_rotated[i] = nv21_data[offset + x]
87 | i++
88 | offset -= width
89 | }
90 | }
91 | // Rotate the U and V color components
92 | i = buffser_size - 1
93 | var x = width - 1
94 | while (x > 0) {
95 | var offset = y_size
96 | for (y in 0 until height / 2) {
97 | nv21_rotated[i] = nv21_data[offset + x]
98 | i--
99 | nv21_rotated[i] = nv21_data[offset + (x - 1)]
100 | i--
101 | offset += width
102 | }
103 | x = x - 2
104 | }
105 | return nv21_rotated
106 | } catch (e: Exception) {
107 | }
108 | return null
109 | }
110 |
111 |
112 | fun rotate(bytes: ByteArray, width: Int, height: Int, rotateDegree: Int): ByteArray? {
113 | return if (rotateDegree == 270) {
114 | rotate270(bytes, width, height)
115 | } else if (rotateDegree == 180) {
116 | rotate180(bytes, width, height)
117 |
118 | } else {
119 | rotate90(bytes, width, height)
120 | }
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/app/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
22 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
49 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
70 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.utils
2 |
3 | import android.app.Activity
4 | import android.app.ActivityManager
5 | import android.content.Context
6 | import android.graphics.ImageFormat
7 | import android.graphics.Rect
8 | import android.graphics.YuvImage
9 | import android.net.wifi.WifiManager
10 | import android.os.Build
11 | import android.text.format.Formatter
12 | import android.util.TypedValue
13 | import android.widget.Toast
14 | import androidx.annotation.AttrRes
15 | import androidx.annotation.ColorInt
16 | import androidx.annotation.RequiresApi
17 | import androidx.camera.core.ImageProxy
18 | import java.io.ByteArrayOutputStream
19 | import java.nio.ByteBuffer
20 |
21 |
22 | fun Int.isBitSet(bit: Int): Boolean {
23 | return this and (1 shl bit) != 0
24 | }
25 |
26 | @ColorInt
27 | fun Context.getColorFromAttr(
28 | @AttrRes attrColor: Int,
29 | typedValue: TypedValue = TypedValue(),
30 | resolveRefs: Boolean = true
31 | ): Int {
32 | theme.resolveAttribute(attrColor, typedValue, resolveRefs)
33 | return typedValue.data
34 | }
35 |
36 | fun Context.isServiceRunning(service: Class<*>): Boolean {
37 | val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
38 | manager.getRunningServices(Integer.MAX_VALUE).forEach {
39 | if (service.name.equals(it.service.className)) {
40 | return true
41 | }
42 | }
43 |
44 | return false
45 | }
46 |
47 |
48 | fun ByteArray.NV21toJPEG( width: Int, height: Int, quality: Int): ByteArray? {
49 | val out = ByteArrayOutputStream()
50 | val yuv = YuvImage(this, ImageFormat.NV21, width, height, null)
51 | yuv.compressToJpeg(Rect(0, 0, width, height), quality, out)
52 | return out.toByteArray()
53 | }
54 |
55 |
56 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
57 | fun ImageProxy.YUV420toNV21(): ByteArray {
58 | val width: Int = cropRect.width()
59 | val height: Int = cropRect.height()
60 | val data = ByteArray(width * height * ImageFormat.getBitsPerPixel(format) / 8)
61 | val rowData = ByteArray(planes[0].rowStride)
62 | var channelOffset = 0
63 | var outputStride = 1
64 | for (i in planes.indices) {
65 | when (i) {
66 | 0 -> {
67 | channelOffset = 0
68 | outputStride = 1
69 | }
70 | 1 -> {
71 | channelOffset = width * height + 1
72 | outputStride = 2
73 | }
74 | 2 -> {
75 | channelOffset = width * height
76 | outputStride = 2
77 | }
78 | }
79 | val buffer: ByteBuffer = planes[i].buffer
80 | val rowStride: Int = planes[i].rowStride
81 | val pixelStride: Int = planes[i].pixelStride
82 | val shift = if (i == 0) 0 else 1
83 | val w = width shr shift
84 | val h = height shr shift
85 | buffer.position(rowStride * (cropRect.top shr shift) + pixelStride * (cropRect.left shr shift))
86 | for (row in 0 until h) {
87 | var length: Int
88 | if (pixelStride == 1 && outputStride == 1) {
89 | length = w
90 | buffer.get(data, channelOffset, length)
91 | channelOffset += length
92 | } else {
93 | length = (w - 1) * pixelStride + 1
94 | buffer.get(rowData, 0, length)
95 | for (col in 0 until w) {
96 | data[channelOffset] = rowData[col * pixelStride]
97 | channelOffset += outputStride
98 | }
99 | }
100 | if (row < h - 1) {
101 | buffer.position(buffer.position() + rowStride - length)
102 | }
103 | }
104 | }
105 | return data
106 | }
107 |
108 | private fun is64Bit(): Boolean {
109 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
110 | (Build.SUPPORTED_64_BIT_ABIS != null && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty())
111 | } else {
112 | !(Build.CPU_ABI != "x86" && Build.CPU_ABI2 != "x86")
113 | }
114 | }
115 |
116 | fun getArchString(): String {
117 | var arch = System.getProperty("os.arch")!!.toString()
118 |
119 | if (arch != "x86_64" && arch != "i686") {
120 | arch = if (is64Bit()) {
121 | "aarch64"
122 | } else {
123 | "arm"
124 | }
125 | }
126 | return arch
127 | }
128 |
129 | val Context.ipAddress: String
130 | get() {
131 | val wm = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
132 | return Formatter.formatIpAddress(wm.connectionInfo.ipAddress)
133 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # octo4a - Run OctoPrint on Android
2 | [](https://paypal.me/feelfreelinux)
3 | 
4 | 
5 |
6 |
7 | 
8 |
9 | You don't have a Raspberry Pi, but you want to control your 3D printer remotely? Use your phone as an octoprint host! With the Octo4a app you can install Octoprint on your android phone in minutes, without any special Linux knowledge.
10 |
11 | ## Important note about charging and using OTG
12 |
13 | Some phones do not support simultaneous charging and USB on the go (OTG). Please consult this community contributed list of phones our users use, If you wish to buy a phone for the purpose of running octo4a:
14 |
15 | https://docs.google.com/spreadsheets/d/1pbKAELzvTrMVm6NpDpK8TfQR33w3QHQEgqkcGdbPFI0/edit?usp=sharing
16 |
17 | If you have a phone that is not on the list, please add it and let us know if it works or not:
18 |
19 | https://forms.gle/cnTtdDqG3UcwTgnW9
20 |
21 | ## Download
22 |
23 | Newest `.apk` files are available in the [releases page](https://github.com/feelfreelinux/octo4a/releases).
24 |
25 | ## Usage
26 |
27 | 1. Enable installing 3rd-party .apk in your phone's settings.
28 | 2. Install the apk file downloaded from the releases page.
29 | 3. Open the app.
30 | 4. Click "Install OctoPrint" to download and install OctoPrint (needs working connection to internet & querries Google DNS servers 8.8.8.8 and 8.8.4.4)
31 | 5. Allow the app to access the storage, if asked for permission.
32 | 6. Wait for the installation to complete. This may take a long time, depending on your internet speed.
33 | 7. Click "Continue" when the installation finishes.
34 | 8. Optionally start the camera server to enable watching your printer from octoprint.
35 | 9. Navigate to the IP address shown at the top in your browser to access and set-up OctoPrint.
36 | 10. Happy printing!
37 |
38 | ## Features
39 |
40 | - Quick and easy octoprint installation.
41 | - Printer connection via USB OTG. Thanks to our custom USB driver you can use octoprint even on phones without root access.
42 | - Built-in camera support. You can use the built-in camera in your phone to see the progress of your 3D prints, instead of buying a separate module. The app also supports octolapse.
43 | - SSH support. You can easily log-in via ssh and customize your octoprint installation.
44 |
45 | ## Wiki
46 |
47 | FAQ and many different topics are described in the [project's wiki](https://github.com/feelfreelinux/octo4a/wiki)
48 |
49 | ## Video tutorials
50 |
51 | Here are some video tutorials made by the octo4a community that will help you with setting everything up:
52 |
53 | - ["Octo4A - Octoprint On Your Android Phone - 2022 - Chris's Basement" by Chris Riley](https://www.youtube.com/watch?v=2Psbwc-NFTU)
54 |
55 | [](https://www.youtube.com/watch?v=2Psbwc-NFTU)
56 | - ["How to run OctoPrint on your phone!" by Thomas Sanladerer](https://www.youtube.com/watch?v=74xdib_-X38)
57 |
58 | [](https://www.youtube.com/watch?v=74xdib_-X38)
59 | - ["Running OctoPrint on Android with Octo4a!" by Sumit Basra](https://www.youtube.com/watch?v=X56itQuqXY4)
60 |
61 | [](https://www.youtube.com/watch?v=X56itQuqXY4)
62 |
63 |
64 | ## Contributing
65 |
66 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
67 |
68 | ## Donating
69 |
70 | If you like this project, feel free to [donate on PayPal](https://paypal.me/feelfreelinux) or [sponsor me on GitHub](https://github.com/sponsors/feelfreelinux). This project heavily relies on great work done on [OctoPrint](https://www.patreon.com/foosel) so please consider donating to them if you like this app. Thank you for your support :)
71 |
72 | ## Disclaimer
73 |
74 | ```
75 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
76 | ```
77 |
78 | TL;DR: If your house burns down because this app malfunctioned, you cannot sue me.
79 |
80 |
81 | ## License
82 |
83 | This project is licensed under the AGPL license.
84 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/view_status_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
17 |
18 |
25 |
26 |
27 |
38 |
39 |
53 |
54 |
66 |
67 |
76 |
77 |
86 |
87 |
100 |
101 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/fragments/TerminalSheetDialog.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui.fragments
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Dialog
5 | import android.content.DialogInterface
6 | import android.content.Intent
7 | import android.content.res.Resources
8 | import android.graphics.Color
9 | import android.os.Bundle
10 | import android.os.Handler
11 | import android.os.Looper
12 | import android.text.Spannable
13 | import android.text.SpannableString
14 | import android.text.TextUtils
15 | import android.text.style.ForegroundColorSpan
16 | import android.util.Log
17 | import android.view.LayoutInflater
18 | import android.view.View
19 | import android.view.ViewGroup
20 | import android.widget.FrameLayout
21 | import android.widget.TextView
22 | import androidx.lifecycle.asLiveData
23 | import com.google.android.material.bottomsheet.BottomSheetBehavior
24 | import com.google.android.material.bottomsheet.BottomSheetDialog
25 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
26 | import com.octo4a.R
27 | import com.octo4a.repository.LogEntry
28 | import com.octo4a.repository.LogType
29 | import com.octo4a.repository.LoggerRepository
30 | import com.octo4a.repository.getTypeEmoji
31 | import kotlinx.android.synthetic.main.fragment_terminal_sheet.*
32 | import kotlinx.coroutines.*
33 | import org.koin.android.ext.android.inject
34 |
35 |
36 | class TerminalSheetDialog: BottomSheetDialogFragment() {
37 | val logger: LoggerRepository by inject()
38 | var logCache = mutableListOf()
39 | var shouldUpdateTermUi = true
40 | private val refreshRate = 500L
41 |
42 | @SuppressLint("SetTextI18n")
43 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
44 | super.onViewCreated(view, savedInstanceState)
45 | logger.logHistoryFlow.asLiveData().observe(viewLifecycleOwner) {
46 | logCache.add(it)
47 | }
48 |
49 | // Update logs every 1/4 second, takes care of rapid logs
50 | CoroutineScope(Dispatchers.IO).launch {
51 | while(shouldUpdateTermUi) {
52 | if (logCache.isNotEmpty()) {
53 | withContext(Dispatchers.Main) {
54 | if (terminalView == null) return@withContext
55 | var textToDisplay: CharSequence? = terminalView.text
56 |
57 | logCache.forEach {
58 | // Display logs
59 | val prefix = it.type.getTypeEmoji()
60 |
61 | textToDisplay = textToDisplay.toString() + "$prefix ${it.entry}\n"
62 | }
63 |
64 | terminalView?.setText(textToDisplay, TextView.BufferType.SPANNABLE)
65 |
66 | // Auto scroll if enabled
67 | if (enableAutoScroll.isChecked) {
68 | scrollView.post {
69 | scrollView.fullScroll(View.FOCUS_DOWN)
70 | }
71 | }
72 | }
73 |
74 | logCache.clear()
75 | }
76 | delay(refreshRate)
77 | }
78 | }
79 |
80 |
81 | closeDialog.setOnClickListener {
82 | dismiss()
83 | }
84 |
85 | shareLogs.setOnClickListener {
86 | val shareIntent: Intent = Intent()
87 | shareIntent.action = Intent.ACTION_SEND
88 | shareIntent.type = "text/plain"
89 | shareIntent.putExtra(Intent.EXTRA_TEXT, terminalView.text.toString())
90 | startActivity(Intent.createChooser(shareIntent, "Share logs"))
91 |
92 | }
93 | }
94 |
95 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
96 | return inflater.inflate(R.layout.fragment_terminal_sheet, container, false)
97 | }
98 |
99 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
100 | val bottomSheetDialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
101 | bottomSheetDialog.setOnShowListener { dialog: DialogInterface ->
102 | val dialogc = dialog as BottomSheetDialog
103 | val bottomSheet =
104 | dialogc.findViewById(R.id.design_bottom_sheet) as FrameLayout
105 | val bottomSheetBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(bottomSheet)
106 | bottomSheetBehavior.peekHeight = Resources.getSystem().displayMetrics.heightPixels
107 | bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
108 | }
109 | return bottomSheetDialog
110 | }
111 |
112 | override fun onDestroy() {
113 | super.onDestroy()
114 | shouldUpdateTermUi = false
115 | }
116 | }
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/views/UsbDeviceView.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui.views
2 |
3 | import android.animation.LayoutTransition
4 | import android.content.Context
5 | import android.os.Build
6 | import android.util.AttributeSet
7 | import android.view.LayoutInflater
8 | import android.widget.Toast
9 | import androidx.constraintlayout.widget.ConstraintLayout
10 | import com.google.android.material.bottomsheet.BottomSheetDialog
11 | import com.octo4a.R
12 | import com.octo4a.serial.ConnectedUsbDevice
13 | import com.octo4a.serial.SerialDriverClass
14 | import com.octo4a.serial.VirtualSerialDriver
15 | import com.octo4a.serial.getCustomPrinterProber
16 | import kotlinx.android.synthetic.main.select_driver_bottom_sheet.*
17 | import kotlinx.android.synthetic.main.view_usb_devices_item.view.*
18 |
19 | class UsbDeviceView @JvmOverloads
20 | constructor(private val ctx: Context, private val vsp: VirtualSerialDriver, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0)
21 | : ConstraintLayout(ctx, attributeSet, defStyleAttr) {
22 |
23 | private val prober by lazy { getCustomPrinterProber() }
24 |
25 | init {
26 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
27 | inflater.inflate(R.layout.view_usb_devices_item, this)
28 | layoutTransition = LayoutTransition()
29 | }
30 |
31 | fun drawDriverInfo(usbDevice: ConnectedUsbDevice) {
32 | val driverText = when (usbDevice.driverClass) {
33 | SerialDriverClass.CDC -> "CDC"
34 | SerialDriverClass.CH341 -> "CH341"
35 | SerialDriverClass.PROLIFIC -> "Prolific"
36 | SerialDriverClass.FTDI -> "FTDI"
37 | SerialDriverClass.CP21XX -> "CP21xx"
38 | SerialDriverClass.UNKNOWN -> "Unknown"
39 | }
40 |
41 | serialDriverText.text = driverText + " " + context.getString(R.string.serial_driver)
42 | if (!usbDevice.autoDetect) {
43 | serialDriverText.text = serialDriverText.text.toString() + context.getString(R.string.tap_to_select)
44 | }
45 | }
46 |
47 | fun setUsbDevice(usbDevice: ConnectedUsbDevice) {
48 | vidPidText.text = "VID " + usbDevice.vendorId.toString(16) + " / PID " + usbDevice.productId.toString(16)
49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
50 | titleText.text = usbDevice.device.productName
51 | }
52 |
53 | drawDriverInfo(usbDevice)
54 | selectCheckbox.isChecked = usbDevice.isSelected
55 | selectCheckbox.setOnCheckedChangeListener { _, checked ->
56 | if (checked) {
57 | if (usbDevice.driverClass != SerialDriverClass.UNKNOWN) {
58 | vsp.tryToSelectDevice(usbDevice)
59 | } else {
60 | selectCheckbox.isChecked = false
61 | Toast.makeText(context, context.getString(R.string.requesting_usb_permission), Toast.LENGTH_LONG).show()
62 | }
63 | } else {
64 | selectCheckbox.isChecked = true
65 | }
66 | }
67 |
68 |
69 | setOnClickListener {
70 | openBottomSheet(usbDevice)
71 | }
72 | }
73 |
74 | fun openBottomSheet(usbDevice: ConnectedUsbDevice) {
75 | if (usbDevice.autoDetect) {
76 | return
77 | }
78 | val bottomSheetDialog = BottomSheetDialog(context)
79 | bottomSheetDialog.setContentView(R.layout.select_driver_bottom_sheet)
80 |
81 |
82 | bottomSheetDialog.apply {
83 | ftdi.setOnClickListener {
84 | usbDevice.driverClass = SerialDriverClass.FTDI
85 | drawDriverInfo(usbDevice)
86 | vsp.tryToSelectDevice(usbDevice)
87 | dismiss()
88 | }
89 |
90 | cp21xx.setOnClickListener {
91 | usbDevice.driverClass = SerialDriverClass.CP21XX
92 | drawDriverInfo(usbDevice)
93 | vsp.tryToSelectDevice(usbDevice)
94 | dismiss()
95 | }
96 |
97 | prolific.setOnClickListener {
98 | usbDevice.driverClass = SerialDriverClass.PROLIFIC
99 | drawDriverInfo(usbDevice)
100 | vsp.tryToSelectDevice(usbDevice)
101 | dismiss()
102 | }
103 |
104 | cdcAcm.setOnClickListener {
105 | usbDevice.driverClass = SerialDriverClass.CDC
106 | drawDriverInfo(usbDevice)
107 | vsp.tryToSelectDevice(usbDevice)
108 | dismiss()
109 | }
110 |
111 | ch341.setOnClickListener {
112 | usbDevice.driverClass = SerialDriverClass.CH341
113 | drawDriverInfo(usbDevice)
114 | vsp.tryToSelectDevice(usbDevice)
115 | dismiss()
116 | }
117 | }
118 | bottomSheetDialog.show()
119 | }
120 | }
--------------------------------------------------------------------------------
/app/app/src/main/res/xml/main_preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
15 |
18 |
24 |
30 |
37 |
44 |
45 |
48 |
54 |
60 |
66 |
72 |
73 |
82 |
83 |
84 |
93 |
100 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/app/app/src/main/res/layout/select_driver_bottom_sheet.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
13 |
20 |
22 |
28 |
34 |
35 |
36 |
37 |
44 |
45 |
51 |
52 |
53 |
60 |
61 |
67 |
68 |
69 |
70 |
77 |
78 |
79 |
85 |
86 |
87 |
88 |
95 |
96 |
102 |
103 |
104 |
111 |
112 |
118 |
119 |
--------------------------------------------------------------------------------
/app/app/src/main/java/com/octo4a/ui/views/StatusView.kt:
--------------------------------------------------------------------------------
1 | package com.octo4a.ui.views
2 |
3 | import android.content.Context
4 | import android.graphics.PorterDuff
5 | import android.util.AttributeSet
6 | import android.view.LayoutInflater
7 | import android.view.ViewGroup
8 | import androidx.annotation.ColorInt
9 | import androidx.annotation.ColorRes
10 | import androidx.annotation.DrawableRes
11 | import androidx.constraintlayout.widget.ConstraintLayout
12 | import androidx.core.content.ContextCompat
13 | import androidx.core.view.setPadding
14 | import androidx.recyclerview.widget.LinearLayoutManager
15 | import androidx.recyclerview.widget.RecyclerView
16 | import com.octo4a.R
17 | import com.octo4a.repository.ServerStatus
18 | import com.octo4a.viewmodel.IPAddress
19 | import kotlinx.android.synthetic.main.view_installation_item.view.*
20 | import kotlinx.android.synthetic.main.view_status_card.view.*
21 |
22 | class IPAddressListAdapter(private val dataSet: Array) :
23 | RecyclerView.Adapter() {
24 | class ViewHolder(val ipAddressView: IPAddressView) : RecyclerView.ViewHolder(ipAddressView) {}
25 |
26 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
27 | val view = IPAddressView(viewGroup.context)
28 | return ViewHolder(view)
29 | }
30 |
31 | override fun getItemCount(): Int {
32 | return dataSet.size
33 | }
34 |
35 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
36 | holder.ipAddressView.ipAddress = dataSet[position]
37 | }
38 | }
39 |
40 | class StatusView @JvmOverloads
41 | constructor(
42 | private val ctx: Context,
43 | private val attributeSet: AttributeSet? = null,
44 | private val defStyleAttr: Int = 0
45 | ) : ConstraintLayout(ctx, attributeSet, defStyleAttr) {
46 |
47 | var onActionClicked: () -> Unit = {}
48 |
49 | init {
50 | val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
51 | inflater.inflate(R.layout.view_status_card, this)
52 | setPadding(12)
53 | attributeSet?.apply {
54 | val styledAttributes =
55 | context.obtainStyledAttributes(this, R.styleable.StatusView, 0, 0)
56 | val titleValue = styledAttributes.getString(R.styleable.StatusView_title)
57 | val subtitleValue = styledAttributes.getString(R.styleable.StatusView_subtitle)
58 | val icon = styledAttributes.getDrawable(R.styleable.StatusView_icon)
59 | val iconColor = styledAttributes.getColor(R.styleable.StatusView_iconColor, 0)
60 |
61 | val backgroundDrawable = ContextCompat.getDrawable(context, R.drawable.circle_drawable)
62 | backgroundDrawable!!.setColorFilter(iconColor, PorterDuff.Mode.SRC_ATOP)
63 | iconCircle.background = backgroundDrawable
64 |
65 | iconCircle.iconView.setImageDrawable(icon)
66 | titleText.text = titleValue
67 | subtitleText.text = subtitleValue
68 | styledAttributes.recycle()
69 | ipAddressList.adapter = IPAddressListAdapter(arrayOf())
70 | ipAddressList.layoutManager = LinearLayoutManager(context)
71 | }
72 |
73 | actionButton.setOnClickListener {
74 | onActionClicked()
75 | }
76 | }
77 |
78 | fun setDrawableAndColor(@DrawableRes resource: Int, @ColorRes colorRes: Int) {
79 | val backgroundDrawable = ContextCompat.getDrawable(context, resource)
80 | val color = ContextCompat.getColor(context, colorRes);
81 | backgroundDrawable!!.setColorFilter(color, PorterDuff.Mode.SRC_ATOP)
82 | actionButton.background = backgroundDrawable
83 | }
84 |
85 | var title: String
86 | get() = ""
87 | set(value) {
88 | titleText.text = value
89 | }
90 |
91 | var subtitle: String
92 | get() = ""
93 | set(value) {
94 | subtitleText.text = value
95 | }
96 | var showIpAddresses: Boolean
97 | get() = false
98 | set(value) {
99 | if (value) {
100 | ipAddressList.visibility = VISIBLE
101 | subtitleText.visibility = GONE
102 | warningText.visibility = if (warningText.text.isEmpty()) GONE else VISIBLE
103 | } else {
104 | ipAddressList.visibility = GONE
105 | subtitleText.visibility = VISIBLE
106 | warningText.visibility = GONE
107 | }
108 | }
109 |
110 | var ipAddresses: Array
111 | get() = arrayOf()
112 | set(value) {
113 | ipAddressList.adapter = IPAddressListAdapter(value)
114 | }
115 | var warning: String
116 | get() = ""
117 | set(value) {
118 | warningText.text = value
119 | warningText.visibility = if (value.isEmpty() || ipAddressList.visibility != VISIBLE) GONE else VISIBLE
120 | }
121 | }
122 |
123 |
124 |
--------------------------------------------------------------------------------