├── .gitignore ├── LICENSE ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── jsinterfacetest.html │ ├── java │ └── me │ │ └── rapierxbox │ │ └── shellyelevatev2 │ │ ├── BootReceiver.java │ │ ├── Constants.java │ │ ├── HttpServer.java │ │ ├── MainActivity.kt │ │ ├── SettingsActivity.kt │ │ ├── SettingsParser.java │ │ ├── ShellyElevateApplication.java │ │ ├── ShellyElevateJavascriptInterface.java │ │ ├── backbutton │ │ ├── BackAccessibilityService.kt │ │ └── FloatingBackButtonService.kt │ │ ├── helper │ │ ├── DeviceHelper.java │ │ ├── DeviceSensorManager.java │ │ ├── MediaHelper.java │ │ ├── ServiceHelper.java │ │ └── SwipeHelper.java │ │ ├── mqtt │ │ ├── MQTTServer.java │ │ └── ShellyElevateMQTTCallback.java │ │ └── screensavers │ │ ├── DigitalClockAndDateScreenSaver.java │ │ ├── DigitalClockScreenSaver.java │ │ ├── ScreenOffScreenSaver.java │ │ ├── ScreenSaver.java │ │ ├── ScreenSaverManager.java │ │ ├── ScreenSaverManagerHolder.kt │ │ ├── UserInteractionReceiver.kt │ │ └── activities │ │ └── DigitalClockAndDateScreenSaverActivity.kt │ └── res │ ├── drawable │ ├── ic_back_arrow.xml │ ├── ic_exit.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_settings.xml │ └── round_button_bg.xml │ ├── layout │ ├── digital_clock_and_date_screen_saver.xml │ ├── floating_button_layout.xml │ ├── main_activity.xml │ └── settings_activity.xml │ ├── menu │ └── settings_menu.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values-night │ ├── colors.xml │ ├── theme_overlays.xml │ └── themes.xml │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── donottranslate.xml │ ├── strings.xml │ ├── theme_overlays.xml │ └── themes.xml │ └── xml │ └── accessibility_service_config.xml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | app/build/* 17 | .kotlin/* 18 | .idea/* 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 RapierXbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShellyElevate 2 | > [!IMPORTANT] 3 | > Make sure to update your Home Assistant config.yaml file to comply with the new API. You can find the new yaml in the wiki. 4 | 5 | > [!CAUTION] 6 | > All content in this repository is provided "as is" and may render your device unusable. Always exercise caution when working with your device. No warranty or guarantee is provided. 7 | 8 | Shelly Elevate is an app designed for the Shelly Wall Display, codenamed Stargate, that add's full Home Assistant functionality to the device. The Wiki also provides a detailed tutorial on hacking your device, installing a launcher, configuring Shelly Elevate, and integrating it with Home Assistant.
9 | 10 | https://github.com/user-attachments/assets/adf46edd-9bf1-45da-b553-bf7781d17fbd 11 | 12 | ### Features 13 | * full screen Home Assistant controll 14 | * automatic Home Assistant IP detection 15 | * autostart 16 | * swipe to switch 17 | * full access to sensors and the relay over a api 18 | * playing sound files over the api 19 | * hidden settings 20 | * automatic brightness 21 | * multiple screen savers with settable delay 22 | * changing settings over the api 23 | * lite mode 24 | * support for all displays 25 | * viewing any url 26 | 27 | And of couse you can disable each feature compeltely. 28 | 29 | If you'd like to contribute or have a feature request, please do so by creating a pull request or opening an issue. 30 | 31 | ### Dont know where to start? 32 | Hack your display using the [guide](https://github.com/RapierXbox/ShellyElevate/wiki/Jailbreak) or check out the [releases](https://github.com/RapierXbox/ShellyElevate/releases). 33 | If you want to add the modified display to Home Assistant, check out [this](https://github.com/RapierXbox/ShellyElevate/wiki/Integration-into-Home-Assistant). 34 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "me.rapierxbox.shellyelevatev2" 8 | compileSdk = 35 9 | 10 | defaultConfig { 11 | applicationId = "me.rapierxbox.shellyelevatev2" 12 | minSdk = 24 13 | //noinspection ExpiredTargetSdkVersion 14 | targetSdk = 24 15 | versionCode = 2_03_00 16 | versionName = "2.3.0" 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | vectorDrawables { 20 | useSupportLibrary = true 21 | } 22 | } 23 | 24 | buildTypes { 25 | release { 26 | isMinifyEnabled = false 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_17 35 | targetCompatibility = JavaVersion.VERSION_17 36 | } 37 | kotlinOptions { 38 | jvmTarget = "17" 39 | } 40 | packaging { 41 | resources { 42 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 43 | } 44 | } 45 | buildFeatures { 46 | viewBinding = true 47 | buildConfig = true 48 | } 49 | } 50 | 51 | dependencies { 52 | 53 | implementation(libs.appcompat) 54 | implementation(libs.material) 55 | implementation(libs.lifecycle.runtime.ktx) 56 | implementation(libs.preference) 57 | implementation(libs.nanohttpd) 58 | implementation(libs.org.eclipse.paho.mqttv5.client) 59 | 60 | implementation(platform(libs.okhttpbom)) 61 | implementation(libs.okhttp) 62 | implementation(libs.appcompat) 63 | 64 | testImplementation(libs.junit) 65 | androidTestImplementation(libs.ext.junit) 66 | androidTestImplementation(libs.espresso.core) 67 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 15 | 16 | 19 | 20 | 21 | 30 | 34 | 35 | 39 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app/src/main/assets/jsinterfacetest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JS Interface Test 7 | 14 | 15 | 16 |

Test ShellyElevate Interface

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

36 | 37 | 104 | 105 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/BootReceiver.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2; 2 | 3 | import static android.content.Context.MODE_PRIVATE; 4 | import static me.rapierxbox.shellyelevatev2.Constants.SHARED_PREFERENCES_NAME; 5 | import static me.rapierxbox.shellyelevatev2.Constants.SP_LITE_MODE; 6 | 7 | import android.content.BroadcastReceiver; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.util.Log; 11 | 12 | import java.util.Objects; 13 | 14 | 15 | public class BootReceiver extends BroadcastReceiver { 16 | @Override 17 | public void onReceive(Context context, Intent intent) { 18 | if (Objects.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) { 19 | Log.i("ShellyElevateV2", "Starting... (If not already started)"); 20 | 21 | if (context.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE).getBoolean(SP_LITE_MODE, false)) { 22 | Intent appIntent = new Intent(context, ShellyElevateApplication.class); 23 | context.startService(appIntent); 24 | } else { 25 | Log.i("ShellyElevateV2", "Starting MainActivity"); 26 | Intent activityIntent = new Intent(context, MainActivity.class); 27 | context.startActivity(activityIntent); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/Constants.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2; 2 | 3 | public class Constants { 4 | public static final String SHARED_PREFERENCES_NAME = "ShellyElevateV2"; 5 | 6 | public static final String SP_WEBVIEW_URL = "webviewUrl"; 7 | public static final String SP_HTTP_SERVER_ENABLED = "httpServer"; 8 | public static final String SP_SWITCH_ON_SWIPE = "switchOnSwipe"; 9 | public static final String SP_AUTOMATIC_BRIGHTNESS = "automaticBrightness"; 10 | public static final String SP_MIN_BRIGHTNESS = "minBrightness"; 11 | public static final String SP_BRIGHTNESS = "brightness"; 12 | public static final String SP_SCREEN_SAVER_ENABLED = "screenSaver"; 13 | public static final String SP_SCREEN_SAVER_DELAY = "screenSaverDelay"; 14 | public static final String SP_SCREEN_SAVER_ID = "screenSaverId"; 15 | public static final String SP_LITE_MODE = "liteMode"; 16 | public static final String SP_EXTENDED_JAVASCRIPT_INTERFACE = "extendedJavascriptInterface"; 17 | 18 | public static final String SP_MQTT_ENABLED = "mqttEnabled"; 19 | public static final String SP_MQTT_BROKER = "mqttBroker"; 20 | public static final String SP_MQTT_PORT = "mqttPort"; 21 | public static final String SP_MQTT_USERNAME = "mqttUsername"; 22 | public static final String SP_MQTT_PASSWORD = "mqttPassword"; 23 | public static final String SP_MQTT_DEVICE_ID = "mqttDeviceId"; 24 | 25 | public static final String SP_DEPRECATED_HA_IP = "homeAssistantIp"; 26 | 27 | public static final String INTENT_WEBVIEW_REFRESH = "me.rapierxbox.shellyelevatev2.REFRESH_WEBVIEW"; 28 | public static final String INTENT_WEBVIEW_INJECT_JAVASCRIPT = "me.rapierxbox.shellyelevatev2.WEBVIEW_INJECT_JAVASCRIPT"; 29 | 30 | public static final String MQTT_TOPIC_CONFIG_DEVICE = "homeassistant/device/%s/config"; 31 | public static final String MQTT_TOPIC_STATUS = "shellyelevatev2/%s/status"; 32 | 33 | public static final String MQTT_TOPIC_TEMP_SENSOR = "shellyelevatev2/%s/temp"; 34 | public static final String MQTT_TOPIC_HUM_SENSOR = "shellyelevatev2/%s/hum"; 35 | public static final String MQTT_TOPIC_LUX_SENSOR = "shellyelevatev2/%s/lux"; 36 | public static final String MQTT_TOPIC_RELAY_STATE = "shellyelevatev2/%s/relay_state"; 37 | public static final String MQTT_TOPIC_RELAY_COMMAND = "shellyelevatev2/%s/relay_command"; 38 | public static final String MQTT_TOPIC_SLEEP_BUTTON = "shellyelevatev2/%s/sleep"; 39 | public static final String MQTT_TOPIC_WAKE_BUTTON = "shellyelevatev2/%s/wake"; 40 | public static final String MQTT_TOPIC_REFRESH_WEBVIEW_BUTTON = "shellyelevatev2/%s/refresh_webview"; 41 | public static final String MQTT_TOPIC_REBOOT_BUTTON = "shellyelevatev2/%s/reboot"; 42 | public static final String MQTT_TOPIC_SWIPE_EVENT = "shellyelevatev2/%s/swipe_event"; 43 | public static final String MQTT_TOPIC_SLEEPING_BINARY_SENSOR = "shellyelevatev2/%s/sleeping"; 44 | 45 | public static final String MQTT_TOPIC_HOME_ASSISTANT_STATUS = "homeassistant/status"; 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/HttpServer.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2; 2 | 3 | import static me.rapierxbox.shellyelevatev2.Constants.INTENT_WEBVIEW_INJECT_JAVASCRIPT; 4 | import static me.rapierxbox.shellyelevatev2.Constants.INTENT_WEBVIEW_REFRESH; 5 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mApplicationContext; 6 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceHelper; 7 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceSensorManager; 8 | 9 | import android.content.Intent; 10 | import android.net.Uri; 11 | import android.util.Log; 12 | import android.widget.Toast; 13 | 14 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; 15 | 16 | import org.json.JSONException; 17 | import org.json.JSONObject; 18 | 19 | import java.io.IOException; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | import fi.iki.elonen.NanoHTTPD; 24 | import me.rapierxbox.shellyelevatev2.helper.MediaHelper; 25 | import me.rapierxbox.shellyelevatev2.screensavers.ScreenSaverManagerHolder; 26 | 27 | public class HttpServer extends NanoHTTPD { 28 | public HttpServer() { 29 | super(8080); 30 | } 31 | 32 | SettingsParser mSettingsParser = new SettingsParser(); 33 | MediaHelper mMediaHelper = new MediaHelper(); 34 | 35 | @Override 36 | public Response serve(IHTTPSession session) { 37 | Method method = session.getMethod(); 38 | String uri = session.getUri(); 39 | JSONObject jsonResponse = new JSONObject(); 40 | 41 | try { 42 | if (uri.startsWith("/media/")) { 43 | return handleMediaRequest(session); 44 | } else if (uri.startsWith("/device/")) { 45 | return handleDeviceRequest(session); 46 | } else if (uri.startsWith("/webview/")) { 47 | return handleWebviewRequest(session); 48 | } else if (uri.equals("/settings")) { 49 | if (method.equals(Method.GET)) { 50 | jsonResponse.put("success", true); 51 | jsonResponse.put("settings", mSettingsParser.getSettings()); 52 | } else if (method.equals(Method.POST)) { 53 | Map files = new HashMap<>(); 54 | session.parseBody(files); 55 | String postData = files.get("postData"); 56 | JSONObject jsonObject = new JSONObject(postData); 57 | 58 | mSettingsParser.setSettings(jsonObject); 59 | 60 | jsonResponse.put("success", true); 61 | jsonResponse.put("settings", mSettingsParser.getSettings()); 62 | } else { 63 | jsonResponse.put("success", false); 64 | jsonResponse.put("error", "Invalid request method"); 65 | } 66 | return newFixedLengthResponse(Response.Status.OK, "application/json", jsonResponse.toString()); 67 | } else if (uri.equals("/")) { 68 | return newFixedLengthResponse(Response.Status.OK, "application/json", 69 | "{\"message\": \"ShellyElevateV2 by RapierXbox\"}"); 70 | } 71 | } catch (JSONException | ResponseException | IOException e) { 72 | Log.e("HttpServer", "Error handling request", e); 73 | } 74 | 75 | return newFixedLengthResponse(Response.Status.NOT_FOUND, "application/json", jsonResponse.toString()); 76 | } 77 | 78 | private Response handleWebviewRequest(IHTTPSession session) throws JSONException, ResponseException, IOException { 79 | Method method = session.getMethod(); 80 | String uri = session.getUri(); 81 | JSONObject jsonResponse = new JSONObject(); 82 | 83 | switch (uri.replace("/webview/", "")) { 84 | case "refresh": 85 | if (method.equals(Method.GET)) { 86 | Intent intent = new Intent(INTENT_WEBVIEW_REFRESH); 87 | LocalBroadcastManager.getInstance(ShellyElevateApplication.mApplicationContext).sendBroadcast(intent); 88 | jsonResponse.put("success", true); 89 | } 90 | case "inject": 91 | if (method.equals(Method.POST)) { 92 | Map files = new HashMap<>(); 93 | session.parseBody(files); 94 | String postData = files.get("postData"); 95 | JSONObject jsonObject = new JSONObject(postData); 96 | 97 | String javascript = jsonObject.getString("javascript"); 98 | 99 | Intent intent = new Intent(INTENT_WEBVIEW_INJECT_JAVASCRIPT); 100 | intent.putExtra("javascript", javascript); 101 | LocalBroadcastManager.getInstance(ShellyElevateApplication.mApplicationContext).sendBroadcast(intent); 102 | 103 | jsonResponse.put("success", true); 104 | } 105 | } 106 | 107 | return newFixedLengthResponse(jsonResponse.getBoolean("success") ? Response.Status.OK : Response.Status.INTERNAL_ERROR, "application/json", jsonResponse.toString()); 108 | } 109 | 110 | private Response handleMediaRequest(IHTTPSession session) throws JSONException, ResponseException, IOException { 111 | Method method = session.getMethod(); 112 | String uri = session.getUri(); 113 | JSONObject jsonResponse = new JSONObject(); 114 | 115 | switch (uri.replace("/media/", "")) { 116 | case "play": 117 | if (method.equals(Method.POST)) { 118 | Map files = new HashMap<>(); 119 | session.parseBody(files); 120 | String postData = files.get("postData"); 121 | JSONObject jsonObject = new JSONObject(postData); 122 | 123 | Uri mediaUri = Uri.parse(jsonObject.getString("url")); 124 | boolean music = jsonObject.getBoolean("music"); 125 | double volume = jsonObject.getDouble("volume"); 126 | 127 | mMediaHelper.setVolume(volume); 128 | 129 | if (music) { 130 | mMediaHelper.playMusic(mediaUri); 131 | } else { 132 | mMediaHelper.playEffect(mediaUri); 133 | } 134 | 135 | jsonResponse.put("success", true); 136 | jsonResponse.put("url", jsonObject.getString("url")); 137 | jsonResponse.put("music", music); 138 | jsonResponse.put("volume", volume); 139 | } else { 140 | jsonResponse.put("success", false); 141 | jsonResponse.put("error", "Invalid request method"); 142 | } 143 | break; 144 | case "pause": 145 | if (method.equals(Method.POST)) { 146 | mMediaHelper.pauseMusic(); 147 | jsonResponse.put("success", true); 148 | } else { 149 | jsonResponse.put("success", false); 150 | jsonResponse.put("error", "Invalid request method"); 151 | } 152 | break; 153 | case "resume": 154 | if (method.equals(Method.POST)) { 155 | mMediaHelper.resumeMusic(); 156 | jsonResponse.put("success", true); 157 | } else { 158 | jsonResponse.put("success", false); 159 | jsonResponse.put("error", "Invalid request method"); 160 | } 161 | break; 162 | case "stop": 163 | if (method.equals(Method.POST)) { 164 | mMediaHelper.stopAll(); 165 | jsonResponse.put("success", true); 166 | } else { 167 | jsonResponse.put("success", false); 168 | jsonResponse.put("error", "Invalid request method"); 169 | } 170 | break; 171 | case "volume": 172 | if (method.equals(Method.POST)) { 173 | Map files = new HashMap<>(); 174 | session.parseBody(files); 175 | String postData = files.get("postData"); 176 | JSONObject jsonObject = new JSONObject(postData); 177 | 178 | double volume = jsonObject.getDouble("volume"); 179 | 180 | mMediaHelper.setVolume(volume); 181 | 182 | jsonResponse.put("success", true); 183 | jsonResponse.put("volume", mMediaHelper.getVolume()); 184 | } else if (method.equals(Method.GET)) { 185 | jsonResponse.put("success", true); 186 | jsonResponse.put("volume", mMediaHelper.getVolume()); 187 | } else { 188 | jsonResponse.put("success", false); 189 | jsonResponse.put("error", "Invalid request method"); 190 | } 191 | break; 192 | default: 193 | jsonResponse.put("success", false); 194 | jsonResponse.put("error", "Invalid request URI"); 195 | break; 196 | } 197 | 198 | return newFixedLengthResponse(jsonResponse.getBoolean("success") ? Response.Status.OK : Response.Status.INTERNAL_ERROR, "application/json", jsonResponse.toString()); 199 | } 200 | 201 | private Response handleDeviceRequest(IHTTPSession session) throws JSONException, ResponseException, IOException { 202 | Method method = session.getMethod(); 203 | String uri = session.getUri(); 204 | JSONObject jsonResponse = new JSONObject(); 205 | 206 | switch (uri.replace("/device/", "")) { 207 | case "relay": 208 | if (method.equals(Method.GET)) { 209 | jsonResponse.put("success", true); 210 | jsonResponse.put("state", mDeviceHelper.getRelay()); 211 | } else if (method.equals(Method.POST)) { 212 | Map files = new HashMap<>(); 213 | session.parseBody(files); 214 | String postData = files.get("postData"); 215 | JSONObject jsonObject = new JSONObject(postData); 216 | 217 | mDeviceHelper.setRelay(jsonObject.getBoolean("state")); 218 | jsonResponse.put("success", true); 219 | jsonResponse.put("state", mDeviceHelper.getRelay()); 220 | } else { 221 | jsonResponse.put("success", false); 222 | jsonResponse.put("error", "Invalid request method"); 223 | } 224 | break; 225 | case "getTemperature": 226 | if (method.equals(Method.GET)) { 227 | jsonResponse.put("success", true); 228 | jsonResponse.put("temperature", mDeviceHelper.getTemperature()); 229 | } else { 230 | jsonResponse.put("success", false); 231 | jsonResponse.put("error", "Invalid request method"); 232 | } 233 | break; 234 | case "getHumidity": 235 | if (method.equals(Method.GET)) { 236 | jsonResponse.put("success", true); 237 | jsonResponse.put("humidity", mDeviceHelper.getHumidity()); 238 | } else { 239 | jsonResponse.put("success", false); 240 | jsonResponse.put("error", "Invalid request method"); 241 | } 242 | break; 243 | case "getLux": 244 | if (method.equals(Method.GET)) { 245 | jsonResponse.put("success", true); 246 | jsonResponse.put("lux", mDeviceSensorManager.getLastMeasuredLux()); 247 | } else { 248 | jsonResponse.put("success", false); 249 | jsonResponse.put("error", "Invalid request method"); 250 | } 251 | break; 252 | case "wake": 253 | jsonResponse.put("success", false); 254 | if (method.equals(Method.POST)) { 255 | ScreenSaverManagerHolder.getInstance().stopScreenSaver(); 256 | jsonResponse.put("success", true); 257 | } 258 | break; 259 | case "sleep": 260 | jsonResponse.put("success", false); 261 | if (method.equals(Method.POST)) { 262 | ScreenSaverManagerHolder.getInstance().startScreenSaver(); 263 | jsonResponse.put("success", true); 264 | } 265 | break; 266 | case "reboot": 267 | jsonResponse.put("success", false); 268 | if (method.equals(Method.POST)) { 269 | long deltaTime = System.currentTimeMillis() - ShellyElevateApplication.getApplicationStartTime(); 270 | deltaTime /= 1000; 271 | if (deltaTime > 20) { 272 | try { 273 | Runtime.getRuntime().exec("reboot"); 274 | jsonResponse.put("success", true); 275 | } catch (IOException e) { 276 | Log.e("MQTT", "Error rebooting:", e); 277 | } 278 | } else { 279 | Toast.makeText(mApplicationContext, "Please wait %s seconds before rebooting".replace("%s", String.valueOf(20 - deltaTime)), Toast.LENGTH_LONG).show(); 280 | } 281 | } 282 | break; 283 | default: 284 | jsonResponse.put("success", false); 285 | jsonResponse.put("error", "Invalid request URI"); 286 | break; 287 | } 288 | 289 | return newFixedLengthResponse(jsonResponse.getBoolean("success") ? Response.Status.OK : Response.Status.INTERNAL_ERROR, "application/json", jsonResponse.toString()); 290 | } 291 | 292 | public void onDestroy() { 293 | closeAllConnections(); 294 | stop(); 295 | mMediaHelper.onDestroy(); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.os.Bundle 9 | import android.util.Log 10 | import android.view.MotionEvent 11 | import android.webkit.WebChromeClient 12 | import android.webkit.WebSettings 13 | import android.webkit.WebViewClient 14 | import androidx.activity.ComponentActivity 15 | import androidx.activity.enableEdgeToEdge 16 | import androidx.localbroadcastmanager.content.LocalBroadcastManager 17 | import me.rapierxbox.shellyelevatev2.BuildConfig 18 | import me.rapierxbox.shellyelevatev2.Constants.INTENT_WEBVIEW_INJECT_JAVASCRIPT 19 | import me.rapierxbox.shellyelevatev2.Constants.INTENT_WEBVIEW_REFRESH 20 | import me.rapierxbox.shellyelevatev2.Constants.SHARED_PREFERENCES_NAME 21 | import me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mShellyElevateJavascriptInterface 22 | import me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSwipeHelper 23 | import me.rapierxbox.shellyelevatev2.databinding.MainActivityBinding 24 | import me.rapierxbox.shellyelevatev2.helper.ServiceHelper 25 | import me.rapierxbox.shellyelevatev2.screensavers.ScreenSaverManagerHolder 26 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull 27 | 28 | class MainActivity : ComponentActivity() { 29 | private lateinit var binding: MainActivityBinding // Declare the binding object 30 | 31 | private var clicksButtonRight: Int = 0 32 | private var clicksButtonLeft: Int = 0 33 | 34 | private var webviewRefreshBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { 35 | override fun onReceive(context: Context?, intent: Intent?) { 36 | binding.myWebView.loadUrl(ServiceHelper.getWebviewUrl()) 37 | } 38 | } 39 | 40 | private var webviewJavascriptInjectorBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { 41 | override fun onReceive(context: Context?, intent: Intent?) { 42 | intent?.getStringExtra("javascript")?.let { extra -> 43 | binding.myWebView.evaluateJavascript(extra, null) 44 | } 45 | } 46 | } 47 | 48 | @SuppressLint("ClickableViewAccessibility") 49 | private fun setupSettingsButtons() { 50 | binding.settingButtonOverlayRight.setOnTouchListener { _, event -> 51 | if (event.action == MotionEvent.ACTION_DOWN) { 52 | clicksButtonRight++ 53 | } 54 | return@setOnTouchListener false 55 | } 56 | 57 | binding.settingButtonOverlayLeft.setOnTouchListener { _, event -> 58 | if (event.action == MotionEvent.ACTION_DOWN) { 59 | if (clicksButtonRight == 10) { 60 | clicksButtonLeft++ 61 | } else { 62 | clicksButtonRight = 0 63 | clicksButtonLeft = 0 64 | } 65 | 66 | if (clicksButtonLeft == 10) { 67 | startActivity(Intent(this, SettingsActivity::class.java)) 68 | 69 | clicksButtonRight = 0 70 | clicksButtonLeft = 0 71 | } 72 | } 73 | return@setOnTouchListener false 74 | } 75 | } 76 | 77 | @SuppressLint("SetJavaScriptEnabled") 78 | private fun configureWebView() { 79 | val webSettings: WebSettings = binding.myWebView.settings 80 | 81 | webSettings.javaScriptEnabled = true 82 | webSettings.domStorageEnabled = true 83 | webSettings.databaseEnabled = true 84 | 85 | webSettings.javaScriptCanOpenWindowsAutomatically = true 86 | 87 | webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW 88 | 89 | binding.myWebView.webViewClient = WebViewClient() 90 | binding.myWebView.webChromeClient = WebChromeClient() 91 | 92 | binding.myWebView.addJavascriptInterface(mShellyElevateJavascriptInterface, "ShellyElevate") 93 | 94 | binding.myWebView.loadUrl(ServiceHelper.getWebviewUrl()) 95 | } 96 | 97 | override fun onResume() { 98 | super.onResume() 99 | if (binding.myWebView.originalUrl?.toHttpUrlOrNull() != ServiceHelper.getWebviewUrl().toHttpUrlOrNull()) 100 | binding.myWebView.loadUrl(ServiceHelper.getWebviewUrl()) 101 | } 102 | 103 | @SuppressLint("ClickableViewAccessibility") 104 | override fun onCreate(savedInstanceState: Bundle?) { 105 | super.onCreate(savedInstanceState) 106 | enableEdgeToEdge() 107 | 108 | binding = MainActivityBinding.inflate(layoutInflater) // Inflate the binding 109 | setContentView(binding.root) // Set the content view using binding.root 110 | 111 | configureWebView() 112 | setupSettingsButtons() 113 | 114 | binding.swipeDetectionOverlay.setOnTouchListener { _, event -> 115 | if (ScreenSaverManagerHolder.getInstance().onTouchEvent()) { 116 | Log.d("ShellyElevateV2", "Touch blocked by ScreenSaverManager") 117 | return@setOnTouchListener true 118 | } 119 | mSwipeHelper.onTouchEvent(event) 120 | binding.myWebView.onTouchEvent(event) 121 | 122 | return@setOnTouchListener true 123 | } 124 | 125 | val localBroadcastManager: LocalBroadcastManager = LocalBroadcastManager.getInstance(this) 126 | localBroadcastManager.registerReceiver(webviewRefreshBroadcastReceiver, IntentFilter(INTENT_WEBVIEW_REFRESH)) 127 | localBroadcastManager.registerReceiver(webviewJavascriptInjectorBroadcastReceiver, IntentFilter(INTENT_WEBVIEW_INJECT_JAVASCRIPT)) 128 | 129 | if (!getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE).getBoolean("settingEverShown", false) || BuildConfig.DEBUG) 130 | startActivity(Intent(this, SettingsActivity::class.java)) 131 | } 132 | } -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.provider.Settings 7 | import android.util.Log 8 | import android.view.Menu 9 | import android.view.MenuItem 10 | import android.view.inputmethod.EditorInfo 11 | import android.widget.Toast 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.core.content.edit 14 | import androidx.core.net.toUri 15 | import androidx.core.view.isVisible 16 | import com.google.android.material.slider.Slider 17 | import me.rapierxbox.shellyelevatev2.Constants.SHARED_PREFERENCES_NAME 18 | import me.rapierxbox.shellyelevatev2.Constants.SP_AUTOMATIC_BRIGHTNESS 19 | import me.rapierxbox.shellyelevatev2.Constants.SP_BRIGHTNESS 20 | import me.rapierxbox.shellyelevatev2.Constants.SP_EXTENDED_JAVASCRIPT_INTERFACE 21 | import me.rapierxbox.shellyelevatev2.Constants.SP_HTTP_SERVER_ENABLED 22 | import me.rapierxbox.shellyelevatev2.Constants.SP_LITE_MODE 23 | import me.rapierxbox.shellyelevatev2.Constants.SP_MIN_BRIGHTNESS 24 | import me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_BROKER 25 | import me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_ENABLED 26 | import me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_PASSWORD 27 | import me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_PORT 28 | import me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_USERNAME 29 | import me.rapierxbox.shellyelevatev2.Constants.SP_SCREEN_SAVER_DELAY 30 | import me.rapierxbox.shellyelevatev2.Constants.SP_SCREEN_SAVER_ENABLED 31 | import me.rapierxbox.shellyelevatev2.Constants.SP_SCREEN_SAVER_ID 32 | import me.rapierxbox.shellyelevatev2.Constants.SP_SWITCH_ON_SWIPE 33 | import me.rapierxbox.shellyelevatev2.Constants.SP_WEBVIEW_URL 34 | import me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceHelper 35 | import me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mHttpServer 36 | import me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSwipeHelper 37 | import me.rapierxbox.shellyelevatev2.backbutton.BackAccessibilityService 38 | import me.rapierxbox.shellyelevatev2.backbutton.FloatingBackButtonService 39 | import me.rapierxbox.shellyelevatev2.databinding.SettingsActivityBinding 40 | import me.rapierxbox.shellyelevatev2.helper.ServiceHelper 41 | import me.rapierxbox.shellyelevatev2.screensavers.ScreenSaverManagerHolder 42 | import java.net.NetworkInterface 43 | 44 | @SuppressLint("UseSwitchCompatOrMaterialCode") 45 | class SettingsActivity : AppCompatActivity() { 46 | 47 | private lateinit var binding: SettingsActivityBinding // Declare the binding object 48 | 49 | private fun loadValues() { 50 | 51 | val preferences = getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE) 52 | 53 | binding.webviewURL.setText(ServiceHelper.getWebviewUrl()) 54 | binding.switchOnSwipe.isChecked = preferences.getBoolean(SP_SWITCH_ON_SWIPE, true) 55 | binding.automaticBrightness.isChecked = preferences.getBoolean(SP_AUTOMATIC_BRIGHTNESS, true) 56 | binding.minBrightness.value = preferences.getInt(SP_MIN_BRIGHTNESS, 48).toFloat() 57 | binding.brightnessSetting.value = preferences.getInt(SP_BRIGHTNESS, DEFAULT_BRIGHTNESS).toFloat() 58 | binding.screenSaver.isChecked = preferences.getBoolean(SP_SCREEN_SAVER_ENABLED, true) 59 | binding.screenSaverDelay.setText(preferences.getInt(SP_SCREEN_SAVER_DELAY, SCREEN_SAVER_DEFAULT_DELAY).toString()) 60 | binding.screenSaverType.setSelection(preferences.getInt(SP_SCREEN_SAVER_ID, 0)) 61 | 62 | binding.httpServerEnabled.isChecked = preferences.getBoolean(SP_HTTP_SERVER_ENABLED, true) 63 | binding.httpServerAddress.text = getString(R.string.server_url, getLocalIpAddress()) 64 | 65 | binding.httpServerStatus.text = getString(if (mHttpServer.isAlive) R.string.http_server_running else R.string.http_server_not_running) 66 | binding.extendedJavascriptInterface.isChecked = preferences.getBoolean(SP_EXTENDED_JAVASCRIPT_INTERFACE, false) 67 | binding.liteMode.isChecked = preferences.getBoolean(SP_LITE_MODE, false) 68 | binding.mqttEnabled.isChecked = preferences.getBoolean(SP_MQTT_ENABLED, false) 69 | binding.mqttBroker.setText(preferences.getString(SP_MQTT_BROKER, "")) 70 | binding.mqttPort.setText(preferences.getInt(SP_MQTT_PORT, MQTT_DEFAULT_PORT).toString()) 71 | binding.mqttUsername.setText(preferences.getString(SP_MQTT_USERNAME, "")) 72 | binding.mqttPassword.setText(preferences.getString(SP_MQTT_PASSWORD, "")) 73 | 74 | binding.screenSaverDelayLayout.isVisible = binding.screenSaver.isChecked 75 | binding.screenSaverTypeLayout.isVisible = binding.screenSaver.isChecked 76 | 77 | binding.brightnessSettingLayout.isVisible = !binding.automaticBrightness.isChecked 78 | binding.minBrightnessLayout.isVisible = binding.automaticBrightness.isChecked 79 | 80 | binding.httpServerAddressLayout.isVisible = binding.httpServerEnabled.isChecked 81 | binding.httpServerLayout.isVisible = binding.httpServerEnabled.isChecked 82 | 83 | binding.httpServerButton.isVisible = !mHttpServer.isAlive 84 | 85 | binding.mqttBrokerLayout.isVisible = binding.mqttEnabled.isChecked 86 | binding.mqttPortLayout.isVisible = binding.mqttEnabled.isChecked 87 | binding.mqttUsernameLayout.isVisible = binding.mqttEnabled.isChecked 88 | binding.mqttPasswordLayout.isVisible = binding.mqttEnabled.isChecked 89 | 90 | preferences.edit { 91 | putBoolean("settingEverShown", true) 92 | } 93 | } 94 | 95 | @SuppressLint("ClickableViewAccessibility") 96 | override fun onCreate(savedInstanceState: Bundle?) { 97 | super.onCreate(savedInstanceState) 98 | 99 | binding = SettingsActivityBinding.inflate(layoutInflater) // Inflate the binding 100 | setContentView(binding.root) // Set the content view using binding.root 101 | 102 | setSupportActionBar(binding.toolbar) 103 | 104 | supportActionBar?.let { 105 | it.setHomeButtonEnabled(true) 106 | it.setDisplayHomeAsUpEnabled(true) 107 | title = getString(R.string.settings) 108 | } 109 | 110 | binding.screenSaverType.adapter = ScreenSaverManagerHolder.getInstance().screenSaverSpinnerAdapter 111 | 112 | loadValues() 113 | 114 | binding.findURLButton.setOnClickListener { 115 | ServiceHelper.getHAURL(applicationContext) { url: String -> 116 | runOnUiThread { 117 | binding.webviewURL.setText(url) 118 | } 119 | } 120 | } 121 | 122 | binding.brightnessSetting.addOnChangeListener(object : Slider.OnChangeListener { 123 | 124 | override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { 125 | mDeviceHelper.forceScreenBrightness(value.toInt()) 126 | } 127 | }) 128 | 129 | binding.screenSaver.setOnCheckedChangeListener { _, isChecked -> 130 | binding.screenSaverDelayLayout.isVisible = isChecked 131 | binding.screenSaverTypeLayout.isVisible = isChecked 132 | } 133 | 134 | binding.automaticBrightness.setOnCheckedChangeListener { _, isChecked -> 135 | binding.brightnessSettingLayout.isVisible = !isChecked 136 | binding.minBrightnessLayout.isVisible = isChecked 137 | } 138 | 139 | binding.mqttEnabled.setOnCheckedChangeListener { _, isChecked -> 140 | binding.mqttBrokerLayout.isVisible = isChecked 141 | binding.mqttPortLayout.isVisible = isChecked 142 | binding.mqttUsernameLayout.isVisible = isChecked 143 | binding.mqttPasswordLayout.isVisible = isChecked 144 | } 145 | 146 | binding.httpServerEnabled.setOnCheckedChangeListener { _, isChecked -> 147 | binding.httpServerLayout.isVisible = isChecked 148 | binding.httpServerAddressLayout.isVisible = isChecked 149 | } 150 | 151 | binding.httpServerButton.setOnClickListener { 152 | mHttpServer.start() 153 | binding.httpServerText.text = getString(R.string.http_server_running) 154 | binding.httpServerButton.isVisible = false 155 | } 156 | 157 | binding.swipeDetectionOverlay.setOnTouchListener { _, event -> 158 | if (ScreenSaverManagerHolder.getInstance().onTouchEvent()) { 159 | Log.d("ShellyElevateV2", "Touch blocked by ScreenSaverManager") 160 | return@setOnTouchListener true 161 | } 162 | mSwipeHelper.onTouchEvent(event) 163 | 164 | return@setOnTouchListener false 165 | } 166 | 167 | binding.screenSaverDelay.setOnEditorActionListener { _, actionId, _ -> 168 | if (actionId == EditorInfo.IME_ACTION_DONE) { 169 | 170 | if ((binding.screenSaverDelay.text.toString().toIntOrNull() ?: 5) < 5) { 171 | binding.screenSaverDelay.setText("5") 172 | Toast.makeText(this, R.string.delay_must_be_bigger_then_5s, Toast.LENGTH_SHORT).show() 173 | } 174 | } 175 | 176 | return@setOnEditorActionListener false 177 | } 178 | } 179 | 180 | override fun onSupportNavigateUp(): Boolean { 181 | saveSettings() 182 | return true 183 | } 184 | 185 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 186 | menuInflater.inflate(R.menu.settings_menu, menu) 187 | return true 188 | } 189 | 190 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 191 | return when (item.itemId) { 192 | R.id.action_settings -> { 193 | 194 | if (checkAccessibilityPermission()) { 195 | val intent = Intent(Settings.ACTION_SETTINGS) 196 | startActivity(intent) 197 | } 198 | true 199 | } 200 | 201 | R.id.action_exit -> { 202 | if (checkAccessibilityPermission()) { 203 | moveTaskToBack(true) 204 | finishAffinity() 205 | } 206 | true 207 | } 208 | 209 | else -> super.onOptionsItemSelected(item) 210 | } 211 | } 212 | 213 | private fun checkAccessibilityPermission(): Boolean { 214 | if (!Settings.canDrawOverlays(this)) { 215 | Toast.makeText(this, "Please, grant overlay permission to show the floating back button", Toast.LENGTH_LONG).show() 216 | val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, "package:$packageName".toUri()) 217 | startActivity(intent) 218 | return false 219 | } 220 | 221 | startService(Intent(this, FloatingBackButtonService::class.java)) 222 | 223 | if (!BackAccessibilityService.isAccessibilityEnabled(this)) { 224 | Toast.makeText(this, "Please, grant accessibility permission to use the floating back button", Toast.LENGTH_LONG).show() 225 | startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) 226 | return false 227 | } 228 | 229 | return true 230 | } 231 | 232 | private fun saveSettings() { 233 | getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE).edit { 234 | //Functional mode 235 | putBoolean(SP_LITE_MODE, binding.liteMode.isChecked) 236 | 237 | //WebView 238 | putString(SP_WEBVIEW_URL, binding.webviewURL.text.toString()) 239 | putBoolean(SP_EXTENDED_JAVASCRIPT_INTERFACE, binding.extendedJavascriptInterface.isChecked) 240 | 241 | //MQTT 242 | putBoolean(SP_MQTT_ENABLED, binding.mqttEnabled.isChecked) 243 | putString(SP_MQTT_BROKER, binding.mqttBroker.text.toString()) 244 | putString(SP_MQTT_USERNAME, binding.mqttUsername.text.toString()) 245 | putString(SP_MQTT_PASSWORD, binding.mqttPassword.text.toString()) 246 | putInt(SP_MQTT_PORT, binding.mqttPort.text.toString().toIntOrNull() ?: MQTT_DEFAULT_PORT) 247 | 248 | //Switch 249 | putBoolean(SP_SWITCH_ON_SWIPE, binding.switchOnSwipe.isChecked) 250 | 251 | //Brightness management 252 | putBoolean(SP_AUTOMATIC_BRIGHTNESS, binding.automaticBrightness.isChecked) 253 | putInt(SP_BRIGHTNESS, binding.brightnessSetting.value.toInt()) 254 | putInt(SP_MIN_BRIGHTNESS, binding.minBrightness.value.toInt()) 255 | 256 | //Screen saver 257 | putBoolean(SP_SCREEN_SAVER_ENABLED, binding.screenSaver.isChecked) 258 | putInt(SP_SCREEN_SAVER_DELAY, binding.screenSaverDelay.text.toString().toIntOrNull() ?: SCREEN_SAVER_DEFAULT_DELAY) 259 | putInt(SP_SCREEN_SAVER_ID, binding.screenSaverType.selectedItemPosition) 260 | 261 | //Http Server 262 | putBoolean(SP_HTTP_SERVER_ENABLED, binding.httpServerEnabled.isChecked) 263 | } 264 | 265 | val serverEnabled = binding.httpServerEnabled.isChecked 266 | 267 | if (!serverEnabled && mHttpServer.isAlive) { 268 | mHttpServer.stop() 269 | } else if (serverEnabled && !mHttpServer.isAlive) { 270 | mHttpServer.start() 271 | } 272 | 273 | ShellyElevateApplication.updateSPValues() 274 | Toast.makeText(this, getString(R.string.settings_saved), Toast.LENGTH_SHORT).show() 275 | 276 | finish() 277 | } 278 | 279 | override fun onResume() { 280 | super.onResume() 281 | val intent = Intent(this, FloatingBackButtonService::class.java) 282 | intent.action = FloatingBackButtonService.HIDE_FLOATING_BUTTON 283 | startService(intent) 284 | } 285 | 286 | fun getLocalIpAddress() = NetworkInterface.getNetworkInterfaces().toList().flatMap { it.inetAddresses.toList() }.firstOrNull { it.isSiteLocalAddress }?.hostAddress 287 | 288 | companion object { 289 | const val SCREEN_SAVER_DEFAULT_DELAY = 45 290 | const val MQTT_DEFAULT_PORT = 1833 291 | const val DEFAULT_BRIGHTNESS = 255 292 | } 293 | } -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/SettingsParser.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2; 2 | 3 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSharedPreferences; 4 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.updateSPValues; 5 | 6 | import android.content.SharedPreferences; 7 | 8 | import org.json.JSONException; 9 | import org.json.JSONObject; 10 | 11 | import java.util.Iterator; 12 | import java.util.Map; 13 | 14 | public class SettingsParser { 15 | public JSONObject getSettings() throws JSONException { 16 | JSONObject settings = new JSONObject(); 17 | Map allPreferences = mSharedPreferences.getAll(); 18 | for (String key : allPreferences.keySet()) { 19 | settings.put(key, allPreferences.get(key)); 20 | } 21 | return settings; 22 | } 23 | 24 | public void setSettings(JSONObject settings) throws JSONException { 25 | SharedPreferences.Editor editor = mSharedPreferences.edit(); 26 | for (Iterator it = settings.keys(); it.hasNext(); ) { 27 | String key = it.next(); 28 | Class type = settings.get(key).getClass(); 29 | if (type.equals(String.class)) { 30 | editor.putString(key, settings.getString(key)); 31 | } else if (type.equals(Integer.class)) { 32 | editor.putInt(key, settings.getInt(key)); 33 | } else if (type.equals(Long.class)) { 34 | editor.putLong(key, settings.getLong(key)); 35 | } else if (type.equals(Boolean.class)) { 36 | editor.putBoolean(key, settings.getBoolean(key)); 37 | } 38 | } 39 | editor.apply(); 40 | updateSPValues(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/ShellyElevateApplication.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2; 2 | 3 | import static me.rapierxbox.shellyelevatev2.Constants.INTENT_WEBVIEW_REFRESH; 4 | import static me.rapierxbox.shellyelevatev2.Constants.SHARED_PREFERENCES_NAME; 5 | import static me.rapierxbox.shellyelevatev2.Constants.SP_HTTP_SERVER_ENABLED; 6 | 7 | import android.app.Application; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.SharedPreferences; 11 | import android.hardware.Sensor; 12 | import android.hardware.SensorManager; 13 | import android.util.Log; 14 | 15 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; 16 | 17 | import java.io.IOException; 18 | 19 | import me.rapierxbox.shellyelevatev2.helper.DeviceHelper; 20 | import me.rapierxbox.shellyelevatev2.helper.DeviceSensorManager; 21 | import me.rapierxbox.shellyelevatev2.helper.SwipeHelper; 22 | import me.rapierxbox.shellyelevatev2.mqtt.MQTTServer; 23 | import me.rapierxbox.shellyelevatev2.screensavers.ScreenSaverManagerHolder; 24 | 25 | public class ShellyElevateApplication extends Application { 26 | public static HttpServer mHttpServer; 27 | 28 | public static DeviceHelper mDeviceHelper; 29 | public static DeviceSensorManager mDeviceSensorManager; 30 | public static SwipeHelper mSwipeHelper; 31 | public static ShellyElevateJavascriptInterface mShellyElevateJavascriptInterface; 32 | public static MQTTServer mMQTTServer; 33 | 34 | public static Context mApplicationContext; 35 | public static SharedPreferences mSharedPreferences; 36 | 37 | private static long applicationStartTime; 38 | 39 | @Override 40 | public void onCreate() { 41 | super.onCreate(); 42 | 43 | applicationStartTime = System.currentTimeMillis(); 44 | 45 | mApplicationContext = getApplicationContext(); 46 | mSharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE); 47 | 48 | mHttpServer = new HttpServer(); 49 | 50 | mDeviceHelper = new DeviceHelper(); 51 | ScreenSaverManagerHolder.initialize(); 52 | 53 | mSwipeHelper = new SwipeHelper(); 54 | mShellyElevateJavascriptInterface = new ShellyElevateJavascriptInterface(); 55 | mMQTTServer = new MQTTServer(); 56 | 57 | if (mSharedPreferences.getBoolean(SP_HTTP_SERVER_ENABLED, true)) { 58 | try { 59 | mHttpServer.start(); 60 | } catch (IOException e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | 65 | // Sensors Init 66 | mDeviceSensorManager = new DeviceSensorManager(); 67 | SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); 68 | Sensor lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); 69 | sensorManager.registerListener(mDeviceSensorManager, lightSensor, SensorManager.SENSOR_DELAY_NORMAL); 70 | 71 | //When everything is running, update values 72 | updateSPValues(); 73 | 74 | Log.i("ShellyElevateV2", "Application started"); 75 | } 76 | 77 | public static long getApplicationStartTime() { 78 | return applicationStartTime; 79 | } 80 | 81 | public static void updateSPValues() { 82 | ScreenSaverManagerHolder.getInstance().updateValues(); 83 | mDeviceSensorManager.updateValues(); 84 | mSwipeHelper.updateValues(); 85 | mShellyElevateJavascriptInterface.updateValues(); 86 | mDeviceHelper.updateValues(); 87 | mMQTTServer.updateValues(); 88 | 89 | Intent intent = new Intent(INTENT_WEBVIEW_REFRESH); 90 | LocalBroadcastManager.getInstance(ShellyElevateApplication.mApplicationContext).sendBroadcast(intent); 91 | } 92 | 93 | @Override 94 | public void onTerminate() { 95 | mHttpServer.onDestroy(); 96 | ((SensorManager) getSystemService(Context.SENSOR_SERVICE)).unregisterListener(mDeviceSensorManager); 97 | ScreenSaverManagerHolder.getInstance().onDestroy(); 98 | mMQTTServer.onDestroy(); 99 | 100 | mDeviceHelper.setScreenOn(true); 101 | 102 | Log.i("ShellyElevateV2", "BYEEEEEEEEEEEEEEEEEEEE :)"); 103 | 104 | super.onTerminate(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/ShellyElevateJavascriptInterface.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2; 2 | 3 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceHelper; 4 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceSensorManager; 5 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSharedPreferences; 6 | import static me.rapierxbox.shellyelevatev2.Constants.*; 7 | 8 | import android.webkit.JavascriptInterface; 9 | 10 | import me.rapierxbox.shellyelevatev2.screensavers.ScreenSaverManagerHolder; 11 | 12 | 13 | public class ShellyElevateJavascriptInterface { 14 | private boolean eJSa = false; //Extended Javascript allowed 15 | 16 | ShellyElevateJavascriptInterface() { 17 | updateValues(); 18 | } 19 | 20 | public void updateValues() { 21 | eJSa = mSharedPreferences.getBoolean(SP_EXTENDED_JAVASCRIPT_INTERFACE, false); 22 | } 23 | 24 | @JavascriptInterface 25 | public boolean getRelay() {return mDeviceHelper.getRelay();} 26 | @JavascriptInterface 27 | public int getLux() {return Math.round(mDeviceSensorManager.getLastMeasuredLux());} 28 | @JavascriptInterface 29 | public double getTemperature() {return mDeviceHelper.getTemperature();} 30 | @JavascriptInterface 31 | public double getHumidity() {return mDeviceHelper.getHumidity();} 32 | @JavascriptInterface 33 | public int getScreenBrightness() {return mDeviceHelper.getScreenBrightness();} 34 | @JavascriptInterface 35 | public boolean getScreenSaverRunning() {return ScreenSaverManagerHolder.getInstance().isScreenSaverRunning();} 36 | @JavascriptInterface 37 | public boolean getScreenSaverEnabled() {return ScreenSaverManagerHolder.getInstance().isScreenSaverEnabled();} 38 | @JavascriptInterface 39 | public int getScreenSaverId() {return ScreenSaverManagerHolder.getInstance().getCurrentScreenSaverId();} 40 | @JavascriptInterface 41 | public boolean getExtendedJavascriptInterfaceEnabled() {return eJSa;} 42 | 43 | @JavascriptInterface 44 | public void setRelay(boolean state) {if (eJSa) {mDeviceHelper.setRelay(state);}} 45 | @JavascriptInterface 46 | public void sleep() {if (eJSa) {ScreenSaverManagerHolder.getInstance().startScreenSaver();}} 47 | public void wake() {if (eJSa) {ScreenSaverManagerHolder.getInstance().stopScreenSaver();}} 48 | @JavascriptInterface 49 | public void setScreenBrightness(int brightness) {if (eJSa) {mDeviceHelper.setScreenBrightness(brightness);}} 50 | public void setScreenSaverEnabled(boolean enabled) { 51 | if (eJSa) { 52 | mSharedPreferences.edit().putBoolean(SP_SCREEN_SAVER_ENABLED, enabled).apply(); 53 | ScreenSaverManagerHolder.getInstance().updateValues(); 54 | } 55 | } 56 | @JavascriptInterface 57 | public void setScreenSaverId(int id) { 58 | if (eJSa) { 59 | mSharedPreferences.edit().putInt(SP_SCREEN_SAVER_ID, id).apply(); 60 | ScreenSaverManagerHolder.getInstance().updateValues(); 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/backbutton/BackAccessibilityService.kt: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.backbutton 2 | 3 | import android.accessibilityservice.AccessibilityService 4 | import android.annotation.SuppressLint 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.IntentFilter 9 | import android.view.accessibility.AccessibilityEvent 10 | import android.view.accessibility.AccessibilityEvent.* 11 | 12 | class BackAccessibilityService : AccessibilityService() { 13 | private val backReceiver = object : BroadcastReceiver() { 14 | override fun onReceive(context: Context?, intent: Intent?) { 15 | if (intent?.action == ACTION_BACK) { 16 | performGlobalAction(GLOBAL_ACTION_BACK) 17 | } 18 | } 19 | } 20 | 21 | override fun onServiceConnected() { 22 | super.onServiceConnected() 23 | val filter = IntentFilter(ACTION_BACK) 24 | registerReceiver(backReceiver, filter) 25 | } 26 | 27 | @SuppressLint("SwitchIntDef") 28 | override fun onAccessibilityEvent(event: AccessibilityEvent?) { 29 | when (event?.eventType) { 30 | TYPE_TOUCH_INTERACTION_START, 31 | TYPE_TOUCH_INTERACTION_END, 32 | TYPE_VIEW_CLICKED, 33 | TYPE_VIEW_SCROLLED, 34 | TYPE_VIEW_FOCUSED, 35 | TYPE_GESTURE_DETECTION_START, 36 | TYPE_GESTURE_DETECTION_END -> { 37 | val intent = Intent(ACTION_USER_INTERACTION) 38 | sendBroadcast(intent) 39 | } 40 | } 41 | } 42 | 43 | override fun onInterrupt() {} 44 | 45 | override fun onDestroy() { 46 | unregisterReceiver(backReceiver) 47 | super.onDestroy() 48 | } 49 | 50 | companion object { 51 | fun isAccessibilityEnabled(context: Context): Boolean { 52 | val enabledServices = android.provider.Settings.Secure.getString( 53 | context.contentResolver, 54 | android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES 55 | ) ?: return false 56 | return enabledServices.contains(context.packageName) 57 | } 58 | 59 | const val ACTION_USER_INTERACTION = "shellyelevate.ACTION_USER_INTERACTION" 60 | const val ACTION_BACK = "shellyelevate.ACTION_USER_BACK" 61 | } 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/backbutton/FloatingBackButtonService.kt: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.backbutton 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Service 5 | import android.content.Intent 6 | import android.content.SharedPreferences 7 | import android.graphics.PixelFormat 8 | import android.os.Build 9 | import android.os.IBinder 10 | import android.util.Log 11 | import android.view.Gravity 12 | import android.view.LayoutInflater 13 | import android.view.MotionEvent 14 | import android.view.View 15 | import android.view.WindowManager 16 | import android.widget.ImageView 17 | import android.widget.Toast 18 | import androidx.core.content.edit 19 | import me.rapierxbox.shellyelevatev2.R 20 | 21 | class FloatingBackButtonService : Service() { 22 | 23 | private lateinit var windowManager: WindowManager 24 | private var floatingView: View? = null 25 | 26 | private lateinit var prefs: SharedPreferences 27 | 28 | private var wasVisibleBeforePause = false 29 | 30 | fun pauseFloatingButton() { 31 | wasVisibleBeforePause = (floatingView != null) 32 | hideFloatingButton() 33 | } 34 | 35 | fun resumeFloatingButton() { 36 | if (wasVisibleBeforePause) showFloatingButton() 37 | } 38 | 39 | override fun onCreate() { 40 | super.onCreate() 41 | prefs = getSharedPreferences(FLOATING_BUTTON_PREFS, MODE_PRIVATE) 42 | showFloatingButton() 43 | } 44 | 45 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 46 | Log.d("FloatingBackButtonService", "onStartCommand() called with: intent = $intent, flags = $flags, startId = $startId, action = ${intent?.action}") 47 | 48 | when (intent?.action) { 49 | SHOW_FLOATING_BUTTON -> showFloatingButton() 50 | HIDE_FLOATING_BUTTON -> hideFloatingButton() 51 | PAUSE_BUTTON -> pauseFloatingButton() 52 | RESUME_BUTTON -> resumeFloatingButton() 53 | else -> showFloatingButton() 54 | } 55 | return START_STICKY 56 | } 57 | 58 | @SuppressLint("ClickableViewAccessibility") 59 | fun showFloatingButton() { 60 | //This overrides pause status 61 | wasVisibleBeforePause = true 62 | 63 | if (floatingView != null) return 64 | 65 | floatingView = LayoutInflater.from(this).inflate(R.layout.floating_button_layout, null) 66 | 67 | val layoutParams = WindowManager.LayoutParams( 68 | WindowManager.LayoutParams.WRAP_CONTENT, 69 | WindowManager.LayoutParams.WRAP_CONTENT, 70 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 71 | WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 72 | else 73 | WindowManager.LayoutParams.TYPE_PHONE, 74 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 75 | PixelFormat.TRANSLUCENT 76 | ) 77 | 78 | layoutParams.gravity = Gravity.TOP or Gravity.START 79 | layoutParams.x = prefs.getInt(POS_X, 0) 80 | layoutParams.y = prefs.getInt(POS_Y, 300) 81 | 82 | val button = floatingView!!.findViewById(R.id.floating_back_button) 83 | 84 | windowManager = getSystemService(WINDOW_SERVICE) as WindowManager 85 | windowManager.addView(floatingView, layoutParams) 86 | 87 | var initialX = 0 88 | var initialY = 0 89 | var initialTouchX = 0f 90 | var initialTouchY = 0f 91 | var isClick = false 92 | 93 | button.setOnTouchListener { _, event -> 94 | when (event.action) { 95 | MotionEvent.ACTION_DOWN -> { 96 | initialX = layoutParams.x 97 | initialY = layoutParams.y 98 | initialTouchX = event.rawX 99 | initialTouchY = event.rawY 100 | isClick = true 101 | true 102 | } 103 | 104 | MotionEvent.ACTION_MOVE -> { 105 | val dx = (event.rawX - initialTouchX).toInt() 106 | val dy = (event.rawY - initialTouchY).toInt() 107 | layoutParams.x = initialX + dx 108 | layoutParams.y = initialY + dy 109 | windowManager.updateViewLayout(floatingView, layoutParams) 110 | if (dx != 0 || dy != 0) isClick = false 111 | true 112 | } 113 | 114 | MotionEvent.ACTION_UP -> { 115 | if (isClick) { 116 | performClick() 117 | } else { 118 | prefs.edit { 119 | putInt(POS_X, layoutParams.x) 120 | putInt(POS_Y, layoutParams.y) 121 | } 122 | } 123 | true 124 | } 125 | 126 | else -> false 127 | } 128 | } 129 | } 130 | 131 | private fun performClick() { 132 | if (BackAccessibilityService.isAccessibilityEnabled(this)) { 133 | sendBroadcast(Intent(BackAccessibilityService.ACTION_BACK)) 134 | } else { 135 | Toast.makeText(this, getString(R.string.accessibility_service_not_enabled), Toast.LENGTH_SHORT).show() 136 | } 137 | } 138 | 139 | fun hideFloatingButton() { 140 | //This overrides pause status 141 | wasVisibleBeforePause = false 142 | 143 | if (floatingView != null) { 144 | windowManager.removeView(floatingView) 145 | floatingView = null 146 | } 147 | } 148 | 149 | override fun onDestroy() { 150 | hideFloatingButton() 151 | super.onDestroy() 152 | } 153 | 154 | 155 | override fun onBind(intent: Intent?): IBinder? = null 156 | 157 | companion object { 158 | const val SHOW_FLOATING_BUTTON = "SHOW_FLOATING_BUTTON" 159 | const val HIDE_FLOATING_BUTTON = "HIDE_FLOATING_BUTTON" 160 | const val PAUSE_BUTTON = "PAUSE_BUTTON" 161 | const val RESUME_BUTTON = "RESUME_BUTTON" 162 | 163 | const val POS_X = "pos_x" 164 | const val POS_Y = "pos_y" 165 | 166 | const val FLOATING_BUTTON_PREFS = "floating_button_prefs" 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/helper/DeviceHelper.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.helper; 2 | 3 | import static me.rapierxbox.shellyelevatev2.Constants.SP_AUTOMATIC_BRIGHTNESS; 4 | import static me.rapierxbox.shellyelevatev2.Constants.SP_BRIGHTNESS; 5 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceSensorManager; 6 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mMQTTServer; 7 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSharedPreferences; 8 | 9 | import android.util.Log; 10 | 11 | import java.io.BufferedReader; 12 | import java.io.File; 13 | import java.io.FileReader; 14 | import java.io.FileWriter; 15 | import java.io.IOException; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Objects; 19 | 20 | public class DeviceHelper { 21 | private static final String[] possibleRelayFiles = { 22 | "/sys/devices/platform/leds/green_enable", 23 | "/sys/devices/platform/leds/red_enable", 24 | "/sys/class/strelay/relay1", 25 | "/sys/class/strelay/relay2" 26 | }; 27 | private static final String tempAndHumFile = "/sys/devices/platform/sht3x-user/sht3x_access"; 28 | private static final String[] screenBrightnessFiles = { 29 | "/sys/devices/platform/leds-mt65xx/leds/lcd-backlight/brightness", 30 | "/sys/devices/platform/sprd_backlight/backlight/sprd_backlight/brightness", 31 | "/sys/devices/platform/backlight/backlight/backlight/brightness" 32 | }; 33 | private String screenBrightnessFile; 34 | private final String[] relayFiles; 35 | private boolean screenOn = true; 36 | private int screenBrightness; 37 | private boolean automaticBrightness; 38 | 39 | private final String TAG = "DeviceHelper"; 40 | 41 | public DeviceHelper() { 42 | for (String brightnessFile : screenBrightnessFiles) { 43 | if (new File(brightnessFile).exists()){ 44 | screenBrightnessFile = brightnessFile; 45 | } 46 | } 47 | if (screenBrightnessFile == null) { 48 | Log.e("FATAL ERROR", "no brightness file found"); 49 | screenBrightnessFile = ""; 50 | } 51 | 52 | List relayFileList = new ArrayList<>(); 53 | for (String relayFile : possibleRelayFiles) { 54 | if (new File(relayFile).exists()){ 55 | relayFileList.add(relayFile); 56 | } 57 | } 58 | if (relayFileList.isEmpty()) { 59 | Log.e("FATAL ERROR", "no relay files found"); 60 | relayFileList.add(""); 61 | } 62 | relayFiles = relayFileList.toArray(new String[0]); 63 | 64 | updateValues(); 65 | } 66 | 67 | public void setScreenOn(boolean on) { 68 | screenOn = on; 69 | int brightness = automaticBrightness ? DeviceSensorManager.getScreenBrightnessFromLux(mDeviceSensorManager.getLastMeasuredLux()) : screenBrightness; 70 | forceScreenBrightness(on ? brightness : 0); 71 | } 72 | 73 | public boolean getScreenOn() { 74 | return screenOn; 75 | } 76 | 77 | public void setScreenBrightness(int brightness) { 78 | if (!screenOn) 79 | return; 80 | 81 | forceScreenBrightness(brightness); 82 | 83 | if (!automaticBrightness) { 84 | mSharedPreferences.edit().putInt(SP_BRIGHTNESS, brightness).apply(); 85 | screenBrightness = brightness; 86 | } 87 | } 88 | 89 | public void forceScreenBrightness(int brightness) { 90 | brightness = Math.max(0, Math.min(brightness, 255)); 91 | 92 | Log.d(TAG, "Set brightness to: " + brightness); 93 | 94 | writeFileContent(screenBrightnessFile, String.valueOf(brightness)); 95 | } 96 | public int getScreenBrightness() { 97 | return Integer.parseInt(readFileContent(screenBrightnessFile)); 98 | } 99 | public boolean getRelay() { 100 | boolean relayState = false; 101 | for (String relayFile : relayFiles) { 102 | relayState |= readFileContent(relayFile).contains("1"); 103 | } 104 | return relayState; 105 | } 106 | public void setRelay(boolean state) { 107 | for (String relayFile : relayFiles) { 108 | writeFileContent(relayFile, state ? "1" : "0"); 109 | } 110 | if (mMQTTServer.shouldSend()) { 111 | mMQTTServer.publishRelay(state); 112 | } 113 | } 114 | public double getTemperature() { 115 | String[] tempSplit = readFileContent(tempAndHumFile).split(":"); 116 | double temp = (((Double.parseDouble(tempSplit[1]) * 175.0) / 65535.0) - 45.0) - 1.1; 117 | return Math.round(temp * 10.0) / 10.0; 118 | } 119 | public double getHumidity() { 120 | String[] humiditySplit = readFileContent(tempAndHumFile).split(":"); 121 | double humidity = ((Double.parseDouble(humiditySplit[0]) * 100.0) / 65535.0) + 18.0; 122 | return Math.round(humidity); 123 | } 124 | 125 | public void updateValues() { 126 | screenBrightness = mSharedPreferences.getInt(SP_BRIGHTNESS, 255); 127 | automaticBrightness = mSharedPreferences.getBoolean(SP_AUTOMATIC_BRIGHTNESS, true); 128 | } 129 | 130 | private static String readFileContent(String filePath) { 131 | StringBuilder content = new StringBuilder(); 132 | try (BufferedReader br = new BufferedReader(new FileReader(filePath))) { 133 | String line; 134 | while ((line = br.readLine()) != null) { 135 | content.append(line).append("\n"); 136 | } 137 | } catch (IOException e) { 138 | Log.e("DeviceHelper", "Error when reading file with path:" + filePath + ":" + Objects.requireNonNull(e.getMessage())); 139 | } 140 | return content.toString(); 141 | } 142 | 143 | private static void writeFileContent(String filePath, String content) { 144 | try (FileWriter writer = new FileWriter(filePath)) { 145 | writer.write(content); 146 | } catch (IOException e) { 147 | Log.e("DeviceHelper", "Error when writing file with path:" + filePath + ":" + Objects.requireNonNull(e.getMessage())); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/helper/DeviceSensorManager.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.helper; 2 | 3 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mMQTTServer; 4 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSharedPreferences; 5 | import static me.rapierxbox.shellyelevatev2.Constants.*; 6 | 7 | import android.util.Log; 8 | 9 | import android.hardware.Sensor; 10 | import android.hardware.SensorEvent; 11 | import android.hardware.SensorEventListener; 12 | 13 | import me.rapierxbox.shellyelevatev2.ShellyElevateApplication; 14 | 15 | 16 | public class DeviceSensorManager implements SensorEventListener { 17 | private static final String TAG = "DeviceSensorManager" ; 18 | private float lastMeasuredLux = 0.0f; 19 | private boolean automaticBrightness = true; 20 | 21 | public float getLastMeasuredLux() { 22 | return lastMeasuredLux; 23 | } 24 | 25 | public void updateValues() { 26 | automaticBrightness = mSharedPreferences.getBoolean(SP_AUTOMATIC_BRIGHTNESS, true); 27 | } 28 | 29 | @Override 30 | public void onSensorChanged(SensorEvent event) { 31 | if (event.sensor.getType() == Sensor.TYPE_LIGHT) { 32 | lastMeasuredLux = event.values[0]; 33 | Log.d(TAG, "Light sensor value: " + lastMeasuredLux); 34 | 35 | if (automaticBrightness) { 36 | ShellyElevateApplication.mDeviceHelper.setScreenBrightness(getScreenBrightnessFromLux(lastMeasuredLux)); 37 | } 38 | if (mMQTTServer.shouldSend()) { 39 | mMQTTServer.publishLux(lastMeasuredLux); 40 | } 41 | } 42 | } 43 | 44 | @Override 45 | public void onAccuracyChanged(Sensor sensor, int accuracy) { 46 | // Ignore 47 | } 48 | 49 | public static int getScreenBrightnessFromLux(float lux) { 50 | int minBrightness = mSharedPreferences.getInt(SP_MIN_BRIGHTNESS, 48); 51 | if (lux >= 500) return 255; 52 | if (lux <= 30) return minBrightness; 53 | 54 | double slope = (255.0 - minBrightness) / (500.0 - 30.0); 55 | return (int) (minBrightness + slope * (lux - 30)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/helper/MediaHelper.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.helper; 2 | 3 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mApplicationContext; 4 | 5 | import android.content.Context; 6 | import android.media.AudioManager; 7 | import android.media.MediaPlayer; 8 | import android.net.Uri; 9 | 10 | import java.io.IOException; 11 | 12 | public class MediaHelper { 13 | private final MediaPlayer mediaPlayerEffects; 14 | private final MediaPlayer mediaPlayerMusic; 15 | private final AudioManager audioManager; 16 | 17 | public MediaHelper() { 18 | mediaPlayerEffects = new MediaPlayer(); 19 | mediaPlayerMusic = new MediaPlayer(); 20 | audioManager = (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE); 21 | 22 | mediaPlayerEffects.setAudioStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE); 23 | mediaPlayerMusic.setAudioStreamType(AudioManager.STREAM_MUSIC); 24 | 25 | mediaPlayerEffects.setLooping(false); 26 | mediaPlayerMusic.setLooping(true); 27 | 28 | mediaPlayerEffects.setOnPreparedListener(mp -> { 29 | mp.start(); 30 | pauseMusic(); 31 | }); 32 | mediaPlayerMusic.setOnPreparedListener(MediaPlayer::start); 33 | 34 | mediaPlayerEffects.setOnCompletionListener(mp -> resumeMusic()); 35 | } 36 | 37 | public void playMusic(Uri uri) throws IOException { 38 | mediaPlayerMusic.reset(); 39 | mediaPlayerMusic.setDataSource(mApplicationContext, uri); 40 | mediaPlayerMusic.prepareAsync(); 41 | } 42 | public void playEffect(Uri uri) throws IOException { 43 | mediaPlayerEffects.reset(); 44 | mediaPlayerEffects.setDataSource(mApplicationContext, uri); 45 | mediaPlayerEffects.prepareAsync(); 46 | } 47 | public void pauseMusic() { 48 | mediaPlayerMusic.pause(); 49 | } 50 | public void resumeMusic() { 51 | mediaPlayerMusic.start(); 52 | } 53 | 54 | public void stopAll() { 55 | mediaPlayerEffects.stop(); 56 | mediaPlayerMusic.stop(); 57 | } 58 | public void setVolume(double volume) { 59 | audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (int) (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * volume), 0); 60 | } 61 | 62 | public double getVolume() { 63 | return (double) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) / (double) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 64 | } 65 | 66 | public void onDestroy() { 67 | mediaPlayerEffects.release(); 68 | mediaPlayerMusic.release(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/helper/ServiceHelper.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.helper; 2 | 3 | import static me.rapierxbox.shellyelevatev2.Constants.SP_DEPRECATED_HA_IP; 4 | import static me.rapierxbox.shellyelevatev2.Constants.SP_WEBVIEW_URL; 5 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSharedPreferences; 6 | 7 | import android.content.Context; 8 | import android.content.SharedPreferences; 9 | import android.net.nsd.NsdManager; 10 | import android.net.nsd.NsdServiceInfo; 11 | import android.util.Log; 12 | 13 | import java.util.function.Consumer; 14 | 15 | public class ServiceHelper { 16 | public static void getHAURL(Context context, Consumer action) { 17 | NsdManager nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); 18 | NsdManager.DiscoveryListener discoveryListener = new NsdManager.DiscoveryListener() { 19 | @Override 20 | public void onStartDiscoveryFailed(String serviceType, int errorCode) { 21 | Log.e("discovery", "Discovery failed: Error code: " + errorCode); 22 | } 23 | 24 | @Override 25 | public void onStopDiscoveryFailed(String serviceType, int errorCode) { 26 | Log.e("discovery", "Discovery failed to stop: Error code: " + errorCode); 27 | } 28 | 29 | @Override 30 | public void onDiscoveryStarted(String serviceType) { 31 | Log.i("discovery", "Service discovery started"); 32 | } 33 | 34 | @Override 35 | public void onDiscoveryStopped(String serviceType) { 36 | Log.i("discovery", "Service discovery stopped"); 37 | } 38 | 39 | @Override 40 | public void onServiceFound(NsdServiceInfo serviceInfo) { 41 | if (serviceInfo.getServiceType().equals("_home-assistant._tcp.")) { 42 | Log.i("discovery", "Found Home Assistant service: " + serviceInfo.getServiceName()); 43 | 44 | NsdManager.ResolveListener resolveListener = new NsdManager.ResolveListener() { 45 | @Override 46 | public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { 47 | Log.e("discovery", "Resolve failed: Error code: " + errorCode); 48 | } 49 | 50 | @Override 51 | public void onServiceResolved(NsdServiceInfo serviceInfo) { 52 | Log.i("discovery", "Service resolved: " + serviceInfo); 53 | String url = "http://" + serviceInfo.getHost().getHostAddress() + ":" + serviceInfo.getPort(); 54 | Log.i("discovery", "Home Assistant URL: " + url); 55 | action.accept(url); 56 | } 57 | }; 58 | 59 | nsdManager.resolveService(serviceInfo, resolveListener); 60 | 61 | nsdManager.stopServiceDiscovery(this); 62 | } 63 | } 64 | 65 | @Override 66 | public void onServiceLost(NsdServiceInfo serviceInfo) { 67 | Log.i("discovery", "Service lost: " + serviceInfo); 68 | } 69 | }; 70 | 71 | nsdManager.discoverServices("_home-assistant._tcp.", NsdManager.PROTOCOL_DNS_SD, discoveryListener); 72 | } 73 | 74 | public static String getWebviewUrl() { 75 | if (mSharedPreferences.contains(SP_DEPRECATED_HA_IP)) { 76 | mSharedPreferences.edit() 77 | .putString(SP_WEBVIEW_URL, "http://" + mSharedPreferences.getString(SP_DEPRECATED_HA_IP, "") + ":8123") 78 | .remove(SP_DEPRECATED_HA_IP) 79 | .apply(); 80 | } 81 | return mSharedPreferences.getString(SP_WEBVIEW_URL, ""); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/helper/SwipeHelper.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.helper; 2 | 3 | import static me.rapierxbox.shellyelevatev2.Constants.SP_SWITCH_ON_SWIPE; 4 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceHelper; 5 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mMQTTServer; 6 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSharedPreferences; 7 | 8 | import android.util.Log; 9 | import android.view.MotionEvent; 10 | 11 | public class SwipeHelper{ 12 | private float touchStartY = 0; 13 | private long touchStartEventTime = 0; 14 | 15 | public float minVel = 2.5F; 16 | public float minDist = 250.0F; 17 | 18 | private boolean doSwitchOnSwipe = true; 19 | public boolean onTouchEvent(MotionEvent event){ 20 | switch (event.getAction()){ 21 | case MotionEvent.ACTION_DOWN: 22 | touchStartY = event.getY(); 23 | touchStartEventTime = event.getEventTime(); 24 | return true; 25 | case MotionEvent.ACTION_UP: 26 | float deltaY = Math.abs(touchStartY - event.getY()); 27 | float deltaT = Math.abs(touchStartEventTime - event.getEventTime()); 28 | float velocity = deltaY / deltaT; 29 | if (velocity > minVel && deltaY > minDist){ 30 | if (doSwitchOnSwipe) { 31 | mDeviceHelper.setRelay(!mDeviceHelper.getRelay()); 32 | } 33 | if (mMQTTServer.shouldSend()) { 34 | mMQTTServer.publishSwipeEvent(); 35 | } 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | 42 | public void updateValues() { 43 | doSwitchOnSwipe = mSharedPreferences.getBoolean(SP_SWITCH_ON_SWIPE, true); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/mqtt/MQTTServer.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.mqtt; 2 | 3 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_CONFIG_DEVICE; 4 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_HOME_ASSISTANT_STATUS; 5 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_HUM_SENSOR; 6 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_LUX_SENSOR; 7 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_REBOOT_BUTTON; 8 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_REFRESH_WEBVIEW_BUTTON; 9 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_RELAY_COMMAND; 10 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_RELAY_STATE; 11 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_SLEEPING_BINARY_SENSOR; 12 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_SLEEP_BUTTON; 13 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_STATUS; 14 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_SWIPE_EVENT; 15 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_TEMP_SENSOR; 16 | import static me.rapierxbox.shellyelevatev2.Constants.MQTT_TOPIC_WAKE_BUTTON; 17 | import static me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_BROKER; 18 | import static me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_DEVICE_ID; 19 | import static me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_ENABLED; 20 | import static me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_PASSWORD; 21 | import static me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_PORT; 22 | import static me.rapierxbox.shellyelevatev2.Constants.SP_MQTT_USERNAME; 23 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceHelper; 24 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceSensorManager; 25 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSharedPreferences; 26 | 27 | import android.util.Log; 28 | 29 | import org.eclipse.paho.mqttv5.client.MqttClient; 30 | import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; 31 | import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence; 32 | import org.eclipse.paho.mqttv5.common.MqttException; 33 | import org.json.JSONArray; 34 | import org.json.JSONException; 35 | import org.json.JSONObject; 36 | 37 | import java.util.UUID; 38 | import java.util.concurrent.Executors; 39 | import java.util.concurrent.ScheduledExecutorService; 40 | import java.util.concurrent.TimeUnit; 41 | 42 | import me.rapierxbox.shellyelevatev2.screensavers.ScreenSaverManagerHolder; 43 | 44 | public class MQTTServer { 45 | private MqttClient mMqttClient; 46 | private final MemoryPersistence mMemoryPersistence; 47 | private final ShellyElevateMQTTCallback mShellyElevateMQTTCallback; 48 | private final MqttConnectionOptions mMqttConnectionsOptions; 49 | private final ScheduledExecutorService scheduler; 50 | 51 | private boolean enabled; 52 | private boolean connected; 53 | private byte[] password; 54 | private String username; 55 | private String broker; 56 | private String clientId; 57 | private int port; 58 | 59 | private boolean validForConnection; 60 | 61 | public MQTTServer() { 62 | mMemoryPersistence = new MemoryPersistence(); 63 | mShellyElevateMQTTCallback = new ShellyElevateMQTTCallback(); 64 | mMqttConnectionsOptions = new MqttConnectionOptions(); 65 | 66 | scheduler = Executors.newScheduledThreadPool(1); 67 | scheduler.scheduleWithFixedDelay(this::publishTempAndHum, 0, 5, TimeUnit.SECONDS); 68 | 69 | connected = false; 70 | 71 | clientId = mSharedPreferences.getString(SP_MQTT_DEVICE_ID, "shellywalldisplay"); 72 | if (clientId.equals("shellyelevate") || clientId.equals("shellywalldisplay") || clientId.length() <= 2) { 73 | clientId = "shellyelevate-" + UUID.randomUUID().toString().replaceAll("-", "").substring(2, 6); 74 | mSharedPreferences.edit().putString(SP_MQTT_DEVICE_ID, clientId).apply(); 75 | } 76 | 77 | updateValues(); 78 | } 79 | 80 | public void updateValues() { 81 | password = mSharedPreferences.getString(SP_MQTT_PASSWORD, "").getBytes(); 82 | username = mSharedPreferences.getString(SP_MQTT_USERNAME, ""); 83 | broker = mSharedPreferences.getString(SP_MQTT_BROKER, ""); 84 | port = mSharedPreferences.getInt(SP_MQTT_PORT, 1883); 85 | clientId = mSharedPreferences.getString(SP_MQTT_DEVICE_ID, "shellywalldisplay"); 86 | enabled = mSharedPreferences.getBoolean(SP_MQTT_ENABLED, false); 87 | 88 | validForConnection = password.length > 0 && !username.isEmpty() && !broker.isEmpty(); 89 | 90 | connect(); 91 | } 92 | 93 | public void disconnect() { 94 | if (mMqttClient != null && mMqttClient.isConnected()) { 95 | try { 96 | deleteConfig(); 97 | mMqttClient.publish(parseTopic(MQTT_TOPIC_STATUS), "offline".getBytes(), 1, true); 98 | mMqttClient.disconnect(); 99 | 100 | connected = false; 101 | } catch (MqttException e) { 102 | Log.e("MQTT", "Error disconnecting MQTT client", e); 103 | } 104 | } 105 | } 106 | 107 | public void connect() { 108 | if (validForConnection) { 109 | try { 110 | mMqttConnectionsOptions.setUserName(username); 111 | mMqttConnectionsOptions.setPassword(password); 112 | 113 | if (connected) { 114 | disconnect(); 115 | } 116 | 117 | mMqttClient = new MqttClient(broker + ":" + port, clientId, mMemoryPersistence); 118 | mMqttClient.setCallback(mShellyElevateMQTTCallback); 119 | mMqttClient.connect(mMqttConnectionsOptions); 120 | 121 | publishConfig(); 122 | 123 | mMqttClient.publish(parseTopic(MQTT_TOPIC_STATUS), "online".getBytes(), 1, true); 124 | 125 | mMqttClient.subscribe("shellyelevatev2/#", 0); 126 | mMqttClient.subscribe("shellyelevatev2/#", 1); 127 | mMqttClient.subscribe("shellyelevatev2/#", 2); 128 | 129 | mMqttClient.subscribe(MQTT_TOPIC_HOME_ASSISTANT_STATUS, 0); 130 | mMqttClient.subscribe(MQTT_TOPIC_HOME_ASSISTANT_STATUS, 1); 131 | mMqttClient.subscribe(MQTT_TOPIC_HOME_ASSISTANT_STATUS, 2); 132 | 133 | connected = true; 134 | 135 | publishTempAndHum(); 136 | publishRelay(mDeviceHelper.getRelay()); 137 | publishLux(mDeviceSensorManager.getLastMeasuredLux()); 138 | publishSleeping(ScreenSaverManagerHolder.getInstance().isScreenSaverRunning()); 139 | 140 | } catch (MqttException | JSONException e) { 141 | Log.e("MQTT", "Error connecting:", e); 142 | } 143 | } 144 | } 145 | 146 | public boolean isEnabled() { 147 | return enabled; 148 | } 149 | 150 | public boolean shouldSend() { 151 | return connected && enabled; 152 | } 153 | 154 | public void publishTempAndHum() { 155 | if (this.shouldSend()) { 156 | this.publishTemp((float) mDeviceHelper.getTemperature()); 157 | this.publishHum((float) mDeviceHelper.getHumidity()); 158 | } 159 | } 160 | 161 | public void publishTemp(float temp) { 162 | try { 163 | mMqttClient.publish(parseTopic(MQTT_TOPIC_TEMP_SENSOR), String.valueOf(temp).getBytes(), 1, false); 164 | } catch (MqttException e) { 165 | Log.e("MQTT", "Error publishing temperature", e); 166 | } 167 | } 168 | 169 | public void publishHum(float hum) { 170 | try { 171 | mMqttClient.publish(parseTopic(MQTT_TOPIC_HUM_SENSOR), String.valueOf(hum).getBytes(), 1, false); 172 | } catch (MqttException e) { 173 | Log.e("MQTT", "Error publishing humidity", e); 174 | } 175 | } 176 | 177 | public void publishLux(float lux) { 178 | try { 179 | mMqttClient.publish(parseTopic(MQTT_TOPIC_LUX_SENSOR), String.valueOf(lux).getBytes(), 1, false); 180 | } catch (MqttException e) { 181 | Log.e("MQTT", "Error publishing lux", e); 182 | } 183 | } 184 | 185 | public void publishRelay(boolean state) { 186 | try { 187 | mMqttClient.publish(parseTopic(MQTT_TOPIC_RELAY_STATE), (state ? "ON" : "OFF").getBytes(), 1, false); 188 | } catch (MqttException e) { 189 | Log.e("MQTT", "Error publishing relay state", e); 190 | } 191 | } 192 | 193 | public void publishSleeping(boolean state) { 194 | try { 195 | mMqttClient.publish(parseTopic(MQTT_TOPIC_SLEEPING_BINARY_SENSOR), (state ? "ON" : "OFF").getBytes(), 1, false); 196 | } catch (MqttException e) { 197 | Log.e("MQTT", "Error publishing sleeping state", e); 198 | } 199 | } 200 | 201 | public void publishSwipeEvent() { 202 | try { 203 | mMqttClient.publish(parseTopic(MQTT_TOPIC_SWIPE_EVENT), "{\"event_type\": \"swipe\"}".getBytes(), 1, false); 204 | } catch (MqttException e) { 205 | Log.e("MQTT", "Error publishing swipe event", e); 206 | } 207 | } 208 | 209 | private void deleteConfig() throws MqttException { 210 | mMqttClient.publish(parseTopic(MQTT_TOPIC_CONFIG_DEVICE), "".getBytes(), 1, false); 211 | } 212 | 213 | private void publishConfig() throws JSONException, MqttException { 214 | JSONObject configPayload = new JSONObject(); 215 | 216 | JSONObject device = new JSONObject(); 217 | device.put("ids", clientId); 218 | device.put("name", "Shelly Wall Display"); 219 | device.put("mf", "Shelly"); 220 | configPayload.put("dev", device); 221 | 222 | JSONObject origin = new JSONObject(); 223 | origin.put("name", "ShellyElevateV2"); 224 | origin.put("url", "https://github.com/RapierXbox/ShellyElevate"); 225 | configPayload.put("o", origin); 226 | 227 | JSONObject components = new JSONObject(); 228 | 229 | JSONObject tempSensorPayload = new JSONObject(); 230 | tempSensorPayload.put("p", "sensor"); 231 | tempSensorPayload.put("name", "Temperature"); 232 | tempSensorPayload.put("state_topic", parseTopic(MQTT_TOPIC_TEMP_SENSOR)); 233 | tempSensorPayload.put("device_class", "temperature"); 234 | tempSensorPayload.put("unit_of_measurement", "°C"); 235 | tempSensorPayload.put("unique_id", clientId + "_temp"); 236 | components.put(clientId + "_temp", tempSensorPayload); 237 | 238 | JSONObject humSensorPayload = new JSONObject(); 239 | humSensorPayload.put("p", "sensor"); 240 | humSensorPayload.put("name", "Humidity"); 241 | humSensorPayload.put("state_topic", parseTopic(MQTT_TOPIC_HUM_SENSOR)); 242 | humSensorPayload.put("device_class", "humidity"); 243 | humSensorPayload.put("unit_of_measurement", "%"); 244 | humSensorPayload.put("unique_id", clientId + "_hum"); 245 | components.put(clientId + "_hum", humSensorPayload); 246 | 247 | JSONObject luxSensorPayload = new JSONObject(); 248 | luxSensorPayload.put("p", "sensor"); 249 | luxSensorPayload.put("name", "Light"); 250 | luxSensorPayload.put("state_topic", parseTopic(MQTT_TOPIC_LUX_SENSOR)); 251 | luxSensorPayload.put("device_class", "illuminance"); 252 | luxSensorPayload.put("unit_of_measurement", "lx"); 253 | luxSensorPayload.put("unique_id", clientId + "_lux"); 254 | components.put(clientId + "_lux", luxSensorPayload); 255 | 256 | JSONObject relaySwitchPayload = new JSONObject(); 257 | relaySwitchPayload.put("p", "switch"); 258 | relaySwitchPayload.put("name", "Relay"); 259 | relaySwitchPayload.put("state_topic", parseTopic(MQTT_TOPIC_RELAY_STATE)); 260 | relaySwitchPayload.put("command_topic", parseTopic(MQTT_TOPIC_RELAY_COMMAND)); 261 | relaySwitchPayload.put("device_class", "outlet"); 262 | relaySwitchPayload.put("unique_id", clientId + "_relay"); 263 | components.put(clientId + "_relay", relaySwitchPayload); 264 | 265 | JSONObject sleepButtonPayload = new JSONObject(); 266 | sleepButtonPayload.put("p", "button"); 267 | sleepButtonPayload.put("name", "Sleep"); 268 | sleepButtonPayload.put("command_topic", parseTopic(MQTT_TOPIC_SLEEP_BUTTON)); 269 | sleepButtonPayload.put("unique_id", clientId + "_sleep"); 270 | components.put(clientId + "_sleep", sleepButtonPayload); 271 | 272 | JSONObject wakeButtonPayload = new JSONObject(); 273 | wakeButtonPayload.put("p", "button"); 274 | wakeButtonPayload.put("name", "Wake"); 275 | wakeButtonPayload.put("command_topic", parseTopic(MQTT_TOPIC_WAKE_BUTTON)); 276 | wakeButtonPayload.put("unique_id", clientId + "_wake"); 277 | components.put(clientId + "_wake", wakeButtonPayload); 278 | 279 | JSONObject refreshWebviewButtonPayload = new JSONObject(); 280 | refreshWebviewButtonPayload.put("p", "button"); 281 | refreshWebviewButtonPayload.put("name", "Refresh Webview"); 282 | refreshWebviewButtonPayload.put("command_topic", parseTopic(MQTT_TOPIC_REFRESH_WEBVIEW_BUTTON)); 283 | refreshWebviewButtonPayload.put("device_class", "restart"); 284 | refreshWebviewButtonPayload.put("unique_id", clientId + "_refresh_webview"); 285 | components.put(clientId + "_refresh_webview", refreshWebviewButtonPayload); 286 | 287 | JSONObject rebootButtonPayload = new JSONObject(); 288 | rebootButtonPayload.put("p", "button"); 289 | rebootButtonPayload.put("name", "Reboot"); 290 | rebootButtonPayload.put("command_topic", parseTopic(MQTT_TOPIC_REBOOT_BUTTON)); 291 | rebootButtonPayload.put("device_class", "restart"); 292 | rebootButtonPayload.put("unique_id", clientId + "_reboot"); 293 | components.put(clientId + "_reboot", rebootButtonPayload); 294 | 295 | JSONObject swipeEventPayload = new JSONObject(); 296 | swipeEventPayload.put("p", "event"); 297 | swipeEventPayload.put("name", "Swipe Event"); 298 | swipeEventPayload.put("state_topic", parseTopic(MQTT_TOPIC_SWIPE_EVENT)); 299 | swipeEventPayload.put("device_class", "button"); 300 | swipeEventPayload.put("event_types", new JSONArray().put("swipe")); 301 | swipeEventPayload.put("unique_id", clientId + "_swipe_event"); 302 | components.put(clientId + "_swipe_event", swipeEventPayload); 303 | 304 | JSONObject sleepingBinarySensorPayload = new JSONObject(); 305 | sleepingBinarySensorPayload.put("p", "binary_sensor"); 306 | sleepingBinarySensorPayload.put("name", "Sleeping"); 307 | sleepingBinarySensorPayload.put("state_topic", parseTopic(MQTT_TOPIC_SLEEPING_BINARY_SENSOR)); 308 | sleepingBinarySensorPayload.put("unique_id", clientId + "_sleeping"); 309 | components.put(clientId + "_sleeping", sleepingBinarySensorPayload); 310 | 311 | configPayload.put("cmps", components); 312 | 313 | configPayload.put("state_topic", MQTT_TOPIC_STATUS); 314 | 315 | mMqttClient.publish(parseTopic(MQTT_TOPIC_CONFIG_DEVICE), configPayload.toString().getBytes(), 1, true); 316 | } 317 | 318 | private String parseTopic(String topic) { 319 | return topic.replace("%s", clientId); 320 | } 321 | 322 | public String getClientId() { 323 | return clientId; 324 | } 325 | 326 | public void onDestroy() { 327 | disconnect(); 328 | 329 | if (scheduler != null && !scheduler.isShutdown()) { 330 | scheduler.shutdown(); 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/mqtt/ShellyElevateMQTTCallback.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.mqtt; 2 | 3 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mApplicationContext; 4 | import static me.rapierxbox.shellyelevatev2.Constants.*; 5 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceHelper; 6 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mMQTTServer; 7 | 8 | import android.content.Intent; 9 | import android.util.Log; 10 | import android.widget.Toast; 11 | 12 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; 13 | 14 | import org.eclipse.paho.mqttv5.client.IMqttToken; 15 | import org.eclipse.paho.mqttv5.client.MqttCallback; 16 | import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse; 17 | import org.eclipse.paho.mqttv5.common.MqttException; 18 | import org.eclipse.paho.mqttv5.common.MqttMessage; 19 | import org.eclipse.paho.mqttv5.common.packet.MqttProperties; 20 | 21 | import java.io.IOException; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.Arrays; 24 | 25 | import me.rapierxbox.shellyelevatev2.ShellyElevateApplication; 26 | import me.rapierxbox.shellyelevatev2.screensavers.ScreenSaverManagerHolder; 27 | 28 | public class ShellyElevateMQTTCallback implements MqttCallback { 29 | @Override 30 | public void disconnected(MqttDisconnectResponse disconnectResponse) { 31 | Log.i("MQTT", "Disconnected"); 32 | Toast.makeText(mApplicationContext, "MQTT disconnected", Toast.LENGTH_SHORT).show(); 33 | } 34 | 35 | @Override 36 | public void mqttErrorOccurred(MqttException exception) { 37 | Log.e("MQTT", "Error occurred: " + exception); 38 | } 39 | 40 | @Override 41 | public void messageArrived(String topic, MqttMessage message) { 42 | switch (topic.replace(mMQTTServer.getClientId(), "%s")) { 43 | case MQTT_TOPIC_RELAY_COMMAND: 44 | mDeviceHelper.setRelay(new String(message.getPayload(), StandardCharsets.UTF_8).contains("ON")); 45 | break; 46 | case MQTT_TOPIC_REFRESH_WEBVIEW_BUTTON: 47 | Intent intent = new Intent(INTENT_WEBVIEW_REFRESH); 48 | LocalBroadcastManager.getInstance(ShellyElevateApplication.mApplicationContext).sendBroadcast(intent); 49 | break; 50 | case MQTT_TOPIC_SLEEP_BUTTON: 51 | ScreenSaverManagerHolder.getInstance().startScreenSaver(); 52 | break; 53 | case MQTT_TOPIC_WAKE_BUTTON: 54 | ScreenSaverManagerHolder.getInstance().stopScreenSaver(); 55 | break; 56 | case MQTT_TOPIC_REBOOT_BUTTON: 57 | long deltaTime = System.currentTimeMillis() - ShellyElevateApplication.getApplicationStartTime(); 58 | deltaTime /= 1000; 59 | if (deltaTime > 20) { 60 | try { 61 | Runtime.getRuntime().exec("reboot"); 62 | } catch (IOException e) { 63 | Log.e("MQTT", "Error rebooting:", e); 64 | } 65 | 66 | } else { 67 | Toast.makeText(mApplicationContext, "Please wait %s seconds before rebooting".replace("%s",String.valueOf(20-deltaTime) ), Toast.LENGTH_LONG).show(); 68 | } 69 | } 70 | } 71 | 72 | @Override 73 | public void deliveryComplete(IMqttToken token) { 74 | 75 | } 76 | 77 | @Override 78 | public void connectComplete(boolean reconnect, String serverURI) { 79 | Log.i("MQTT", "Connected to: " + serverURI); 80 | } 81 | 82 | @Override 83 | public void authPacketArrived(int reasonCode, MqttProperties properties) { 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/screensavers/DigitalClockAndDateScreenSaver.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.screensavers; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | 6 | import me.rapierxbox.shellyelevatev2.screensavers.activities.DigitalClockAndDateScreenSaverActivity; 7 | 8 | public class DigitalClockAndDateScreenSaver extends ScreenSaver { 9 | @Override 10 | public void onStart(Context context) { 11 | Intent intent = new Intent(context, DigitalClockAndDateScreenSaverActivity.class); 12 | intent.putExtra("date", true); 13 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 14 | context.startActivity(intent); 15 | } 16 | 17 | @Override 18 | public void onEnd(Context context) {} 19 | 20 | @Override 21 | public String getName() { 22 | return "Digital Clock and Date"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/screensavers/DigitalClockScreenSaver.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.screensavers; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | 6 | import me.rapierxbox.shellyelevatev2.screensavers.activities.DigitalClockAndDateScreenSaverActivity; 7 | 8 | public class DigitalClockScreenSaver extends ScreenSaver { 9 | 10 | public void onStart(Context context) { 11 | Intent intent = new Intent(context, DigitalClockAndDateScreenSaverActivity.class); 12 | intent.putExtra("date", false); 13 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 14 | context.startActivity(intent); 15 | } 16 | 17 | @Override 18 | public void onEnd(Context context) { 19 | } 20 | 21 | @Override 22 | public String getName() { 23 | return "Digital Clock"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/screensavers/ScreenOffScreenSaver.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.screensavers; 2 | 3 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mDeviceHelper; 4 | 5 | import android.content.Context; 6 | 7 | public class ScreenOffScreenSaver extends ScreenSaver{ 8 | @Override 9 | public void onStart(Context context) { 10 | mDeviceHelper.setScreenOn(false); 11 | } 12 | 13 | @Override 14 | public void onEnd(Context context) { 15 | mDeviceHelper.setScreenOn(true); 16 | } 17 | 18 | @Override 19 | public String getName() { 20 | return "Screen Off"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/screensavers/ScreenSaver.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.screensavers; 2 | 3 | import android.content.Context; 4 | 5 | public abstract class ScreenSaver { 6 | public abstract void onStart(Context context); 7 | public abstract void onEnd(Context context); 8 | public abstract String getName(); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/screensavers/ScreenSaverManager.java: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.screensavers; 2 | 3 | import static me.rapierxbox.shellyelevatev2.Constants.SP_SCREEN_SAVER_DELAY; 4 | import static me.rapierxbox.shellyelevatev2.Constants.SP_SCREEN_SAVER_ENABLED; 5 | import static me.rapierxbox.shellyelevatev2.Constants.SP_SCREEN_SAVER_ID; 6 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mApplicationContext; 7 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mMQTTServer; 8 | import static me.rapierxbox.shellyelevatev2.ShellyElevateApplication.mSharedPreferences; 9 | 10 | import android.content.Intent; 11 | import android.util.Log; 12 | import android.widget.ArrayAdapter; 13 | 14 | import java.util.concurrent.Executors; 15 | import java.util.concurrent.ScheduledExecutorService; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | import me.rapierxbox.shellyelevatev2.backbutton.FloatingBackButtonService; 19 | 20 | public class ScreenSaverManager { 21 | private long lastTouchEventTime; 22 | private int screenSaverDelay; 23 | private boolean screenSaverRunning; 24 | 25 | private final ScheduledExecutorService scheduler; 26 | 27 | private final ScreenSaver[] screenSavers; 28 | private int currentScreenSaverId; 29 | 30 | private boolean screenSaverEnabled; 31 | 32 | public ScreenSaverManager() { 33 | 34 | scheduler = Executors.newScheduledThreadPool(1); 35 | scheduler.scheduleWithFixedDelay(this::checkLastTouchEventTime, 0, 1, TimeUnit.SECONDS); 36 | 37 | lastTouchEventTime = 0; 38 | screenSaverDelay = 45; 39 | currentScreenSaverId = 0; 40 | screenSaverRunning = false; 41 | 42 | screenSavers = new ScreenSaver[]{ 43 | new ScreenOffScreenSaver(), 44 | new DigitalClockScreenSaver(), 45 | new DigitalClockAndDateScreenSaver() 46 | }; 47 | } 48 | 49 | public boolean onTouchEvent() { 50 | lastTouchEventTime = System.currentTimeMillis(); 51 | if (screenSaverRunning) { 52 | stopScreenSaver(); 53 | return true; 54 | } 55 | return false; 56 | } 57 | 58 | public void updateValues() { 59 | stopScreenSaver(); 60 | 61 | screenSaverDelay = mSharedPreferences.getInt(SP_SCREEN_SAVER_DELAY, 45); 62 | if (screenSaverDelay < 5) { 63 | screenSaverDelay = 5; 64 | mSharedPreferences.edit().putInt(SP_SCREEN_SAVER_DELAY, 5).apply(); 65 | } 66 | currentScreenSaverId = mSharedPreferences.getInt(SP_SCREEN_SAVER_ID, 0); 67 | if (currentScreenSaverId < 0 || currentScreenSaverId >= screenSavers.length) { 68 | currentScreenSaverId = 0; 69 | mSharedPreferences.edit().putInt(SP_SCREEN_SAVER_ID, 0).apply(); 70 | } 71 | screenSaverEnabled = mSharedPreferences.getBoolean(SP_SCREEN_SAVER_ENABLED, true); 72 | currentScreenSaverId = Math.min(Math.max(currentScreenSaverId, 0), screenSavers.length - 1); 73 | lastTouchEventTime = System.currentTimeMillis(); 74 | } 75 | 76 | public ArrayAdapter getScreenSaverSpinnerAdapter() { 77 | ArrayAdapter adapter = new ArrayAdapter<>(mApplicationContext, android.R.layout.simple_spinner_item); 78 | adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 79 | 80 | for (ScreenSaver screenSaver : screenSavers) { 81 | adapter.add(screenSaver.getName()); 82 | } 83 | 84 | return adapter; 85 | } 86 | 87 | public boolean isScreenSaverRunning() { 88 | return screenSaverRunning; 89 | } 90 | 91 | public int getCurrentScreenSaverId() { 92 | return currentScreenSaverId; 93 | } 94 | 95 | public boolean isScreenSaverEnabled() { 96 | return screenSaverEnabled; 97 | } 98 | 99 | public void onDestroy() { 100 | if (scheduler != null && !scheduler.isShutdown()) { 101 | scheduler.shutdown(); 102 | } 103 | } 104 | 105 | private void checkLastTouchEventTime() { 106 | if (System.currentTimeMillis() - lastTouchEventTime > screenSaverDelay * 1000L && screenSaverEnabled) { 107 | startScreenSaver(); 108 | } 109 | } 110 | 111 | public void startScreenSaver() { 112 | if (!screenSaverRunning) { 113 | screenSaverRunning = true; 114 | 115 | Intent backButtonIntent = new Intent(mApplicationContext, FloatingBackButtonService.class); 116 | backButtonIntent.setAction(FloatingBackButtonService.PAUSE_BUTTON); 117 | mApplicationContext.startService(backButtonIntent); 118 | 119 | screenSavers[currentScreenSaverId].onStart(mApplicationContext); 120 | Log.i("ShellyElevateV2", "Starting screen saver with id: " + currentScreenSaverId); 121 | 122 | if (mMQTTServer.shouldSend()) { 123 | mMQTTServer.publishSleeping(true); 124 | } 125 | } 126 | } 127 | 128 | public void stopScreenSaver() { 129 | if (screenSaverRunning) { 130 | screenSaverRunning = false; 131 | screenSavers[currentScreenSaverId].onEnd(mApplicationContext); 132 | lastTouchEventTime = System.currentTimeMillis(); 133 | Log.i("ShellyElevateV2", "Ending screen saver with id: " + currentScreenSaverId); 134 | 135 | Intent backButtonIntent = new Intent(mApplicationContext, FloatingBackButtonService.class); 136 | backButtonIntent.setAction(FloatingBackButtonService.RESUME_BUTTON); 137 | mApplicationContext.startService(backButtonIntent); 138 | 139 | if (mMQTTServer.shouldSend()) { 140 | mMQTTServer.publishSleeping(false); 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/screensavers/ScreenSaverManagerHolder.kt: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.screensavers 2 | 3 | object ScreenSaverManagerHolder { 4 | private var instance: ScreenSaverManager? = null 5 | 6 | @JvmStatic 7 | fun initialize(): ScreenSaverManager { 8 | if (instance == null) { 9 | instance = ScreenSaverManager() 10 | } 11 | return instance!! 12 | } 13 | 14 | //Currently the two methods are identical, but 15 | //in the future they may be different. For example 16 | //init is probably going to have a context as parameter 17 | @JvmStatic 18 | fun getInstance(): ScreenSaverManager { 19 | if (instance == null) { 20 | instance = ScreenSaverManager() 21 | } 22 | return instance!! 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/screensavers/UserInteractionReceiver.kt: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.screensavers 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import me.rapierxbox.shellyelevatev2.backbutton.BackAccessibilityService.Companion.ACTION_USER_INTERACTION 7 | 8 | class UserInteractionReceiver : BroadcastReceiver() { 9 | override fun onReceive(context: Context?, intent: Intent?) { 10 | if (intent?.action == ACTION_USER_INTERACTION) { 11 | ScreenSaverManagerHolder.getInstance().onTouchEvent() 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/me/rapierxbox/shellyelevatev2/screensavers/activities/DigitalClockAndDateScreenSaverActivity.kt: -------------------------------------------------------------------------------- 1 | package me.rapierxbox.shellyelevatev2.screensavers.activities 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.IntentFilter 9 | import android.os.Bundle 10 | import androidx.core.view.isVisible 11 | import me.rapierxbox.shellyelevatev2.ShellyElevateApplication 12 | import me.rapierxbox.shellyelevatev2.databinding.DigitalClockAndDateScreenSaverBinding 13 | import me.rapierxbox.shellyelevatev2.screensavers.ScreenSaverManagerHolder 14 | import java.text.SimpleDateFormat 15 | import java.util.Date 16 | 17 | class DigitalClockAndDateScreenSaverActivity : Activity() { 18 | private lateinit var binding: DigitalClockAndDateScreenSaverBinding // Declare the binding object 19 | 20 | private val timeFormatter = SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT) 21 | private val dateFormatter = SimpleDateFormat.getDateInstance(SimpleDateFormat.MEDIUM) 22 | 23 | private var showDate = false 24 | 25 | private val mTimeTickBroadCastReciver = object : BroadcastReceiver() { 26 | override fun onReceive(context: Context?, intent: Intent?) { 27 | updateTime() 28 | } 29 | } 30 | 31 | private fun updateTime() { 32 | val now = Date() 33 | binding.clockText.text = timeFormatter.format(now) 34 | 35 | if (showDate) 36 | binding.dateText.text = dateFormatter.format(now) 37 | } 38 | 39 | @SuppressLint("ClickableViewAccessibility") 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | super.onCreate(savedInstanceState) 42 | 43 | showDate = intent.getBooleanExtra("date", false) 44 | 45 | binding = DigitalClockAndDateScreenSaverBinding.inflate(layoutInflater) // Inflate the binding 46 | setContentView(binding.root) // Set the content view using binding.root 47 | 48 | binding.dateText.isVisible = showDate 49 | 50 | updateTime() 51 | 52 | binding.swipeDetectionOverlay.setOnTouchListener { _, event -> 53 | ScreenSaverManagerHolder.getInstance().onTouchEvent() 54 | ShellyElevateApplication.mSwipeHelper.onTouchEvent(event) 55 | 56 | finish() 57 | 58 | return@setOnTouchListener false 59 | } 60 | 61 | registerReceiver(mTimeTickBroadCastReciver, IntentFilter(Intent.ACTION_TIME_TICK)) 62 | } 63 | 64 | override fun onDestroy() { 65 | super.onDestroy() 66 | 67 | unregisterReceiver(mTimeTickBroadCastReciver) 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back_arrow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_button_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/digital_clock_and_date_screen_saver.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 33 | 34 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/floating_button_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 19 | 29 | 30 | 39 | 40 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 14 | 15 | 20 | 21 | 22 | 23 | 29 | 30 | 35 | 36 | 41 | 42 |