├── .github └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── blazecode │ │ └── tsviewer │ │ └── ExampleInstrumentedTest.kt │ ├── core │ └── java │ │ ├── data │ │ └── WearDataPackage.kt │ │ ├── screens │ │ └── Settings.kt │ │ ├── uistate │ │ └── SettingsUiState.kt │ │ ├── util │ │ ├── ClientsWorker.kt │ │ └── SettingsManager.kt │ │ ├── viewmodels │ │ ├── HomeViewModel.kt │ │ └── SettingsViewModel.kt │ │ ├── views │ │ └── DebugMenu.kt │ │ └── wear │ │ ├── WearDataManager.kt │ │ └── WearableListenerService.kt │ ├── foss │ └── java │ │ ├── screens │ │ └── Settings.kt │ │ ├── uistate │ │ └── SettingsUiState.kt │ │ ├── util │ │ ├── ClientsWorker.kt │ │ └── SettingsManager.kt │ │ ├── viewmodels │ │ ├── HomeViewModel.kt │ │ └── SettingsViewModel.kt │ │ └── views │ │ └── DebugMenu.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── blazecode │ │ │ └── tsviewer │ │ │ ├── MainActivity.kt │ │ │ ├── TSViewerApplication.kt │ │ │ ├── data │ │ │ ├── ConnectionDetails.kt │ │ │ ├── Entry.kt │ │ │ ├── ErrorCode.kt │ │ │ ├── TsChannel.kt │ │ │ ├── TsClient.kt │ │ │ └── TsServerInfo.kt │ │ │ ├── database │ │ │ ├── ClientDAO.kt │ │ │ ├── ClientDatabase.kt │ │ │ ├── ClientRepository.kt │ │ │ ├── DatabaseManager.kt │ │ │ ├── ServerDAO.kt │ │ │ ├── ServerDatabase.kt │ │ │ └── ServerRepository.kt │ │ │ ├── navigation │ │ │ ├── NavBarItem.kt │ │ │ └── NavRoutes.kt │ │ │ ├── screens │ │ │ ├── About.kt │ │ │ ├── Data.kt │ │ │ ├── Home.kt │ │ │ └── Introduction.kt │ │ │ ├── ui │ │ │ └── theme │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ ├── uistate │ │ │ ├── AboutUiState.kt │ │ │ ├── DataUiState.kt │ │ │ ├── HomeUiState.kt │ │ │ └── IntroductionUiState.kt │ │ │ ├── util │ │ │ ├── ConnectionManager.kt │ │ │ ├── DemoModeValues.kt │ │ │ ├── ErrorHandler.kt │ │ │ ├── LinkUtil.kt │ │ │ ├── ServiceManager.kt │ │ │ ├── errors │ │ │ │ └── ErrorReportActivity.kt │ │ │ ├── graphmarker │ │ │ │ └── Marker.kt │ │ │ ├── notification │ │ │ │ ├── ClientNotificationManager.kt │ │ │ │ └── NotificationBroadcastReceiver.kt │ │ │ ├── tile │ │ │ │ ├── ClientTileService.kt │ │ │ │ └── TileManager.kt │ │ │ ├── typeconverters │ │ │ │ ├── ClientListTypeConverter.kt │ │ │ │ └── DateTypeConverter.kt │ │ │ └── updater │ │ │ │ ├── GitHubAssets.kt │ │ │ │ ├── GitHubRelease.kt │ │ │ │ └── GitHubUpdater.kt │ │ │ ├── viewmodels │ │ │ ├── AboutViewModel.kt │ │ │ ├── DataViewModel.kt │ │ │ └── IntroductionViewModel.kt │ │ │ └── views │ │ │ ├── BottomNavBar.kt │ │ │ ├── DefaultPreference.kt │ │ │ ├── EditTextPreference.kt │ │ │ ├── GitHubUpdateCard.kt │ │ │ ├── PreferenceGroup.kt │ │ │ ├── SliderPreference.kt │ │ │ ├── SwitchBar.kt │ │ │ ├── SwitchPreference.kt │ │ │ └── TsChannelList.kt │ └── res │ │ ├── drawable-anydpi │ │ └── ic_notification_icon.xml │ │ ├── drawable │ │ ├── ic_back.xml │ │ ├── ic_battery.xml │ │ ├── ic_expand_less.xml │ │ ├── ic_expand_more.xml │ │ ├── ic_github.xml │ │ ├── ic_home.xml │ │ ├── ic_info.xml │ │ ├── ic_insights.xml │ │ ├── ic_ip.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ ├── ic_licenses.xml │ │ ├── ic_mail.xml │ │ ├── ic_mic_muted.xml │ │ ├── ic_notification.xml │ │ ├── ic_password.xml │ │ ├── ic_port.xml │ │ ├── ic_qs_tile.xml │ │ ├── ic_query_client.xml │ │ ├── ic_settings.xml │ │ ├── ic_speaker_muted.xml │ │ ├── ic_suggest.xml │ │ ├── ic_translate.xml │ │ ├── ic_update.xml │ │ ├── ic_user.xml │ │ ├── ic_virtual_server_id.xml │ │ ├── ic_visibility.xml │ │ ├── ic_visibility_off.xml │ │ ├── ic_wearable.xml │ │ └── ic_wifi.xml │ │ ├── font-v26 │ │ └── inter.xml │ │ ├── font │ │ ├── inter.xml │ │ ├── inter_bold.ttf │ │ └── inter_regular.ttf │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── raw │ │ ├── lottie_empty_database.json │ │ ├── lottie_loading.json │ │ └── lottie_no_connection.json │ │ ├── values │ │ ├── constants.xml │ │ ├── dimen.xml │ │ ├── strings.xml │ │ ├── themes.xml │ │ └── wear.xml │ │ └── xml │ │ └── file_provider_paths.xml │ └── test │ └── java │ └── com │ └── blazecode │ └── tsviewer │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── de-DE │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ └── en-US │ ├── changelogs │ ├── 1.txt │ └── 2.txt │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ └── dummy │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── wear ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java ├── com │ └── blazecode │ │ └── tsviewer │ │ └── wear │ │ ├── MainActivity.kt │ │ ├── communication │ │ ├── WearDataManager.kt │ │ └── WearableListenerService.kt │ │ ├── complication │ │ ├── Complication.kt │ │ ├── ComplicationArguments.kt │ │ └── ComplicationProvider.kt │ │ ├── data │ │ ├── DataHolder.kt │ │ ├── TsClient.kt │ │ └── WearDataPackage.kt │ │ ├── enum │ │ └── ErrorCode.kt │ │ ├── navigation │ │ └── NavRoutes.kt │ │ ├── screens │ │ ├── ClientList.kt │ │ ├── Error.kt │ │ ├── Home.kt │ │ └── ServiceOff.kt │ │ ├── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ │ ├── uistate │ │ ├── ClientListUiState.kt │ │ ├── ErrorUiState.kt │ │ ├── HomeUiState.kt │ │ └── ServiceOffUiState.kt │ │ └── viewmodels │ │ ├── ClientListViewModel.kt │ │ ├── ErrorViewModel.kt │ │ ├── HomeViewModel.kt │ │ └── ServiceOffViewModel.kt └── data │ └── WearDataPackage.kt └── res ├── drawable ├── ic_clients.xml ├── ic_error.xml ├── ic_icon.xml ├── ic_launcher_background.xml ├── ic_launcher_foreground.xml ├── ic_open.xml └── ic_refresh.xml ├── mipmap-xxxhdpi └── ic_launcher.xml └── values ├── colors.xml ├── dimen.xml ├── strings.xml └── wear.xml /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | assignees: 6 | - blazecodedev 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | - type: input 13 | id: contact 14 | attributes: 15 | label: Contact Details 16 | description: How can we get in touch with you if we need more info? (publicly visible) 17 | placeholder: ex. email@example.com 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: what-happened 22 | attributes: 23 | label: What happened? 24 | description: Also tell us, what did you expect to happen? 25 | placeholder: Tell us what you see! 26 | value: "A bug happened!" 27 | validations: 28 | required: true 29 | - type: input 30 | id: device 31 | attributes: 32 | label: Device 33 | description: What device does the bug happen on? 34 | placeholder: Pixel 6 Pro 35 | validations: 36 | required: true 37 | - type: input 38 | id: version-android 39 | attributes: 40 | label: Android version 41 | description: What Android version are you using? 42 | placeholder: Android 13 43 | validations: 44 | required: true 45 | - type: input 46 | id: version-app 47 | attributes: 48 | label: App version 49 | description: What version of the app are you using? 50 | placeholder: V1.4 51 | validations: 52 | required: true 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a feature 3 | title: "[Feature Request]: " 4 | labels: ["feature request"] 5 | assignees: 6 | - blazecodedev 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this feature request form! 12 | - type: input 13 | id: contact 14 | attributes: 15 | label: Contact Details 16 | description: How can we get in touch with you if we need more info? (publicly visible) 17 | placeholder: ex. email@example.com 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: problem-relation 22 | attributes: 23 | label: Problem relation 24 | description: Is your request related to a problem you are experiencing? 25 | placeholder: I'm always frustrated when... 26 | validations: 27 | required: false 28 | - type: textarea 29 | id: solution 30 | attributes: 31 | label: Solution / Feature 32 | description: Describe the feature you want to see in the app as precise as possible. 33 | placeholder: I'd like to see... 34 | validations: 35 | required: true 36 | - type: textarea 37 | id: alternatives 38 | attributes: 39 | label: Alternative solutions 40 | description: Describe the alternative solutions you have thought of as precise as possible. 41 | placeholder: I've also considered... 42 | validations: 43 | required: false 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | /*/build/ 3 | 4 | # Crashlytics configuations 5 | com_crashlytics_export_strings.xml 6 | 7 | # Local configuration file (sdk path, etc) 8 | local.properties 9 | 10 | # Gradle generated files 11 | .gradle/ 12 | 13 | # Signing files 14 | .signing/ 15 | 16 | # User-specific configurations 17 | *.iml 18 | .idea/ 19 | app/foss/* 20 | app/core/* 21 | 22 | # OS-specific files 23 | .DS_Store 24 | .DS_Store? 25 | ._* 26 | .Spotlight-V100 27 | .Trashes 28 | ehthumbs.db 29 | Thumbs.db 30 | /app/release/ 31 | /app/debug/ 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 BlazeCodeDev, Ralf Lehmann 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 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -dontwarn org.slf4j.impl.StaticLoggerBinder 24 | -dontwarn sun.security.x509.X509Key 25 | -dontwarn javax.annotation.processing.AbstractProcessor 26 | -dontwarn javax.annotation.processing.SupportedOptions 27 | -keep class com.blazecode.tsviewer.util.updater.GitHubAssets { *; } 28 | -keep class com.blazecode.tsviewer.util.updater.GitHubRelease { *; } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/blazecode/tsviewer/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer 8 | 9 | import androidx.test.ext.junit.runners.AndroidJUnit4 10 | import androidx.test.platform.app.InstrumentationRegistry 11 | import org.junit.Assert.assertEquals 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | 15 | /** 16 | * Instrumented test, which will execute on an Android device. 17 | * 18 | * See [testing documentation](http://d.android.com/tools/testing). 19 | */ 20 | @RunWith(AndroidJUnit4::class) 21 | class ExampleInstrumentedTest { 22 | @Test 23 | fun useAppContext() { 24 | // Context of the app under test. 25 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 26 | assertEquals("com.blazecode.tsviewer", appContext.packageName) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/core/java/data/WearDataPackage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package data 8 | 9 | import com.blazecode.tsviewer.data.TsClient 10 | 11 | data class WearDataPackage( 12 | val clients: List, 13 | val timestamp: Long 14 | ) -------------------------------------------------------------------------------- /app/src/core/java/uistate/SettingsUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package uistate 8 | 9 | data class SettingsUiState ( 10 | val scheduleTime: Float = 15f, 11 | val executeOnlyOnWifi: Boolean = false, 12 | val includeQueryClients: Boolean = false, 13 | val syncWearable: Boolean = false, 14 | 15 | val ip: String = "", 16 | val username: String = "", 17 | val password: String = "", 18 | val queryPort: Int = 10011, 19 | val virtualServerId: Int = 1, 20 | 21 | val connectionSuccessful: Boolean? = null, 22 | val foundWearable: Boolean = false 23 | ) -------------------------------------------------------------------------------- /app/src/core/java/wear/WearDataManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package wear 8 | 9 | import android.content.Context 10 | import com.blazecode.tsviewer.data.ErrorCode 11 | import com.blazecode.tsviewer.data.TsClient 12 | import com.google.android.gms.common.api.ApiException 13 | import com.google.android.gms.wearable.CapabilityClient 14 | import com.google.android.gms.wearable.Wearable 15 | import com.google.gson.GsonBuilder 16 | import com.google.gson.reflect.TypeToken 17 | import data.WearDataPackage 18 | import kotlinx.coroutines.CancellationException 19 | import kotlinx.coroutines.GlobalScope 20 | import kotlinx.coroutines.async 21 | import kotlinx.coroutines.awaitAll 22 | import kotlinx.coroutines.launch 23 | import kotlinx.coroutines.tasks.await 24 | import timber.log.Timber 25 | import java.lang.reflect.Type 26 | 27 | class WearDataManager(context: Context) { 28 | 29 | private val messageClient by lazy { Wearable.getMessageClient(context) } 30 | private val capabilityClient by lazy { Wearable.getCapabilityClient(context) } 31 | 32 | companion object { 33 | private const val WEAR_CAPABILITY = "wear" 34 | private const val CLIENTS_PATH = "/clients" 35 | private const val SERVICE_STATUS_PATH = "/service_status" 36 | private const val ERROR_CODE_PATH = "/error_code" 37 | private const val TOAST_PATH = "/toast" 38 | } 39 | 40 | fun sendClientList(list: MutableList, code: ErrorCode){ 41 | val gson = GsonBuilder().create() 42 | val gsonType: Type = object : TypeToken() {}.type 43 | 44 | val json = gson.toJson(WearDataPackage(list, System.currentTimeMillis()), gsonType) 45 | send(CLIENTS_PATH, json) 46 | send(ERROR_CODE_PATH, code.toString()) 47 | Timber.d("Sent data to wear: $json, $code") 48 | } 49 | 50 | fun sendToastMessage(message: String) { 51 | send(TOAST_PATH, message) 52 | } 53 | 54 | fun sendServiceStatus(isRunning: Boolean) { 55 | send(SERVICE_STATUS_PATH, isRunning.toString()) 56 | if (!isRunning) send(ERROR_CODE_PATH, ErrorCode.NO_ERROR.toString()) 57 | } 58 | 59 | suspend fun areNodesAvailable(): Boolean{ 60 | try { 61 | val nodes = capabilityClient 62 | .getCapability(WEAR_CAPABILITY, CapabilityClient.FILTER_REACHABLE) 63 | .await() 64 | .nodes 65 | return nodes.isNotEmpty() 66 | } catch (e: ApiException){ 67 | Timber.e("Error checking for nodes: $e") 68 | return false 69 | } 70 | } 71 | 72 | private fun send(path: String, data: String) { 73 | GlobalScope.launch { 74 | try { 75 | val nodes = capabilityClient 76 | .getCapability(WEAR_CAPABILITY, CapabilityClient.FILTER_REACHABLE) 77 | .await() 78 | .nodes 79 | 80 | // Send a message to all nodes in parallel 81 | nodes.map { node -> 82 | async { 83 | messageClient.sendMessage(node.id, path, data.toByteArray()) 84 | .await() 85 | } 86 | }.awaitAll() 87 | 88 | Timber.i("Starting activity requests sent successfully") 89 | } catch (cancellationException: CancellationException) { 90 | throw cancellationException 91 | } catch (exception: Exception) { 92 | Timber.i("Error sending start activity requests: $exception") 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/core/java/wear/WearableListenerService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package wear 8 | 9 | import android.content.Context 10 | import android.content.Intent 11 | import androidx.work.Data 12 | import androidx.work.OneTimeWorkRequestBuilder 13 | import androidx.work.WorkManager 14 | import androidx.work.WorkRequest 15 | import com.blazecode.tsviewer.MainActivity 16 | import com.blazecode.tsviewer.R 17 | import com.blazecode.tsviewer.util.ServiceManager 18 | import com.google.android.gms.wearable.MessageEvent 19 | import com.google.android.gms.wearable.WearableListenerService 20 | import util.ClientsWorker 21 | 22 | class WearableListenerService: WearableListenerService() { 23 | 24 | companion object { 25 | private const val LAUNCH_PATH = "/start-activity" 26 | private const val REQUEST_REFRESH = "/request-refresh" 27 | private const val START_SERVICE = "/start-service" 28 | } 29 | 30 | override fun onMessageReceived(messageEvent: MessageEvent) { 31 | super.onMessageReceived(messageEvent) 32 | 33 | when (messageEvent.path) { 34 | LAUNCH_PATH -> { 35 | startActivity(this@WearableListenerService) 36 | } 37 | REQUEST_REFRESH -> { 38 | refreshRequest(this@WearableListenerService) 39 | } 40 | START_SERVICE -> { 41 | startService() 42 | } 43 | } 44 | } 45 | 46 | private fun startActivity(context: Context) { 47 | context.startActivity( 48 | Intent(context, MainActivity::class.java) 49 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 50 | ) 51 | } 52 | 53 | private fun refreshRequest(context: Context){ 54 | var workManager: WorkManager = context.let { WorkManager.getInstance(context) } 55 | 56 | val data = Data.Builder() 57 | data.putBoolean("suppress_db", true) 58 | 59 | val oneTimeclientWorkRequest: WorkRequest = OneTimeWorkRequestBuilder() 60 | .setInputData(data.build()) 61 | .build() 62 | 63 | workManager.enqueue(oneTimeclientWorkRequest) 64 | } 65 | 66 | private fun startService(){ 67 | val serviceManager = ServiceManager(this.application) 68 | if(serviceManager.isRunning()){ 69 | refreshRequest(this@WearableListenerService) 70 | WearDataManager(this).sendServiceStatus(true) 71 | WearDataManager(this).sendToastMessage(this.resources.getString(R.string.service_already_running_please_wait)) 72 | } else { 73 | serviceManager.startService() 74 | 75 | WearDataManager(this).sendServiceStatus(serviceManager.isRunning()) 76 | WearDataManager(this).sendToastMessage(this.resources.getString(R.string.service_started)) 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /app/src/foss/java/uistate/SettingsUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package uistate 8 | 9 | data class SettingsUiState ( 10 | val scheduleTime: Float = 15f, 11 | val executeOnlyOnWifi: Boolean = false, 12 | val includeQueryClients: Boolean = false, 13 | 14 | val ip: String = "", 15 | val username: String = "", 16 | val password: String = "", 17 | val queryPort: Int = 10011, 18 | val virtualServerId: Int = 1, 19 | 20 | val connectionSuccessful: Boolean? = null 21 | ) -------------------------------------------------------------------------------- /app/src/foss/java/viewmodels/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package viewmodels 8 | 9 | import android.app.Application 10 | import androidx.lifecycle.AndroidViewModel 11 | import androidx.lifecycle.viewModelScope 12 | import com.blazecode.tsviewer.data.TsChannel 13 | import com.blazecode.tsviewer.database.DatabaseManager 14 | import com.blazecode.tsviewer.uistate.HomeUiState 15 | import com.blazecode.tsviewer.util.ConnectionManager 16 | import com.blazecode.tsviewer.util.DemoModeValues 17 | import com.blazecode.tsviewer.util.ServiceManager 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.flow.MutableStateFlow 20 | import kotlinx.coroutines.flow.StateFlow 21 | import kotlinx.coroutines.flow.asStateFlow 22 | import kotlinx.coroutines.launch 23 | import util.SettingsManager 24 | 25 | class HomeViewModel(val app: Application) : AndroidViewModel(app) { 26 | 27 | private val settingsManager = SettingsManager(app) 28 | private val serviceManager = ServiceManager(app) 29 | 30 | // UI STATE 31 | private val _uiState = MutableStateFlow(HomeUiState()) 32 | val uiState: StateFlow = _uiState.asStateFlow() 33 | 34 | init { 35 | if(!settingsManager.isDemoModeActive()){ 36 | // DEFAULT OPERATION 37 | _uiState.value = _uiState.value.copy(serviceRunning = serviceManager.isRunning()) 38 | _uiState.value = _uiState.value.copy(debug_updateAvailable = isDebugUpdateActive()) 39 | 40 | if(areCredentialsSet()){ 41 | viewModelScope.launch { 42 | _uiState.value = _uiState.value.copy(channels = getChannels()) 43 | } 44 | _uiState.value = _uiState.value.copy(areCredentialsSet = true) 45 | } 46 | 47 | viewModelScope.launch { 48 | _uiState.value = _uiState.value.copy(lastUpdate = getLastUpdate()) 49 | } 50 | } else { 51 | // DEMO MODE 52 | _uiState.value = _uiState.value.copy(serviceRunning = true) 53 | _uiState.value = _uiState.value.copy(lastUpdate = 5) 54 | _uiState.value = _uiState.value.copy(areCredentialsSet = true) 55 | _uiState.value = _uiState.value.copy(channels = DemoModeValues.channels()) 56 | } 57 | } 58 | 59 | // SETTERS 60 | fun setRunService(serviceRunning: Boolean){ 61 | _uiState.value = _uiState.value.copy(serviceRunning = serviceRunning) 62 | 63 | if(serviceRunning){ 64 | serviceManager.startService() 65 | } else { 66 | serviceManager.stopService() 67 | } 68 | _uiState.value = _uiState.value.copy(serviceRunning = serviceManager.isRunning()) 69 | } 70 | 71 | private suspend fun getChannels(): MutableList { 72 | var tempChannels = mutableListOf() 73 | val job = viewModelScope.launch { 74 | val connectionmanager = ConnectionManager(app) 75 | tempChannels = connectionmanager.getChannels(settingsManager.getConnectionDetails()) 76 | } 77 | job.join() 78 | return tempChannels 79 | } 80 | 81 | // GETTERS 82 | private fun areCredentialsSet(): Boolean { 83 | return settingsManager.areCredentialsSet() 84 | } 85 | 86 | private fun isDebugUpdateActive(): Boolean { 87 | return settingsManager.isDebugUpdateActive() 88 | } 89 | 90 | private suspend fun getLastUpdate(): Long { 91 | var timestamp: Long = 0 92 | val job = viewModelScope.launch(Dispatchers.IO) { 93 | val databaseManager = DatabaseManager(app) 94 | timestamp = databaseManager.getTimestampOfLastUpdate() 95 | } 96 | job.join() 97 | return if(timestamp == 0L) 0 else (System.currentTimeMillis() - timestamp) / 1000 / 60 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/foss/java/viewmodels/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package viewmodels 8 | 9 | import android.app.Application 10 | import androidx.lifecycle.AndroidViewModel 11 | import androidx.lifecycle.viewModelScope 12 | import uistate.SettingsUiState 13 | import com.blazecode.tsviewer.util.ConnectionManager 14 | import util.SettingsManager 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.StateFlow 17 | import kotlinx.coroutines.flow.asStateFlow 18 | import kotlinx.coroutines.launch 19 | 20 | class SettingsViewModel(val app: Application) : AndroidViewModel(app){ 21 | 22 | private val settingsManager = SettingsManager(app) 23 | 24 | // UI STATE 25 | private val _uiState = MutableStateFlow(SettingsUiState()) 26 | val uiState: StateFlow = _uiState.asStateFlow() 27 | 28 | // INIT 29 | init { 30 | if(!settingsManager.isDemoModeActive()){ 31 | // NORMAL OPERATION 32 | loadSettings() 33 | } else { 34 | // DEMO MODE 35 | _uiState.value = settingsManager.getSettingsDemoUiState() 36 | _uiState.value = _uiState.value.copy( 37 | connectionSuccessful = true 38 | ) 39 | } 40 | } 41 | 42 | // NETWORK 43 | fun testConnection(){ 44 | val connectionManager = ConnectionManager(app) 45 | val connectionResult = connectionManager.testConnection( 46 | ip = uiState.value.ip, 47 | username = uiState.value.username, 48 | password = uiState.value.password, 49 | queryPort = uiState.value.queryPort, 50 | virtualServerId = uiState.value.virtualServerId) 51 | 52 | _uiState.value = _uiState.value.copy(connectionSuccessful = connectionResult) 53 | } 54 | 55 | // SETTERS 56 | fun setScheduleTime(scheduleTime: Float){ 57 | _uiState.value = _uiState.value.copy(scheduleTime = scheduleTime) 58 | saveSettings() 59 | } 60 | 61 | fun setExecuteOnlyOnWifi(executeOnlyOnWifi: Boolean){ 62 | _uiState.value = _uiState.value.copy(executeOnlyOnWifi = executeOnlyOnWifi) 63 | saveSettings() 64 | } 65 | 66 | fun setIncludeQueryClients(includeQueryClients: Boolean){ 67 | _uiState.value = _uiState.value.copy(includeQueryClients = includeQueryClients) 68 | saveSettings() 69 | } 70 | 71 | fun setIp(ip: String){ 72 | _uiState.value = _uiState.value.copy(ip = ip, connectionSuccessful = null) 73 | saveSettings() 74 | } 75 | 76 | fun setUsername(username: String){ 77 | _uiState.value = _uiState.value.copy(username = username, connectionSuccessful = null) 78 | saveSettings() 79 | } 80 | 81 | fun setPassword(password: String){ 82 | _uiState.value = _uiState.value.copy(password = password, connectionSuccessful = null) 83 | saveSettings() 84 | } 85 | 86 | fun setQueryPort(queryPort: Int){ 87 | _uiState.value = _uiState.value.copy(queryPort = queryPort, connectionSuccessful = null) 88 | saveSettings() 89 | } 90 | 91 | fun setVirtualServerId(virtualServerId: Int){ 92 | _uiState.value = _uiState.value.copy(virtualServerId = virtualServerId, connectionSuccessful = null) 93 | saveSettings() 94 | } 95 | 96 | // SETTINGS 97 | private fun loadSettings(){ 98 | val settingsManager = SettingsManager(app) 99 | _uiState.value = settingsManager.getSettingsUiState() 100 | } 101 | 102 | private fun saveSettings(){ 103 | val settingsManager = SettingsManager(app) 104 | settingsManager.saveSettingsUiState(_uiState.value) 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/TSViewerApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer 8 | 9 | import android.app.Application 10 | import android.content.Context 11 | import com.blazecode.tsviewer.util.errors.ErrorReportActivity 12 | import org.acra.config.coreConfiguration 13 | import org.acra.config.dialog 14 | import org.acra.config.mailSender 15 | import org.acra.data.StringFormat 16 | import org.acra.ktx.initAcra 17 | 18 | 19 | class TSViewerApplication: Application() { 20 | override fun attachBaseContext(base: Context) { 21 | super.attachBaseContext(base) 22 | 23 | initAcra{ 24 | coreConfiguration { 25 | withBuildConfigClass(BuildConfig::class.java) 26 | withReportFormat(StringFormat.JSON) 27 | } 28 | mailSender { 29 | mailTo = resources.getString(R.string.email_address) 30 | reportAsFile = true 31 | reportFileName = "error_report.json" 32 | subject = "TSViewer - Error Report" 33 | } 34 | dialog { 35 | enabled = true 36 | reportDialogClass = ErrorReportActivity::class.java 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/data/ConnectionDetails.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.data 8 | 9 | data class ConnectionDetails( 10 | val ip: String, 11 | val username: String, 12 | val password: String, 13 | val includeQueryClients: Boolean, 14 | val port: Int, 15 | val virtualServerId: Int 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/data/Entry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.data 8 | 9 | import com.patrykandpatrick.vico.core.entry.ChartEntry 10 | 11 | class Entry( 12 | override val x: Float, 13 | override val y: Float, 14 | val tsServerInfo: TsServerInfo 15 | ) : ChartEntry { 16 | override fun withY(y: Float) = Entry(x, y, tsServerInfo) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/data/ErrorCode.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.data 2 | 3 | enum class ErrorCode { 4 | NO_ERROR, 5 | NO_WIFI, 6 | NO_NETWORK, 7 | AIRPLANE_MODE 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/data/TsChannel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.data 8 | 9 | data class TsChannel( 10 | val name: String, 11 | val members: MutableList = mutableListOf() 12 | ){ 13 | fun isEmpty() : Boolean { return members.isEmpty() } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/data/TsClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.data 8 | 9 | import androidx.room.Entity 10 | import androidx.room.PrimaryKey 11 | import java.util.* 12 | 13 | @Entity 14 | data class TsClient( 15 | @PrimaryKey(autoGenerate = true) val id: Int = 0, 16 | val nickname: String, 17 | val isInputMuted: Boolean = false, 18 | val isOutputMuted: Boolean = false, 19 | 20 | val lastSeen: Date = Date(), 21 | val activeConnectionTime: Long = 0, // IN MINUTES 22 | ) 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/data/TsServerInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.data 8 | 9 | import androidx.room.Entity 10 | import androidx.room.PrimaryKey 11 | 12 | @Entity 13 | data class TsServerInfo ( 14 | @PrimaryKey(autoGenerate = true) val id: Int = 0, 15 | val timestamp: Long, 16 | val clients: MutableList 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/database/ClientDAO.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.database 8 | 9 | import androidx.room.* 10 | import com.blazecode.tsviewer.data.TsClient 11 | 12 | @Dao 13 | interface ClientDAO{ 14 | @Query("SELECT * FROM tsclient") 15 | fun getAll(): MutableList 16 | 17 | @Insert(onConflict = OnConflictStrategy.REPLACE) 18 | fun insertClientData(vararg client: TsClient) 19 | 20 | @Query("SELECT * FROM tsclient WHERE nickname = :nickname") 21 | fun getClientByName(nickname: String): TsClient? 22 | 23 | @Delete 24 | fun delete(client: TsClient) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/database/ClientDatabase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.database 8 | 9 | import android.content.Context 10 | import androidx.room.Database 11 | import androidx.room.Room 12 | import androidx.room.RoomDatabase 13 | import androidx.room.TypeConverters 14 | import com.blazecode.tsviewer.data.TsClient 15 | import com.blazecode.tsviewer.util.typeconverters.DateTypeConverter 16 | 17 | @Database( 18 | version = 1, 19 | entities = [TsClient::class], 20 | exportSchema = true 21 | ) 22 | @TypeConverters(DateTypeConverter::class) 23 | abstract class ClientDatabase : RoomDatabase() { 24 | abstract fun clientDao(): ClientDAO 25 | 26 | companion object { 27 | fun build(context: Context) = Room.databaseBuilder(context, ClientDatabase::class.java, "clients").build() 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/database/ClientRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.database 8 | 9 | import android.content.Context 10 | import com.blazecode.tsviewer.data.TsClient 11 | 12 | class ClientRepository(context: Context) { 13 | 14 | val database = ClientDatabase.build(context) 15 | val clientDao = database.clientDao() 16 | 17 | fun getAllClients(): MutableList { 18 | return clientDao.getAll() 19 | } 20 | 21 | fun getClientByName(nickname: String): TsClient? { 22 | return clientDao.getClientByName(nickname) 23 | } 24 | 25 | fun insertClient(client: TsClient) { 26 | clientDao.insertClientData(client) 27 | } 28 | 29 | fun deleteClient(client: TsClient) { 30 | clientDao.delete(client) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/database/DatabaseManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.database 8 | 9 | import android.content.Context 10 | import com.blazecode.tsviewer.data.TsClient 11 | import com.blazecode.tsviewer.data.TsServerInfo 12 | import util.SettingsManager 13 | 14 | class DatabaseManager(context: Context) { 15 | 16 | val clientRepository = ClientRepository(context) 17 | val serverRepository = ServerRepository(context) 18 | val settingsManager = SettingsManager(context) 19 | 20 | fun writeClients(list: MutableList){ 21 | for (client in list) { 22 | val dbClient = clientRepository.getClientByName(client.nickname) 23 | println(client.nickname) 24 | 25 | if (dbClient != null) { 26 | clientRepository.insertClient( 27 | client.copy( 28 | id = dbClient.id, 29 | activeConnectionTime = dbClient.activeConnectionTime + getScheduleTime().toInt() 30 | ) 31 | ) 32 | } else { 33 | clientRepository.insertClient(client) 34 | } 35 | } 36 | } 37 | 38 | fun writeServerInfo(list: MutableList) { 39 | val serverInfo = TsServerInfo( 40 | timestamp = System.currentTimeMillis(), 41 | clients = list 42 | ) 43 | serverRepository.insertServerInfo(serverInfo) 44 | } 45 | 46 | fun getTimestampOfLastUpdate(): Long { 47 | return serverRepository.getTimestampOfLastUpdate() 48 | } 49 | 50 | private fun getScheduleTime(): Float { 51 | return settingsManager.getScheduleTime() 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/database/ServerDAO.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.database 8 | 9 | import androidx.room.* 10 | import com.blazecode.tsviewer.data.TsServerInfo 11 | 12 | @Dao 13 | interface ServerDAO{ 14 | @Query("SELECT * FROM tsserverinfo") 15 | fun getAll(): MutableList 16 | 17 | @Query("SELECT * FROM tsserverinfo ORDER BY timestamp DESC LIMIT 288") // 288 = 3 Days at 15 min interval 18 | fun getLast3Days(): MutableList 19 | 20 | @Insert 21 | fun insertServerInfo(vararg serverInfo: TsServerInfo) 22 | 23 | @Query("SELECT timestamp FROM tsserverinfo ORDER BY timestamp DESC LIMIT 1") 24 | fun getTimestampOfLastUpdate(): Long 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/database/ServerDatabase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.database 8 | 9 | import android.content.Context 10 | import androidx.room.Database 11 | import androidx.room.Room 12 | import androidx.room.RoomDatabase 13 | import androidx.room.TypeConverters 14 | import com.blazecode.tsviewer.data.TsServerInfo 15 | import com.blazecode.tsviewer.util.typeconverters.ClientListTypeConverter 16 | 17 | @Database( 18 | version = 1, 19 | entities = [TsServerInfo::class], 20 | exportSchema = true 21 | ) 22 | @TypeConverters(ClientListTypeConverter::class) 23 | abstract class ServerDatabase : RoomDatabase() { 24 | abstract fun serverDao(): ServerDAO 25 | 26 | companion object { 27 | fun build(context: Context) = Room.databaseBuilder(context, ServerDatabase::class.java, "server-info").build() 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/database/ServerRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.database 8 | 9 | import android.content.Context 10 | import com.blazecode.tsviewer.data.TsServerInfo 11 | 12 | class ServerRepository(context: Context) { 13 | 14 | val database = ServerDatabase.build(context) 15 | val serverDao = database.serverDao() 16 | 17 | fun getServerInfo(): MutableList { 18 | return serverDao.getAll() 19 | } 20 | 21 | fun getLast3Days(): MutableList { 22 | return serverDao.getLast3Days() 23 | } 24 | 25 | fun insertServerInfo(serverInfo: TsServerInfo) { 26 | serverDao.insertServerInfo(serverInfo) 27 | } 28 | 29 | fun getTimestampOfLastUpdate(): Long { 30 | return serverDao.getTimestampOfLastUpdate() 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/navigation/NavBarItem.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.navigation 8 | 9 | import com.blazecode.tsviewer.R 10 | 11 | sealed class NavBarItem(val title: String, val icon: Int, val route: String) { 12 | object Home: NavBarItem("Home", R.drawable.ic_home, NavRoutes.Home.route) 13 | object Data: NavBarItem("Data", R.drawable.ic_insights, NavRoutes.Data.route) 14 | object Settings: NavBarItem("Settings", R.drawable.ic_settings, NavRoutes.Settings.route) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/navigation/NavRoutes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.navigation 8 | 9 | sealed class NavRoutes(val route: String) { 10 | object Home: NavRoutes("home") 11 | object Data: NavRoutes("data") 12 | object Settings: NavRoutes("settings") 13 | object About: NavRoutes("about") 14 | object Introduction: NavRoutes("introduction") 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.ui.theme 8 | 9 | import android.app.Activity 10 | import android.os.Build 11 | import androidx.compose.foundation.isSystemInDarkTheme 12 | import androidx.compose.material3.* 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.SideEffect 15 | import androidx.compose.ui.graphics.toArgb 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.platform.LocalView 18 | import androidx.core.view.ViewCompat 19 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 20 | 21 | private val DarkColorScheme = darkColorScheme( 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | ) 26 | 27 | @Composable 28 | fun TSViewerTheme( 29 | darkTheme: Boolean = isSystemInDarkTheme(), 30 | dynamicColor: Boolean = true, 31 | content: @Composable () -> Unit 32 | ) { 33 | // NEEDED FOR ICON COLOR 34 | val systemUiController = rememberSystemUiController() 35 | 36 | val colorScheme = when { 37 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 38 | val context = LocalContext.current 39 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 40 | } 41 | darkTheme -> DarkColorScheme 42 | else -> LightColorScheme 43 | } 44 | val view = LocalView.current 45 | if (!view.isInEditMode) { 46 | SideEffect { 47 | (view.context as Activity).window.statusBarColor = colorScheme.background.toArgb() 48 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 49 | 50 | systemUiController.statusBarDarkContentEnabled = !darkTheme 51 | } 52 | } 53 | 54 | MaterialTheme( 55 | colorScheme = colorScheme, 56 | typography = Typography, 57 | content = content 58 | ) 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.ui.theme 8 | 9 | import androidx.compose.material3.Typography 10 | import androidx.compose.ui.text.TextStyle 11 | import androidx.compose.ui.text.font.FontFamily 12 | import androidx.compose.ui.text.font.FontWeight 13 | import androidx.compose.ui.unit.sp 14 | 15 | val Typography = Typography( 16 | bodyLarge = TextStyle( 17 | fontFamily = FontFamily.Default, 18 | fontWeight = FontWeight.Normal, 19 | fontSize = 16.sp, 20 | lineHeight = 24.sp, 21 | letterSpacing = 0.5.sp 22 | ), 23 | titleSmall = TextStyle( 24 | fontFamily = FontFamily.Default, 25 | fontWeight = FontWeight.Bold, 26 | fontSize = 16.sp, 27 | lineHeight = 24.sp, 28 | letterSpacing = 0.5.sp 29 | ), 30 | titleMedium = TextStyle( 31 | fontFamily = FontFamily.Default, 32 | fontWeight = FontWeight.Normal, 33 | fontSize = 22.sp, 34 | lineHeight = 25.sp, 35 | letterSpacing = 0.15.sp 36 | ), 37 | titleLarge = TextStyle( 38 | fontFamily = FontFamily.Default, 39 | fontWeight = FontWeight.Normal, 40 | fontSize = 22.sp, 41 | lineHeight = 25.sp, 42 | letterSpacing = 0.15.sp 43 | ), 44 | headlineMedium = TextStyle( 45 | fontFamily = FontFamily.Default, 46 | fontWeight = FontWeight.Normal, 47 | fontSize = 35.sp, 48 | lineHeight = 40.sp, 49 | letterSpacing = 0.15.sp 50 | ) 51 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/uistate/AboutUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.uistate 8 | 9 | data class AboutUiState ( 10 | val versionName: String = "", 11 | val versionCode: Int = 0 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/uistate/DataUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.uistate 8 | 9 | import com.blazecode.tsviewer.data.TsClient 10 | import com.blazecode.tsviewer.data.TsServerInfo 11 | 12 | data class DataUiState( 13 | val serverInfoList: MutableList = mutableListOf(), 14 | val clientList: MutableList? = null, 15 | 16 | var isClientInfoSheetVisible: Boolean = false, 17 | var clientInfoSheetClient: TsClient? = null, 18 | ) 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/uistate/HomeUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.uistate 8 | 9 | import com.blazecode.tsviewer.data.TsChannel 10 | 11 | data class HomeUiState ( 12 | val serviceRunning: Boolean = false, 13 | val lastUpdate: Long = 0, 14 | val channels: List = listOf(), 15 | val areCredentialsSet: Boolean = false, 16 | val debug_forceNoCredentials: Boolean = false, 17 | val debug_updateAvailable: Boolean = false 18 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/uistate/IntroductionUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.uistate 8 | 9 | data class IntroductionUiState( 10 | val isBatteryOptimizationActive : Boolean = true, 11 | val placedQsTile : Boolean = false 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/DemoModeValues.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.util 8 | 9 | import com.blazecode.tsviewer.data.TsChannel 10 | import com.blazecode.tsviewer.data.TsClient 11 | import com.blazecode.tsviewer.data.TsServerInfo 12 | import java.util.Date 13 | 14 | object DemoModeValues { 15 | 16 | fun serverInfoList(): MutableList{ 17 | return mutableListOf( 18 | TsServerInfo(timestamp = 1620000000000, clients = mutableListOf(TsClient(nickname = "Cocktail"))), 19 | TsServerInfo(timestamp = 1620003600000, clients = mutableListOf(TsClient(nickname = "Cocktail"))), 20 | TsServerInfo(timestamp = 1620007200000, clients = mutableListOf(TsClient(nickname = "Cocktail"), TsClient(nickname = "Cosmo"))), 21 | TsServerInfo(timestamp = 1620010800000, clients = mutableListOf(TsClient(nickname = "Cocktail"), TsClient(nickname = "Cosmo"))), 22 | TsServerInfo(timestamp = 1620014400000, clients = mutableListOf(TsClient(nickname = "Cocktail"), TsClient(nickname = "Cosmo"), TsClient(nickname = "Dangle"))), 23 | TsServerInfo(timestamp = 1620018000000, clients = mutableListOf(TsClient(nickname = "Cocktail"), TsClient(nickname = "Cosmo"), TsClient(nickname = "Dangle"))), 24 | TsServerInfo(timestamp = 1620021600000, clients = mutableListOf(TsClient(nickname = "Cocktail"), TsClient(nickname = "Cosmo"), TsClient(nickname = "Dangle"))), 25 | TsServerInfo(timestamp = 1620025200000, clients = mutableListOf(TsClient(nickname = "Cocktail"), TsClient(nickname = "Cosmo"), TsClient(nickname = "Dangle"))), 26 | TsServerInfo(timestamp = 1620028800000, clients = mutableListOf(TsClient(nickname = "Cocktail"), TsClient(nickname = "Cosmo"))), 27 | TsServerInfo(timestamp = 1620032400000, clients = mutableListOf()), 28 | ) 29 | } 30 | 31 | fun clientList(): MutableList { 32 | return mutableListOf( 33 | TsClient(nickname = "Cocktail", lastSeen = Date(1680512400000), activeConnectionTime = 480), 34 | TsClient(nickname = "Cosmo"), 35 | TsClient(nickname = "Dangle"), 36 | TsClient(nickname = "Commando"), 37 | TsClient(nickname = "SnoopWoot") 38 | ) 39 | } 40 | 41 | fun channels(): MutableList { 42 | return mutableListOf( 43 | TsChannel(name = "[cspacer]Rules"), 44 | TsChannel(name = "[*spacer1]_"), 45 | TsChannel(name = "[cspacer]Welcome"), 46 | TsChannel(name = "[*spacer2]_"), 47 | TsChannel(name = "Channel 1"), 48 | TsChannel(name = "Channel 2", 49 | members = mutableListOf( 50 | TsClient(nickname = "Cocktail"), 51 | TsClient(nickname = "Cosmo"), 52 | TsClient(nickname = "Dangle", isInputMuted = true) 53 | )), 54 | TsChannel(name = "Channel 3", 55 | members = mutableListOf( 56 | TsClient(nickname = "Commando"), 57 | TsClient(nickname = "SnoopWoot") 58 | )), 59 | TsChannel(name = "Channel 4"), 60 | TsChannel(name = "Channel 5"), 61 | TsChannel(name = "[*spacer3]_"), 62 | TsChannel(name = "[cspacer]AFK"), 63 | TsChannel(name = "[*spacer4]_") 64 | ) 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/ErrorHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.util 8 | 9 | import android.content.Context 10 | import android.net.ConnectivityManager 11 | import android.net.NetworkCapabilities 12 | import android.os.Looper 13 | import android.provider.Settings 14 | import android.widget.Toast 15 | import com.blazecode.tsviewer.data.ErrorCode 16 | 17 | class ErrorHandler(val context: Context) { 18 | 19 | private var cutException: String = "" 20 | 21 | fun reportError(exception: String) { 22 | 23 | var code : ErrorCode = ErrorCode.NO_ERROR 24 | 25 | if (isAirplaneMode(context)){ 26 | // AIRPLANE MODE IS ACTIVE 27 | code = ErrorCode.AIRPLANE_MODE 28 | } else if (!hasCellReception()){ 29 | // NO INTERNET CONNECTION 30 | code = ErrorCode.NO_NETWORK 31 | } 32 | 33 | if(code != ErrorCode.NO_ERROR) { 34 | // DISPLAY ERROR MESSAGE 35 | //USED FOR DISPLAYING THE TOAST WITHOUT A VIEW 36 | //CANNOT CREATE MULTIPLE LOOPERS 37 | if (Looper.myLooper() == null) { 38 | Looper.prepare() 39 | } 40 | 41 | //DISPLAY EXCEPTION 42 | cutException = when { 43 | exception.contains(">>") -> exception.split(">>")[1].trim() 44 | exception.contains("Exception:") -> exception.split("Exception:")[1].trim() 45 | else -> exception 46 | } 47 | Toast.makeText(context, cutException, Toast.LENGTH_LONG).show() 48 | println(exception) 49 | } 50 | } 51 | 52 | private fun hasCellReception() : Boolean{ 53 | val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 54 | val network = connectivityManager.activeNetwork ?: return false 55 | val activeNetwork = connectivityManager.getNetworkCapabilities(network) ?: return false 56 | return activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) 57 | } 58 | 59 | fun isAirplaneMode(context: Context): Boolean { 60 | return Settings.Global.getInt( 61 | context.contentResolver, 62 | Settings.Global.AIRPLANE_MODE_ON, 0 63 | ) != 0 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/LinkUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.scrapguidev2.util 8 | 9 | import android.content.Context 10 | import android.content.Intent 11 | import android.net.Uri 12 | import android.util.Log 13 | import android.widget.Toast 14 | import timber.log.Timber 15 | 16 | // TUTORIAL 17 | // https://www.baeldung.com/kotlin/builder-pattern 18 | 19 | private var aboutLinkFailed = false 20 | 21 | class LinkUtil private constructor( 22 | val context: Context, 23 | val link: String 24 | ) { 25 | 26 | data class Builder( 27 | var context: Context, 28 | var link: String = "null" 29 | ) { 30 | 31 | fun context(context: Context) = apply { this.context = context } 32 | fun link(link: String) = apply { this.link = link } 33 | fun open() = LinkUtil(context, link).openLink(this) 34 | } 35 | 36 | fun openLink(builder: Builder) { 37 | try { 38 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(builder.link)) 39 | browserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 40 | builder.context.startActivity(browserIntent) 41 | } catch (e: Exception) { 42 | Timber.log(Log.ERROR, "LinkUtil Link Failed: $e") 43 | if (!aboutLinkFailed) { 44 | Toast.makeText( 45 | builder.context, 46 | "No Browser found\nTap again to see Error Log", 47 | Toast.LENGTH_SHORT 48 | ).show() 49 | aboutLinkFailed = true 50 | } else { 51 | Toast.makeText(builder.context, "No Browser found\n\n$e", Toast.LENGTH_LONG).show() 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/ServiceManager.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.util 2 | 3 | import android.app.Application 4 | import androidx.work.ExistingPeriodicWorkPolicy 5 | import androidx.work.OneTimeWorkRequestBuilder 6 | import androidx.work.PeriodicWorkRequest 7 | import androidx.work.PeriodicWorkRequestBuilder 8 | import androidx.work.WorkInfo 9 | import androidx.work.WorkManager 10 | import androidx.work.WorkRequest 11 | import com.google.common.util.concurrent.ListenableFuture 12 | import util.ClientsWorker 13 | import util.SettingsManager 14 | import java.util.concurrent.ExecutionException 15 | import java.util.concurrent.TimeUnit 16 | 17 | class ServiceManager(val app: Application) { 18 | 19 | private val TAG = "scheduleClients" 20 | private var workManager: WorkManager = app.let { WorkManager.getInstance(it) } 21 | private val settingsManager = SettingsManager(app) 22 | 23 | fun startService(){ 24 | val clientWorkRequest: PeriodicWorkRequest = PeriodicWorkRequestBuilder( 25 | settingsManager.getScheduleTime().toLong(), //GIVE NEW WORK TIME 26 | TimeUnit.MINUTES, 27 | 1, TimeUnit.MINUTES) //FLEX TIME INTERVAL 28 | .build() 29 | 30 | val oneTimeclientWorkRequest: WorkRequest = OneTimeWorkRequestBuilder().build() //RUN ONE TIME 31 | workManager.enqueue(oneTimeclientWorkRequest) 32 | workManager.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, clientWorkRequest) //SCHEDULE THE NEXT RUNS 33 | } 34 | 35 | fun stopService(){ 36 | workManager.cancelUniqueWork(TAG) 37 | } 38 | 39 | fun isRunning(): Boolean { 40 | val instance = WorkManager.getInstance(app.applicationContext) 41 | val statuses: ListenableFuture> = instance.getWorkInfosForUniqueWork(TAG) 42 | return try { 43 | var running = false 44 | val workInfoList: List = statuses.get() 45 | for (workInfo in workInfoList) { 46 | val state = workInfo.state 47 | running = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED 48 | } 49 | running 50 | } catch (e: ExecutionException) { 51 | e.printStackTrace() 52 | false 53 | } catch (e: InterruptedException) { 54 | e.printStackTrace() 55 | false 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/notification/ClientNotificationManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.util.notification 8 | 9 | import android.app.NotificationChannel 10 | import android.app.NotificationManager 11 | import android.app.PendingIntent 12 | import android.content.Context 13 | import android.content.Intent 14 | import androidx.core.app.NotificationCompat 15 | import androidx.core.app.NotificationManagerCompat 16 | import com.blazecode.tsviewer.MainActivity 17 | import com.blazecode.tsviewer.R 18 | import com.blazecode.tsviewer.data.TsClient 19 | 20 | class ClientNotificationManager(private val context: Context) { 21 | 22 | val ACTION_REFRESH = "refresh" 23 | 24 | fun post(clientListNames: MutableList){ 25 | val names = clientListNames.map { it.nickname } 26 | 27 | // ON TAP 28 | val notificationIntent = Intent(context, MainActivity::class.java).apply { 29 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 30 | } 31 | val notificationPendingIntent = PendingIntent.getActivity( 32 | context, 0, notificationIntent, 33 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 34 | ) 35 | 36 | // ON ACTION 37 | val refreshIntent = Intent(context, NotificationBroadcastReceiver::class.java).apply { 38 | action = ACTION_REFRESH 39 | putExtra(ACTION_REFRESH, 0) 40 | } 41 | val refreshPendingIntent: PendingIntent = 42 | PendingIntent.getBroadcast(context, 0, refreshIntent, 43 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 44 | ) 45 | 46 | // BUILDER 47 | if (names.isNotEmpty()) { 48 | val builder = NotificationCompat.Builder(context, context.getString(R.string.notificationChannelClientID)) 49 | .setGroup(context.getString(R.string.notificationChannelClientID)) 50 | .setSmallIcon(R.drawable.ic_notification_icon) 51 | .setContentText(names.joinToString()) 52 | .setStyle(NotificationCompat.BigTextStyle() 53 | .bigText(names.joinToString())) 54 | .setPriority(NotificationCompat.PRIORITY_MIN) 55 | .setContentIntent(notificationPendingIntent) 56 | .addAction(R.drawable.ic_notification_icon, context.getString(R.string.notification_action_refresh), 57 | refreshPendingIntent) 58 | 59 | if (names.size == 1) builder.setContentTitle("${names.size} ${context.resources.getString(R.string.client_connected)}") 60 | else builder.setContentTitle("${names.size} ${context.resources.getString(R.string.clients_connected)}") 61 | 62 | with(NotificationManagerCompat.from(context)) { 63 | notify(1, builder.build()) 64 | } 65 | } else { 66 | removeNotification() 67 | } 68 | } 69 | 70 | fun createChannel(){ 71 | val name = context.getString(R.string.notificationChannelClient) 72 | val importance = NotificationManager.IMPORTANCE_MIN 73 | val channel = NotificationChannel(context.getString(R.string.notificationChannelClientID), name, importance).apply{} 74 | // Register the channel with the system 75 | val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 76 | notificationManager.createNotificationChannel(channel) 77 | } 78 | 79 | fun removeNotification(){ 80 | NotificationManagerCompat.from(context).cancel(null, 1) 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/notification/NotificationBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.util.notification 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.work.Data 7 | import androidx.work.OneTimeWorkRequestBuilder 8 | import androidx.work.WorkManager 9 | import androidx.work.WorkRequest 10 | import util.ClientsWorker 11 | 12 | class NotificationBroadcastReceiver : BroadcastReceiver() { 13 | 14 | val ACTION_REFRESH = "refresh" 15 | 16 | override fun onReceive(context: Context, intent: Intent?) { 17 | //REFRESH 18 | if (intent?.action == ACTION_REFRESH) { 19 | var workManager: WorkManager = context.let { WorkManager.getInstance(context) } 20 | 21 | val data = Data.Builder() 22 | data.putBoolean("suppress_db", true) 23 | 24 | val oneTimeclientWorkRequest: WorkRequest = OneTimeWorkRequestBuilder() 25 | .setInputData(data.build()) 26 | .build() 27 | 28 | workManager.enqueue(oneTimeclientWorkRequest) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/tile/ClientTileService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.util.tile 8 | 9 | import android.app.PendingIntent 10 | import android.content.Intent 11 | import android.os.Build 12 | import android.service.quicksettings.Tile 13 | import android.service.quicksettings.TileService 14 | import android.widget.Toast 15 | import androidx.appcompat.app.AppCompatActivity 16 | import com.blazecode.tsviewer.MainActivity 17 | import com.blazecode.tsviewer.R 18 | 19 | 20 | class ClientTileService : TileService() { 21 | 22 | override fun onClick() { 23 | super.onClick() 24 | 25 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 26 | if(Build.VERSION.SDK_INT >= 34){ 27 | val intent = Intent(applicationContext, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 28 | val pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) 29 | startActivityAndCollapse(pi) 30 | } else { 31 | startActivityAndCollapse(Intent(applicationContext, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) 32 | } 33 | } else { 34 | Toast.makeText(this, resources.getString(R.string.not_supported_below_android_ten), Toast.LENGTH_LONG).show() 35 | } 36 | } 37 | 38 | override fun onStartListening() { 39 | super.onStartListening() 40 | 41 | //GET SHARED PREFERENCES 42 | val tileSetting = this.getSharedPreferences("tile", AppCompatActivity.MODE_PRIVATE)!! 43 | val tileActive = tileSetting.getBoolean("stateActive", false) 44 | 45 | //SET STATE 46 | if(tileActive) qsTile.state = Tile.STATE_ACTIVE 47 | else qsTile.state = Tile.STATE_INACTIVE 48 | 49 | //SET SUBTITLE 50 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ 51 | qsTile.subtitle = tileSetting.getString("subtitle", "") 52 | } 53 | 54 | //APPLY 55 | qsTile.updateTile() 56 | } 57 | 58 | override fun onTileAdded() { 59 | super.onTileAdded() 60 | 61 | //ENABLE TILE IF DEVICE IS NEWER OR EQUAL ANDROID 10 62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 63 | qsTile.state = Tile.STATE_INACTIVE 64 | qsTile.updateTile() 65 | } else { 66 | qsTile.label = "Not supported" 67 | qsTile.updateTile() 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/tile/TileManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.util.tile 8 | 9 | import android.content.Context 10 | import android.content.SharedPreferences 11 | import androidx.appcompat.app.AppCompatActivity 12 | import com.blazecode.tsviewer.R 13 | import com.blazecode.tsviewer.data.ErrorCode 14 | import com.blazecode.tsviewer.data.TsClient 15 | 16 | class TileManager(val context: Context) { 17 | 18 | lateinit var tileSettings : SharedPreferences 19 | lateinit var editor : SharedPreferences.Editor 20 | 21 | init { 22 | //INITIALIZE SHARED PREFERENCES 23 | tileSettings = context?.getSharedPreferences("tile", AppCompatActivity.MODE_PRIVATE)!! 24 | editor = tileSettings.edit() 25 | } 26 | 27 | fun post(clientListNames: MutableList) { 28 | //DISABLE TILE WHEN SERVER IS EMPTY 29 | if(clientListNames.isEmpty()) setState(false) 30 | else setState(true) 31 | 32 | //SAVE SUBTITLE 33 | if (clientListNames.size == 1) setSubtitle("${clientListNames.size} ${context.getString(R.string.client)}") 34 | else setSubtitle("${clientListNames.size} ${context.getString(R.string.clients)}") 35 | editor.commit() 36 | } 37 | 38 | fun error (code : ErrorCode){ 39 | setState(false) 40 | when (code) { 41 | ErrorCode.NO_ERROR -> setSubtitle("") 42 | ErrorCode.NO_NETWORK -> setSubtitle(context.getString(R.string.no_network)) 43 | ErrorCode.AIRPLANE_MODE -> setSubtitle(context.getString(R.string.airplane_mode)) 44 | ErrorCode.NO_WIFI -> setSubtitle(context.getString(R.string.no_wifi)) 45 | } 46 | } 47 | 48 | fun setSubtitle(subtitle: String){ 49 | editor.putString("subtitle", subtitle) 50 | editor.commit() 51 | } 52 | 53 | fun setState(state: Boolean) { 54 | editor.putBoolean("stateActive", state) 55 | editor.commit() 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/typeconverters/ClientListTypeConverter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.util.typeconverters 8 | 9 | import androidx.room.TypeConverter 10 | import com.blazecode.tsviewer.data.TsClient 11 | import com.google.gson.GsonBuilder 12 | import java.util.* 13 | 14 | class ClientListTypeConverter { 15 | 16 | @TypeConverter 17 | fun fromList(list: MutableList): String { 18 | val jsonParser = GsonBuilder().create() 19 | return jsonParser.toJson(list) 20 | } 21 | 22 | @TypeConverter 23 | fun toList(string: String): MutableList { 24 | val jsonParser = GsonBuilder().create() 25 | val array = jsonParser.fromJson(string, Array::class.java) 26 | 27 | val list = mutableListOf() 28 | for (item in array){ 29 | list.add(item) 30 | } 31 | return list 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/typeconverters/DateTypeConverter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.util.typeconverters 8 | 9 | import androidx.room.TypeConverter 10 | import java.util.* 11 | 12 | class DateTypeConverter { 13 | 14 | @TypeConverter 15 | fun fromTimestamp(value: Long?): Date? { 16 | return value?.let { Date(it) } 17 | } 18 | 19 | @TypeConverter 20 | fun dateToTimestamp(date: Date?): Long? { 21 | return date?.time?.toLong() 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/updater/GitHubAssets.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.util.updater 8 | 9 | data class GitHubAssets( 10 | val name: String, 11 | val browser_download_url: String 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/util/updater/GitHubRelease.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.util.updater 8 | 9 | data class GitHubRelease( 10 | val tag_name: String, 11 | val body: String, 12 | val assets: ArrayList 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/viewmodels/AboutViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.viewmodels 8 | 9 | import android.app.Application 10 | import android.content.ClipData 11 | import android.content.ClipboardManager 12 | import android.content.Context.CLIPBOARD_SERVICE 13 | import android.widget.Toast 14 | import androidx.lifecycle.AndroidViewModel 15 | import com.blazecode.scrapguidev2.util.LinkUtil 16 | import com.blazecode.tsviewer.BuildConfig 17 | import com.blazecode.tsviewer.R 18 | import com.blazecode.tsviewer.uistate.AboutUiState 19 | import kotlinx.coroutines.flow.MutableStateFlow 20 | import kotlinx.coroutines.flow.StateFlow 21 | import kotlinx.coroutines.flow.asStateFlow 22 | 23 | class AboutViewModel(val app: Application): AndroidViewModel(app) { 24 | 25 | // UI STATE 26 | private val _uiState = MutableStateFlow(AboutUiState()) 27 | val uiState: StateFlow = _uiState.asStateFlow() 28 | 29 | init { 30 | _uiState.value = _uiState.value.copy( 31 | versionName = BuildConfig.VERSION_NAME, 32 | versionCode = BuildConfig.VERSION_CODE 33 | ) 34 | } 35 | 36 | fun openGithubIssues(){ 37 | val issuesUrl = app.resources.getString(R.string.github_issues_url) 38 | LinkUtil.Builder( 39 | context = app, 40 | link = issuesUrl, 41 | ).open() 42 | } 43 | 44 | fun openSource(){ 45 | val sourceUrl = app.resources.getString(R.string.github_source_url) 46 | LinkUtil.Builder( 47 | context = app, 48 | link = sourceUrl, 49 | ).open() 50 | } 51 | 52 | fun helpTranslate(){ 53 | val sourceUrl = app.resources.getString(R.string.translation_url) 54 | LinkUtil.Builder( 55 | context = app, 56 | link = sourceUrl, 57 | ).open() 58 | } 59 | 60 | fun copyVersion(){ 61 | val clipboard: ClipboardManager? = app.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager? 62 | val clip = ClipData.newPlainText("version", _uiState.value.versionName) 63 | clipboard?.setPrimaryClip(clip) 64 | 65 | Toast.makeText(app, app.resources.getString(R.string.copied_version), Toast.LENGTH_SHORT).show() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/viewmodels/DataViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.viewmodels 8 | 9 | import android.app.Application 10 | import androidx.lifecycle.AndroidViewModel 11 | import androidx.lifecycle.viewModelScope 12 | import com.blazecode.tsviewer.data.TsClient 13 | import com.blazecode.tsviewer.data.TsServerInfo 14 | import com.blazecode.tsviewer.database.ClientRepository 15 | import com.blazecode.tsviewer.database.ServerRepository 16 | import com.blazecode.tsviewer.uistate.DataUiState 17 | import com.blazecode.tsviewer.util.DemoModeValues 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.flow.MutableStateFlow 20 | import kotlinx.coroutines.flow.StateFlow 21 | import kotlinx.coroutines.flow.asStateFlow 22 | import kotlinx.coroutines.launch 23 | import util.SettingsManager 24 | 25 | class DataViewModel(val app: Application): AndroidViewModel(app){ 26 | 27 | private val settingsManager = SettingsManager(app) 28 | 29 | // UI STATE 30 | private val _uiState = MutableStateFlow(DataUiState()) 31 | val uiState: StateFlow = _uiState.asStateFlow() 32 | 33 | init { 34 | if(!settingsManager.isDemoModeActive()){ 35 | // NORMAL OPERATION 36 | viewModelScope.launch { 37 | _uiState.value = _uiState.value.copy( 38 | serverInfoList = getServerInfoList(), 39 | clientList = getClientList() 40 | ) 41 | } 42 | } else { 43 | // DEMO MODE 44 | _uiState.value = _uiState.value.copy( 45 | serverInfoList = DemoModeValues.serverInfoList(), 46 | clientList = DemoModeValues.clientList() 47 | ) 48 | } 49 | } 50 | 51 | fun openClientInfoSheet(client: TsClient) { 52 | _uiState.value = _uiState.value.copy( 53 | clientInfoSheetClient = client, 54 | isClientInfoSheetVisible = true 55 | ) 56 | } 57 | 58 | fun closeClientInfoSheet() { 59 | _uiState.value = _uiState.value.copy( 60 | isClientInfoSheetVisible = false, 61 | clientInfoSheetClient = null 62 | ) 63 | } 64 | 65 | private suspend fun getServerInfoList(): MutableList { 66 | var list = mutableListOf() 67 | val job = viewModelScope.launch(Dispatchers.IO) { 68 | val repository = ServerRepository(app) 69 | list = repository.getLast3Days() 70 | } 71 | job.join() 72 | return list 73 | } 74 | 75 | private suspend fun getClientList(): MutableList { 76 | var list = mutableListOf() 77 | val job = viewModelScope.launch(Dispatchers.IO) { 78 | val repository = ClientRepository(app) 79 | list = repository.getAllClients() 80 | list.sortByDescending { it.activeConnectionTime } 81 | } 82 | job.join() 83 | return list 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/viewmodels/IntroductionViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.viewmodels 8 | 9 | import android.app.Application 10 | import android.app.StatusBarManager 11 | import android.content.ComponentName 12 | import android.content.Context 13 | import android.content.Intent 14 | import android.graphics.drawable.Icon 15 | import android.net.Uri 16 | import android.os.Build 17 | import android.os.PowerManager 18 | import android.provider.Settings 19 | import androidx.annotation.RequiresApi 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.lifecycle.AndroidViewModel 22 | import com.blazecode.tsviewer.R 23 | import com.blazecode.tsviewer.uistate.IntroductionUiState 24 | import com.blazecode.tsviewer.util.tile.ClientTileService 25 | import kotlinx.coroutines.flow.MutableStateFlow 26 | import kotlinx.coroutines.flow.StateFlow 27 | import kotlinx.coroutines.flow.asStateFlow 28 | 29 | class IntroductionViewModel(val app: Application) : AndroidViewModel(app) { 30 | 31 | // UI STATE 32 | private val _uiState = MutableStateFlow(IntroductionUiState()) 33 | val uiState: StateFlow = _uiState.asStateFlow() 34 | 35 | init { 36 | checkPermissions() 37 | } 38 | 39 | fun checkPermissions(){ 40 | _uiState.value = _uiState.value.copy( 41 | isBatteryOptimizationActive = isBatteryOptimizationActive(), 42 | ) 43 | } 44 | 45 | // GETTERS 46 | 47 | fun isBatteryOptimizationActive(): Boolean { 48 | val powerManager: PowerManager = app.getSystemService(Context.POWER_SERVICE) as PowerManager 49 | return powerManager.isIgnoringBatteryOptimizations(app.packageName) 50 | } 51 | 52 | // SETTERS 53 | 54 | fun askBatteryOptimization(){ 55 | val intent = Intent() 56 | intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 57 | intent.data = Uri.parse("package:${app.packageName}") 58 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 59 | app.startActivity(intent) 60 | } 61 | 62 | @RequiresApi(Build.VERSION_CODES.TIRAMISU) 63 | fun placeQsTile(){ 64 | val statusBarManager = app.getSystemService(AppCompatActivity.STATUS_BAR_SERVICE) as StatusBarManager 65 | statusBarManager.requestAddTileService( 66 | ComponentName( 67 | app, 68 | ClientTileService::class.java 69 | ), 70 | app.resources.getString(R.string.app_name), 71 | Icon.createWithResource(app, R.drawable.ic_notification_icon), 72 | {}, 73 | {} 74 | ) 75 | 76 | _uiState.value = _uiState.value.copy( 77 | placedQsTile = true, 78 | ) 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/views/BottomNavBar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.views 8 | 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.NavigationBar 11 | import androidx.compose.material3.NavigationBarItem 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.navigation.NavController 17 | import androidx.navigation.compose.currentBackStackEntryAsState 18 | import com.blazecode.tsviewer.navigation.NavBarItem 19 | 20 | 21 | val items = listOf( 22 | NavBarItem.Home, 23 | NavBarItem.Data, 24 | NavBarItem.Settings 25 | ) 26 | 27 | @Composable 28 | fun BottomNavBar(navController: NavController, openDebugMenu : () -> Unit){ 29 | NavigationBar { 30 | val navBackStackEntry by navController.currentBackStackEntryAsState() 31 | val currentRoute = navBackStackEntry?.destination 32 | items.forEach { screen -> 33 | NavigationBarItem( 34 | icon = { Icon(painterResource(screen.icon), screen.title) }, 35 | label = { Text(screen.title) }, 36 | selected = currentRoute?.route == screen.route, 37 | onClick = { 38 | if (currentRoute?.route != screen.route) { 39 | navController.navigate(screen.route) 40 | } else { 41 | openDebugMenu() 42 | } 43 | } 44 | ) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/views/DefaultPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.eventtool.views 8 | 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Row 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.size 16 | import androidx.compose.material3.Card 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.graphics.painter.Painter 24 | import androidx.compose.ui.res.dimensionResource 25 | import androidx.compose.ui.res.painterResource 26 | import androidx.compose.ui.tooling.preview.Preview 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.unit.sp 29 | import com.blazecode.tsviewer.R 30 | 31 | @Composable 32 | fun DefaultPreference( 33 | modifier: Modifier = Modifier, 34 | title: String, 35 | icon: Painter? = null, 36 | summary: String? = null, 37 | onClick: () -> Unit){ 38 | Box(modifier = modifier){ 39 | Card (modifier = Modifier.padding(dimensionResource(R.dimen.medium_padding), dimensionResource(R.dimen.small_padding), dimensionResource(R.dimen.medium_padding), 0.dp).clickable(onClick = onClick)){ 40 | Row (modifier = Modifier.fillMaxWidth().padding(dimensionResource(R.dimen.small_padding)), verticalAlignment = Alignment.CenterVertically){ 41 | if(icon != null){ 42 | Box (modifier = Modifier.size(dimensionResource(R.dimen.icon_button_size)).weight(1f), contentAlignment = Alignment.Center){ 43 | Icon(icon, "") 44 | } 45 | } 46 | Column (modifier = Modifier.weight(6f, true)){ 47 | Text(title, color = MaterialTheme.colorScheme.onSurface, fontSize = 16.sp) 48 | if(!summary.isNullOrEmpty()) Text(summary, fontSize = 15.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | @Preview 56 | @Composable 57 | private fun Preview(){ 58 | Column{ 59 | DefaultPreference(icon = painterResource(R.drawable.ic_settings), title = "title"){} 60 | DefaultPreference(icon = painterResource(R.drawable.ic_settings), title = "title", summary = "summary"){} 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/views/GitHubUpdateCard.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.views 8 | 9 | import androidx.compose.animation.AnimatedVisibility 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Row 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.material3.Button 16 | import androidx.compose.material3.Card 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.IconButton 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.res.dimensionResource 26 | import androidx.compose.ui.res.painterResource 27 | import androidx.compose.ui.res.stringResource 28 | import com.blazecode.tsviewer.R 29 | 30 | @Composable 31 | fun GitHubUpdateCard(title: String, description: String, onClickDownload : () -> Unit){ 32 | val isCardExtended = remember { mutableStateOf(false) } 33 | val expandMoreIcon = painterResource(R.drawable.ic_expand_more) 34 | val expandLessIcon = painterResource(R.drawable.ic_expand_less) 35 | var icon = painterResource(R.drawable.ic_expand_more) 36 | Card (modifier = Modifier.fillMaxWidth().padding(dimensionResource(R.dimen.medium_padding))){ 37 | Column { 38 | Row (verticalAlignment = Alignment.CenterVertically){ 39 | Text(modifier = Modifier.padding(dimensionResource(R.dimen.medium_padding)).weight(2f), 40 | text = title, 41 | softWrap = true 42 | ) 43 | 44 | Box(modifier = Modifier.fillMaxWidth().padding(dimensionResource(R.dimen.medium_padding)).weight(2f), contentAlignment = Alignment.CenterEnd){ 45 | Row { 46 | Button(onClick = { onClickDownload() }) { Text(stringResource(R.string.download)) } 47 | IconButton(onClick = { 48 | isCardExtended.value = !isCardExtended.value 49 | if(isCardExtended.value) icon = expandLessIcon 50 | else icon = expandMoreIcon 51 | }) { Icon(icon, "extend") } 52 | } 53 | } 54 | } 55 | AnimatedVisibility(isCardExtended.value){ 56 | Text(modifier = Modifier.padding(dimensionResource(R.dimen.medium_padding)), text = description) 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/views/PreferenceGroup.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.eventtool.views 8 | 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Surface 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.dimensionResource 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | import com.blazecode.tsviewer.R 24 | 25 | @Composable 26 | fun PreferenceGroup( 27 | modifier: Modifier = Modifier, 28 | title: String, 29 | content: @Composable () -> Unit){ 30 | Box(modifier = modifier){ 31 | Column { 32 | Box(modifier = Modifier.fillMaxWidth().padding(dimensionResource(R.dimen.medium_padding), dimensionResource(R.dimen.medium_padding), 0.dp, 0.dp)){ 33 | Text(text = title, color = MaterialTheme.colorScheme.primary, fontSize = 15.sp) 34 | } 35 | content() 36 | } 37 | } 38 | } 39 | 40 | @Preview 41 | @Composable 42 | private fun Preview(){ 43 | Surface { 44 | PreferenceGroup(title = "GroupName"){ 45 | DefaultPreference(icon = painterResource(R.drawable.ic_settings), title = "title") {} 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/views/SliderPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.eventtool.views 8 | 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.Spacer 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.size 16 | import androidx.compose.material3.Card 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Slider 20 | import androidx.compose.material3.Text 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.graphics.painter.Painter 27 | import androidx.compose.ui.res.dimensionResource 28 | import androidx.compose.ui.res.painterResource 29 | import androidx.compose.ui.tooling.preview.Preview 30 | import androidx.compose.ui.unit.dp 31 | import androidx.compose.ui.unit.sp 32 | import com.blazecode.tsviewer.R 33 | 34 | @Composable 35 | fun SliderPreference( 36 | modifier: Modifier = Modifier, 37 | title: String, 38 | icon: Painter? = null, 39 | value: Float = 0f, 40 | steps: Int = 0, 41 | valueRange: ClosedFloatingPointRange = 0f..1f, 42 | unitSuffix: String = "", 43 | onValueChange: (Float) -> Unit){ 44 | val currentValue = remember { mutableStateOf(value) } 45 | 46 | Box(modifier = modifier){ 47 | Card (modifier = Modifier.padding(dimensionResource(R.dimen.medium_padding), dimensionResource(R.dimen.small_padding), dimensionResource(R.dimen.medium_padding), 0.dp)){ 48 | Row (modifier = Modifier.fillMaxWidth().padding(dimensionResource(R.dimen.small_padding)), verticalAlignment = Alignment.CenterVertically){ 49 | if(icon != null){ 50 | Box (modifier = Modifier.size(dimensionResource(R.dimen.icon_button_size)).weight(1f), contentAlignment = Alignment.Center){ 51 | Icon(icon, "") 52 | } 53 | } 54 | Column (modifier = Modifier.weight(6f, true)){ 55 | Row { 56 | Text(title, color = MaterialTheme.colorScheme.onSurface, fontSize = 16.sp) 57 | Spacer(modifier = Modifier.weight(1f)) 58 | Text("${currentValue.value.toInt()} $unitSuffix", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp) 59 | } 60 | Slider( 61 | value = currentValue.value, 62 | steps = steps, 63 | valueRange = valueRange, 64 | onValueChange = { onValueChange(it); currentValue.value = it }) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | @Preview 72 | @Composable 73 | private fun Preview(){ 74 | Column{ 75 | SliderPreference(icon = painterResource(R.drawable.ic_settings), title = "title"){} 76 | SliderPreference(icon = painterResource(R.drawable.ic_settings), title = "title", steps = 10, value = .5f){} 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/views/SwitchBar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.eventtool.views 8 | 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.material3.Card 15 | import androidx.compose.material3.CardDefaults 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Switch 18 | import androidx.compose.material3.SwitchDefaults 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.alpha 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.res.dimensionResource 26 | import androidx.compose.ui.tooling.preview.Preview 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.unit.sp 29 | import com.blazecode.tsviewer.R 30 | 31 | @Composable 32 | fun SwitchBar( 33 | modifier: Modifier = Modifier, 34 | title: String, 35 | summary: String = "", 36 | checked: Boolean, 37 | onCheckChanged: (Boolean) -> Unit){ 38 | Box(modifier = modifier){ 39 | Card (modifier = Modifier.padding(dimensionResource(R.dimen.medium_padding), dimensionResource(R.dimen.small_padding), dimensionResource(R.dimen.medium_padding), 0.dp), 40 | colors = CardDefaults.cardColors(containerColor = getContainerColor(checked)),){ 41 | Row (modifier = Modifier.fillMaxWidth().padding(dimensionResource(R.dimen.medium_padding)), verticalAlignment = Alignment.CenterVertically){ 42 | Column (modifier = Modifier.weight(5f, true)){ 43 | Text(title, color = getTextColor(checked), fontSize = 18.sp) 44 | if(summary.isNotEmpty()) Text(summary, color = getTextColor(checked), fontSize = 16.sp, modifier = Modifier.alpha(.7f)) 45 | } 46 | Box (modifier = Modifier.fillMaxWidth().padding(0.dp, 0.dp, dimensionResource(R.dimen.medium_padding), 0.dp).weight(1.5f), contentAlignment = Alignment.CenterEnd){ 47 | Switch( 48 | checked = checked, 49 | onCheckedChange = { 50 | onCheckChanged(it) 51 | }, 52 | colors = SwitchDefaults.colors( 53 | checkedTrackColor = MaterialTheme.colorScheme.secondaryContainer, 54 | checkedThumbColor = MaterialTheme.colorScheme.primary, 55 | ) 56 | ) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | @Composable 64 | private fun getContainerColor(checked: Boolean): Color { 65 | return if (checked) MaterialTheme.colorScheme.surfaceTint else MaterialTheme.colorScheme.surfaceVariant 66 | } 67 | 68 | @Composable 69 | private fun getTextColor(checked: Boolean): Color { 70 | return if (checked) MaterialTheme.colorScheme.inverseOnSurface else MaterialTheme.colorScheme.onSurfaceVariant 71 | } 72 | 73 | @Preview 74 | @Composable 75 | private fun Preview(){ 76 | Column{ 77 | SwitchBar(title = "title", checked = true, summary = "summary"){} 78 | SwitchBar(title = "title", checked = false){} 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/blazecode/tsviewer/views/SwitchPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.eventtool.views 8 | 9 | import androidx.compose.animation.ExperimentalAnimationApi 10 | import androidx.compose.foundation.layout.* 11 | import androidx.compose.material3.* 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.painter.Painter 16 | import androidx.compose.ui.res.dimensionResource 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import com.blazecode.tsviewer.R 22 | 23 | @OptIn(ExperimentalAnimationApi::class) 24 | @Composable 25 | fun SwitchPreference( 26 | modifier: Modifier = Modifier, 27 | title: String, 28 | checked: Boolean, 29 | icon: Painter? = null, 30 | summary: String? = null, 31 | switchEnabled: Boolean? = null, 32 | onCheckChanged: (Boolean) -> Unit){ 33 | Box(modifier = modifier){ 34 | Card (modifier = Modifier.padding(dimensionResource(R.dimen.medium_padding), dimensionResource(R.dimen.small_padding), dimensionResource(R.dimen.medium_padding), 0.dp)){ 35 | Row (modifier = Modifier.fillMaxWidth().padding(dimensionResource(R.dimen.small_padding)), verticalAlignment = Alignment.CenterVertically){ 36 | if(icon != null){ 37 | Box (modifier = Modifier.size(dimensionResource(R.dimen.icon_button_size)).weight(1f), contentAlignment = Alignment.Center){ 38 | Icon(icon, "") 39 | } 40 | } 41 | Column (modifier = Modifier.weight(5f, true)){ 42 | Text(title, color = MaterialTheme.colorScheme.onSurface, fontSize = 16.sp) 43 | if(!summary.isNullOrEmpty()) Text(summary, fontSize = 15.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 44 | } 45 | Box (modifier = Modifier.fillMaxWidth().padding(0.dp, 0.dp, dimensionResource(R.dimen.medium_padding), 0.dp).weight(1.5f), contentAlignment = Alignment.CenterEnd){ 46 | Switch(checked = checked, onCheckedChange = onCheckChanged, enabled = switchEnabled ?: true) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | @Preview 54 | @Composable 55 | private fun Preview(){ 56 | Column{ 57 | SwitchPreference(icon = painterResource(R.drawable.ic_settings), title = "title", checked = true){} 58 | SwitchPreference(icon = painterResource(R.drawable.ic_settings), title = "title", checked = false, summary = "summary"){} 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_notification_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 17 | 21 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_battery.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_expand_less.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_expand_more.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_insights.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_ip.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | 23 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | 23 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_licenses.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mail.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic_muted.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notification.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_password.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_port.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_qs_tile.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_query_client.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_speaker_muted.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_suggest.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_translate.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_update.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_user.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_virtual_server_id.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_visibility.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_visibility_off.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_wearable.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_wifi.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/font-v26/inter.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/app/src/main/res/font/inter_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/app/src/main/res/font/inter_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/constants.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | TSViewer 9 | Settings 10 | info@blazecodeapps.com 11 | https://github.com/BlazeCodeDev/TSViewer 12 | https://github.com/BlazeCodeDev/TSViewer/issues/new/choose 13 | https://api.github.com/repos/BlazeCodeDev/TSViewer/releases 14 | https://translate.blazecodeapps.com/engage/tsviewer/ 15 | 16 | ts.youripaddress.com 17 | QueryUser 18 | password 19 | 10011 20 | 1 21 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 50dp 11 | 12 | 13 | 8dp 14 | 16dp 15 | 32dp 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 14 | 15 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/wear.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | mobile 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 12 | 15 | 18 | -------------------------------------------------------------------------------- /app/src/test/java/com/blazecode/tsviewer/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer 8 | 9 | import org.junit.Assert.assertEquals 10 | import org.junit.Test 11 | 12 | /** 13 | * Example local unit test, which will execute on the development machine (host). 14 | * 15 | * See [testing documentation](http://d.android.com/tools/testing). 16 | */ 17 | class ExampleUnitTest { 18 | @Test 19 | fun addition_isCorrect() { 20 | assertEquals(4, 2 + 2) 21 | } 22 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 8 | buildscript { 9 | val aboutLibrariesVersion by extra("11.2.3") 10 | val composeVersion by extra("1.7.4") 11 | val wearComposeVersion by extra("1.3.0-alpha07") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | //NEEDED FOR TOOLTIPS 17 | maven { url = uri("https://jitpack.io") } 18 | //NEEDED FOR ABOUTLIBRARIES 19 | maven { url = uri("https://plugins.gradle.org/m2/")} 20 | } 21 | dependencies { 22 | classpath("com.android.tools.build:gradle:8.6.0") 23 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21") 24 | 25 | //ABOUT LIBRARIES 26 | classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$aboutLibrariesVersion") 27 | } 28 | } 29 | 30 | allprojects { 31 | repositories { 32 | mavenCentral() 33 | google() 34 | maven { url = uri("https://jitpack.io") } 35 | } 36 | } 37 | 38 | plugins { 39 | id("com.google.devtools.ksp") version "2.0.21-1.0.26" apply false 40 | id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false 41 | } -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/full_description.txt: -------------------------------------------------------------------------------- 1 | Eine einfache Android App welche sich in einem vorgegebenen Zeitabstand mit einem TeamSpeak Server verbindet und die Anzahl der verbundenen Clients mit Namen in einer Benachrichtigung anzeigt. Das läuft über einen TS Query Account. Da diese Login Daten sicher sein müssen werden diese ausschließlich lokal und verschlüsselt gespeichert. 2 | 3 | Die App verwendet die neuen Android 12 adaptiven Farben, ist aber auch rückwärtskompatibel. 4 | 5 |
    6 |
  • Automatisierte Abfragen (Einstellbar zwischen 15 und 120 Minuten)
  • 7 |
  • Menge und Namen der verbunden Clients sind in einer Benachrichtigung zu lesen
  • 8 |
  • Menge und Namen der verbunden Clients werden in einer qs tile angezeigt (Android 10+)
  • 9 |
  • Namen und Uhrzeit werden in einer Datenbank hinterlegt und können im Diagramm angezeigt werden
  • 10 |
  • Unterstützung für Material You theming
  • 11 |
  • WearOS Unterstützung (nur core version)
  • 12 |
  • Lokal und verschlüsselt gespeicherte IP und Login Daten
  • 13 |
  • Keine Tracker wie Firebase und Crashlytics
  • 14 |
15 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/short_description.txt: -------------------------------------------------------------------------------- 1 | Einfacher TeamSpeak Client der anzeigt wer mit dem Server verbunden ist. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/title.txt: -------------------------------------------------------------------------------- 1 | TSViewer 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Initial Release 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | Added battery optimization snackbar 2 | Dependency updates 3 | Gradle update 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | This is a simple TeamSpeak client which connects every x minutes to get the connected clients and displays them in a notification and/or quick settings tile. This works by using a Teamspeak Query account to login. That means it is required to enter the ip and login credentials into the app. As this is sensitive, the ip and credentials are saved only locally and encrypted to make sure that other apps cannot scrape the data. 2 | 3 | The app uses the new Material You adaptive colors, but is also backwards compatible to devices below Android 12. 4 | 5 |
    6 |
  • Automated checks running in the background (customizable time between 15 and 120 minutes)
  • 7 |
  • Amount of clients and their nicknames will be written into a notification
  • 8 |
  • Amount of clients will be written in the label of a qs tile (Android 10+)
  • 9 |
  • Amount of clients and time will be added to a database to be later viewed in a graph view
  • 10 |
  • Material You adaptive color theming support
  • 11 |
  • WearOS support (Core flavor only)
  • 12 |
  • Locally stored and encryped IP and login credentials
  • 13 |
  • No trackers like Firebase and Crashlytics
  • 14 |
15 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/dummy: -------------------------------------------------------------------------------- 1 | f 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Simple TS client which shows you the amount and names of connected clients. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | TSViewer 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # /* 3 | # * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | # */ 5 | # 6 | 7 | # Project-wide Gradle settings. 8 | # IDE (e.g. Android Studio) users: 9 | # Gradle settings configured through the IDE *will override* 10 | # any settings specified in this file. 11 | # For more details on how to configure your build environment visit 12 | # http://www.gradle.org/docs/current/userguide/build_environment.html 13 | # Specifies the JVM arguments used for the daemon process. 14 | # The setting is particularly useful for tweaking memory settings. 15 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app"s APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | # Kotlin code style for this project: "official" or "obsolete": 27 | kotlin.code.style=official 28 | android.defaults.buildfeatures.buildconfig=true 29 | android.nonTransitiveRClass=false 30 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlazeCodeDev/TSViewer/ecf16e429cc880b3c6cc4826fd1abbfb9a0e64b3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | # 2 | # /* 3 | # * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | # */ 5 | # 6 | 7 | #Thu Dec 02 15:53:30 CET 2021 8 | distributionBase=GRADLE_USER_HOME 9 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 10 | distributionPath=wrapper/dists 11 | zipStorePath=wrapper/dists 12 | zipStoreBase=GRADLE_USER_HOME 13 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | dependencyResolutionManagement { 8 | repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) 9 | repositories { 10 | google() 11 | maven { url = uri("https://jitpack.io/") } 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "TSViewer" 16 | include (":app") 17 | include (":wear") 18 | -------------------------------------------------------------------------------- /wear/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /wear/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | id("kotlin-parcelize") 5 | id("org.jetbrains.kotlin.plugin.compose") 6 | } 7 | 8 | android { 9 | namespace = "com.blazecode.tsviewer" 10 | compileSdk = 34 11 | 12 | defaultConfig { 13 | applicationId = "com.blazecode.tsviewer" 14 | minSdk = 26 15 | targetSdk = 34 16 | versionCode = 3 17 | versionName = "1.2" 18 | vectorDrawables { 19 | useSupportLibrary = true 20 | } 21 | } 22 | 23 | buildTypes { 24 | getByName("release") { 25 | isMinifyEnabled = false 26 | isShrinkResources = false 27 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility = JavaVersion.VERSION_1_8 32 | targetCompatibility = JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = "1.8" 36 | } 37 | buildFeatures { 38 | compose = true 39 | } 40 | composeOptions { 41 | kotlinCompilerExtensionVersion = "1.4.4" 42 | } 43 | packagingOptions { 44 | resources { 45 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 46 | } 47 | } 48 | } 49 | 50 | val wearComposeVersion: String by rootProject.extra 51 | 52 | dependencies { 53 | implementation(platform("androidx.compose:compose-bom:2023.10.00")) 54 | 55 | implementation("androidx.core:core-ktx:1.12.0") 56 | implementation("com.google.android.gms:play-services-wearable:18.1.0") 57 | implementation("androidx.percentlayout:percentlayout:1.0.0") 58 | implementation("androidx.legacy:legacy-support-v4:1.0.0") 59 | implementation("androidx.compose.ui:ui") 60 | implementation("androidx.wear.compose:compose-material:$wearComposeVersion") 61 | implementation("androidx.wear.compose:compose-foundation:$wearComposeVersion") 62 | implementation("androidx.compose.ui:ui-tooling-preview") 63 | implementation("androidx.activity:activity-compose:1.8.0") 64 | testImplementation("junit:junit:4.13.2") 65 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 66 | debugImplementation("androidx.compose.ui:ui-tooling") 67 | debugImplementation("androidx.compose.ui:ui-test-manifest") 68 | 69 | // COMPLICATIONS 70 | implementation("androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.1") 71 | 72 | //DATA STORE 73 | implementation("androidx.datastore:datastore-preferences:1.0.0") 74 | 75 | // NAVIGATION 76 | implementation("androidx.navigation:navigation-compose:2.7.4") 77 | implementation("com.google.accompanist:accompanist-navigation-animation:0.33.2-alpha") 78 | 79 | // JSON PARSING 80 | implementation("com.google.code.gson:gson:2.10.1") 81 | 82 | // COROUTINES 83 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3") 84 | } -------------------------------------------------------------------------------- /wear/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /wear/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 28 | 29 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 47 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear 8 | 9 | import android.os.Bundle 10 | import androidx.activity.ComponentActivity 11 | import androidx.activity.compose.setContent 12 | import androidx.navigation.compose.NavHost 13 | import androidx.navigation.compose.composable 14 | import androidx.navigation.compose.rememberNavController 15 | import com.blazecode.tsviewer.wear.data.DataHolder 16 | import com.blazecode.tsviewer.wear.enum.ErrorCode 17 | import com.blazecode.tsviewer.wear.navigation.NavRoutes 18 | import com.blazecode.tsviewer.wear.screens.ClientList 19 | import com.blazecode.tsviewer.wear.screens.Error 20 | import com.blazecode.tsviewer.wear.screens.Home 21 | import com.blazecode.tsviewer.wear.screens.ServiceOff 22 | import com.blazecode.tsviewer.wear.viewmodels.ClientListViewModel 23 | import com.blazecode.tsviewer.wear.viewmodels.ErrorViewModel 24 | import com.blazecode.tsviewer.wear.viewmodels.HomeViewModel 25 | import com.blazecode.tsviewer.wear.viewmodels.ServiceOffViewModel 26 | 27 | class MainActivity : ComponentActivity() { 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | 32 | val dataHolder = DataHolder 33 | 34 | setContent { 35 | // CHECK IF COMPLICATION WAS TAPPED 36 | var startDestination: String = NavRoutes.Home.route 37 | 38 | if(intent.extras?.getBoolean("openComplication") != null){ 39 | if(dataHolder.serviceStatus.value == true && dataHolder.errorCode.value == ErrorCode.NO_ERROR) 40 | startDestination = NavRoutes.ClientList.route 41 | else if (dataHolder.serviceStatus.value == false && dataHolder.errorCode.value == ErrorCode.NO_ERROR) 42 | startDestination = NavRoutes.ServiceOffScreen.route 43 | else 44 | startDestination = NavRoutes.ErrorScreen.route 45 | } 46 | 47 | val navController = rememberNavController() 48 | NavHost(navController = navController, startDestination = startDestination) { 49 | composable(NavRoutes.Home.route) { Home(HomeViewModel(application), navController) } 50 | composable(NavRoutes.ClientList.route) { ClientList(ClientListViewModel(application)) } 51 | composable(NavRoutes.ServiceOffScreen.route) { ServiceOff(ServiceOffViewModel(application)) } 52 | composable(NavRoutes.ErrorScreen.route) { Error(ErrorViewModel(application)) } 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/communication/WearDataManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.communication 8 | 9 | import android.content.Context 10 | import android.util.Log 11 | import com.google.android.gms.wearable.CapabilityClient 12 | import com.google.android.gms.wearable.Wearable 13 | import kotlinx.coroutines.CancellationException 14 | import kotlinx.coroutines.GlobalScope 15 | import kotlinx.coroutines.async 16 | import kotlinx.coroutines.awaitAll 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.tasks.await 19 | 20 | class WearDataManager(context: Context) { 21 | 22 | private val messageClient by lazy { Wearable.getMessageClient(context) } 23 | private val capabilityClient by lazy { Wearable.getCapabilityClient(context) } 24 | 25 | fun sendStartActivityRequest() { 26 | send(START_ACTIVITY_PATH) 27 | } 28 | 29 | fun requestRefresh() { 30 | send(REQUEST_REFRESH) 31 | } 32 | 33 | fun startService(){ 34 | send(START_SERVICE) 35 | } 36 | 37 | private fun send(path: String, data: String = "") { 38 | GlobalScope.launch { 39 | try { 40 | val nodes = capabilityClient 41 | .getCapability(MOBILE_CAPABILITY, CapabilityClient.FILTER_REACHABLE) 42 | .await() 43 | .nodes 44 | 45 | // Send a message to all nodes in parallel 46 | nodes.map { node -> 47 | async { 48 | messageClient.sendMessage(node.id, path, data.toByteArray()) 49 | .await() 50 | } 51 | }.awaitAll() 52 | 53 | Log.i("WearDataManager","Starting activity requests sent successfully") 54 | } catch (cancellationException: CancellationException) { 55 | throw cancellationException 56 | } catch (exception: Exception) { 57 | Log.i("WearDataManager","Error sending start activity requests: $exception") 58 | } 59 | } 60 | } 61 | 62 | companion object { 63 | private const val MOBILE_CAPABILITY = "mobile" 64 | private const val START_ACTIVITY_PATH = "/start-activity" 65 | private const val REQUEST_REFRESH = "/request-refresh" 66 | private const val START_SERVICE = "/start-service" 67 | } 68 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/communication/WearableListenerService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.communication 8 | 9 | import android.util.Log 10 | import android.widget.Toast 11 | import com.blazecode.tsviewer.wear.complication.ComplicationProvider 12 | import com.blazecode.tsviewer.wear.data.DataHolder 13 | import com.blazecode.tsviewer.wear.data.WearDataPackage 14 | import com.blazecode.tsviewer.wear.enum.ErrorCode 15 | import com.google.android.gms.wearable.MessageEvent 16 | import com.google.android.gms.wearable.WearableListenerService 17 | import com.google.gson.GsonBuilder 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.GlobalScope 20 | import kotlinx.coroutines.launch 21 | 22 | class WearableListenerService: WearableListenerService() { 23 | 24 | override fun onMessageReceived(messageEvent: MessageEvent) { 25 | super.onMessageReceived(messageEvent) 26 | 27 | when (messageEvent.path) { 28 | CLIENTS_PATH -> { 29 | val gson = GsonBuilder().create() 30 | val data = gson.fromJson(String(messageEvent.data), WearDataPackage::class.java) 31 | 32 | GlobalScope.launch(Dispatchers.Main) { 33 | DataHolder.time.value = data.timestamp 34 | DataHolder.list.value = data.clients.toMutableList() 35 | ComplicationProvider().update(this@WearableListenerService) 36 | } 37 | 38 | Log.i("WearableListenerService", "Received ${data.clients.size} clients") 39 | } 40 | SERVICE_STATUS_PATH -> { 41 | GlobalScope.launch(Dispatchers.Main) { 42 | DataHolder.serviceStatus.value = String(messageEvent.data).toBoolean() 43 | println(String(messageEvent.data).toBoolean()) 44 | ComplicationProvider().update(this@WearableListenerService) 45 | } 46 | } 47 | ERROR_CODE_PATH -> { 48 | GlobalScope.launch(Dispatchers.Main) { 49 | DataHolder.errorCode.value = ErrorCode.valueOf(String(messageEvent.data)) 50 | println(ErrorCode.valueOf(String(messageEvent.data))) 51 | ComplicationProvider().update(this@WearableListenerService) 52 | } 53 | } 54 | TOAST_PATH -> { 55 | Toast.makeText(this, String(messageEvent.data), Toast.LENGTH_SHORT).show() 56 | } 57 | } 58 | } 59 | 60 | companion object { 61 | private const val CLIENTS_PATH = "/clients" 62 | private const val SERVICE_STATUS_PATH = "/service_status" 63 | private const val ERROR_CODE_PATH = "/error_code" 64 | private const val TOAST_PATH = "/toast" 65 | } 66 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/complication/Complication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.complication 8 | 9 | enum class Complication(val key: String){ 10 | ICON("Icon"), 11 | LONG_TEXT("LongText") 12 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/complication/ComplicationArguments.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.complication 8 | 9 | import android.content.ComponentName 10 | import android.os.Parcelable 11 | import kotlinx.parcelize.Parcelize 12 | 13 | @Parcelize 14 | data class ComplicationArguments( 15 | val component: ComponentName, 16 | val complication: Complication, 17 | val complicationId: Int 18 | ) : Parcelable -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/complication/ComplicationProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.complication 8 | 9 | import android.app.PendingIntent 10 | import android.app.TaskStackBuilder 11 | import android.content.ComponentName 12 | import android.content.Context 13 | import android.content.Intent 14 | import android.graphics.drawable.Icon 15 | import androidx.wear.watchface.complications.data.ComplicationData 16 | import androidx.wear.watchface.complications.data.ComplicationType 17 | import androidx.wear.watchface.complications.data.MonochromaticImage 18 | import androidx.wear.watchface.complications.data.PlainComplicationText 19 | import androidx.wear.watchface.complications.data.ShortTextComplicationData 20 | import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService 21 | import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester 22 | import androidx.wear.watchface.complications.datasource.ComplicationRequest 23 | import com.blazecode.tsviewer.R 24 | import com.blazecode.tsviewer.wear.MainActivity 25 | import com.blazecode.tsviewer.wear.data.DataHolder 26 | import com.blazecode.tsviewer.wear.enum.ErrorCode 27 | 28 | class ComplicationProvider: ComplicationDataSourceService() { 29 | 30 | val dataHolder = DataHolder 31 | 32 | override fun getPreviewData(type: ComplicationType): ComplicationData? { 33 | return getComplicationData(null) 34 | } 35 | 36 | override fun onComplicationRequest(request: ComplicationRequest, listener: ComplicationRequestListener) { 37 | 38 | val tapIntent = Intent(this, MainActivity::class.java) 39 | .putExtra("openComplication", true) 40 | val tapPendingIntent: PendingIntent? = TaskStackBuilder.create(this).run { 41 | addNextIntentWithParentStack(tapIntent) 42 | getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE) 43 | } 44 | 45 | listener.onComplicationData(getComplicationData(tapPendingIntent)) 46 | } 47 | 48 | private fun getComplicationData(tapAction: PendingIntent?): ComplicationData{ 49 | val icon = Icon.createWithResource(this, R.drawable.ic_icon) 50 | 51 | val text = if (dataHolder.serviceStatus.value == true && dataHolder.errorCode.value == ErrorCode.NO_ERROR) //true = running, false = !running 52 | dataHolder.list.value?.size.toString() 53 | else if (dataHolder.errorCode.value != ErrorCode.NO_ERROR) 54 | "!" 55 | else 56 | "-" 57 | 58 | return ShortTextComplicationData.Builder( 59 | text = PlainComplicationText.Builder( 60 | text = text 61 | ).build(), 62 | 63 | contentDescription = PlainComplicationText.Builder( 64 | text = "Shows client amount" 65 | ).build(), 66 | ) 67 | .setMonochromaticImage(MonochromaticImage.Builder(icon).build()) 68 | .setTapAction(tapAction).build() 69 | } 70 | 71 | fun update(context: Context){ 72 | ComplicationDataSourceUpdateRequester.create( 73 | context, 74 | complicationDataSourceComponent = ComponentName(context, ComplicationProvider::class.java) 75 | ).requestUpdateAll() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/data/DataHolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.data 8 | 9 | import androidx.lifecycle.MutableLiveData 10 | import com.blazecode.tsviewer.wear.enum.ErrorCode 11 | 12 | object DataHolder { 13 | val list: MutableLiveData> by lazy { 14 | MutableLiveData>() 15 | } 16 | val time: MutableLiveData by lazy { 17 | MutableLiveData() 18 | } 19 | val serviceStatus: MutableLiveData by lazy { 20 | MutableLiveData(false) 21 | } 22 | val errorCode: MutableLiveData by lazy { 23 | MutableLiveData(ErrorCode.NO_ERROR) 24 | } 25 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/data/TsClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.data 8 | 9 | import java.util.Date 10 | 11 | data class TsClient( 12 | val id: Int = 0, 13 | val nickname: String, 14 | val isInputMuted: Boolean = false, 15 | val isOutputMuted: Boolean = false, 16 | 17 | val lastSeen: Date = Date(), 18 | val activeConnectionTime: Long = 0, // IN MINUTES 19 | ) 20 | -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/data/WearDataPackage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.data 8 | 9 | import com.blazecode.tsviewer.wear.enum.ErrorCode 10 | 11 | data class WearDataPackage( 12 | val clients: List, 13 | val timestamp: Long, 14 | val errorCode: ErrorCode? 15 | ) -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/enum/ErrorCode.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.enum 2 | 3 | enum class ErrorCode { 4 | NO_ERROR, 5 | NO_WIFI, 6 | NO_NETWORK, 7 | AIRPLANE_MODE 8 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/navigation/NavRoutes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.navigation 8 | 9 | sealed class NavRoutes(val route: String) { 10 | object Home: NavRoutes("home") 11 | object ClientList: NavRoutes("clientList") 12 | object ServiceOffScreen: NavRoutes("serviceOffScreen") 13 | object ErrorScreen: NavRoutes("errorScreen") 14 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/screens/Error.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.screens 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.focusable 5 | import androidx.compose.foundation.gestures.animateScrollBy 6 | import androidx.compose.foundation.gestures.scrollBy 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.runtime.collectAsState 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.rememberCoroutineScope 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.focus.FocusRequester 17 | import androidx.compose.ui.focus.focusRequester 18 | import androidx.compose.ui.input.rotary.onPreRotaryScrollEvent 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.res.colorResource 21 | import androidx.compose.ui.res.painterResource 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import androidx.lifecycle.viewmodel.compose.viewModel 26 | import androidx.wear.compose.foundation.lazy.ScalingLazyColumn 27 | import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState 28 | import androidx.wear.compose.material.Chip 29 | import androidx.wear.compose.material.ChipDefaults 30 | import androidx.wear.compose.material.Icon 31 | import androidx.wear.compose.material.Text 32 | import com.blazecode.tsviewer.R 33 | import com.blazecode.tsviewer.wear.theme.TSViewerTheme 34 | import com.blazecode.tsviewer.wear.viewmodels.ErrorViewModel 35 | import kotlinx.coroutines.Dispatchers 36 | import kotlinx.coroutines.launch 37 | 38 | @Composable 39 | fun Error(viewModel: ErrorViewModel = viewModel()) { 40 | TSViewerTheme { 41 | MainLayout(viewModel) 42 | } 43 | } 44 | 45 | @Composable 46 | private fun MainLayout(viewModel: ErrorViewModel){ 47 | val uiState = viewModel.uiState.collectAsState() 48 | 49 | val context = LocalContext.current 50 | val scrollState = rememberScalingLazyListState() 51 | val scope = rememberCoroutineScope() 52 | val focusRequester = remember { FocusRequester() } 53 | LaunchedEffect(Unit) { 54 | focusRequester.requestFocus() 55 | } 56 | 57 | ScalingLazyColumn (modifier = Modifier 58 | .onPreRotaryScrollEvent { 59 | scope.launch { 60 | scrollState.scrollBy(it.verticalScrollPixels) 61 | scrollState.animateScrollBy(0f) 62 | } 63 | true 64 | } 65 | .focusRequester(focusRequester) 66 | .focusable() 67 | .fillMaxSize(), 68 | state = scrollState 69 | ){ 70 | 71 | item { Image(painter = painterResource(id = R.drawable.ic_error), contentDescription = "error") } 72 | item { Spacer(modifier = Modifier.size(2.dp)) } 73 | 74 | item { Text(text = uiState.value.error, fontSize = 16.sp) } 75 | item { Spacer(modifier = Modifier.size(8.dp)) } 76 | 77 | item { 78 | Chip(onClick = { scope.launch(Dispatchers.IO) { viewModel.launchApp() }}, 79 | label = { Text(stringResource(R.string.launch_app_on_phone)) }, 80 | icon = { Icon(painterResource(R.drawable.ic_open), contentDescription = null) }, 81 | colors = ChipDefaults.chipColors(backgroundColor = colorResource(R.color.background), iconColor = colorResource(R.color.primary))) } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.wear.compose.material.Colors 5 | 6 | val Purple200 = Color(0xFFBB86FC) 7 | val Purple500 = Color(0xFF6200EE) 8 | val Purple700 = Color(0xFF3700B3) 9 | val Teal200 = Color(0xFF03DAC5) 10 | val Red400 = Color(0xFFCF6679) 11 | 12 | internal val wearColorPalette: Colors = Colors( 13 | primary = Purple200, 14 | primaryVariant = Purple700, 15 | secondary = Teal200, 16 | secondaryVariant = Teal200, 17 | error = Red400, 18 | onPrimary = Color.Black, 19 | onSecondary = Color.Black, 20 | onError = Color.Black 21 | ) -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.wear.compose.material.MaterialTheme 5 | 6 | @Composable 7 | fun TSViewerTheme( 8 | content: @Composable () -> Unit 9 | ) { 10 | MaterialTheme( 11 | colors = wearColorPalette, 12 | typography = Typography, 13 | // For shapes, we generally recommend using the default Material Wear shapes which are 14 | // optimized for round and non-round devices. 15 | content = content 16 | ) 17 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2022. 4 | * 5 | */ 6 | 7 | package com.blazecode.tsviewer.wear.theme 8 | 9 | import androidx.compose.ui.text.TextStyle 10 | import androidx.compose.ui.text.font.FontFamily 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.unit.sp 13 | import androidx.wear.compose.material.Typography 14 | 15 | // Set of Material typography styles to start with 16 | val Typography = Typography( 17 | body1 = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.Normal, 20 | fontSize = 16.sp 21 | ), 22 | body2 = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Bold, 25 | fontSize = 16.sp 26 | ) 27 | 28 | /* Other default text styles to override 29 | button = TextStyle( 30 | fontFamily = FontFamily.Default, 31 | fontWeight = FontWeight.W500, 32 | fontSize = 14.sp 33 | ), 34 | caption = TextStyle( 35 | fontFamily = FontFamily.Default, 36 | fontWeight = FontWeight.Normal, 37 | fontSize = 12.sp 38 | ) 39 | */ 40 | ) -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/uistate/ClientListUiState.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.uistate 2 | 3 | import com.blazecode.tsviewer.wear.data.TsClient 4 | 5 | data class ClientListUiState ( 6 | val clientListString: String = "", 7 | var clientList: MutableList = mutableListOf(), 8 | val time: Long = 0, 9 | val isLoading: Boolean = false 10 | ) -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/uistate/ErrorUiState.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.uistate 2 | 3 | data class ErrorUiState( 4 | val error: String = "", 5 | ) 6 | -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/uistate/HomeUiState.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.uistate 2 | 3 | data class HomeUiState( 4 | val version: String = "" 5 | ) 6 | -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/uistate/ServiceOffUiState.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.uistate 2 | 3 | data class ServiceOffUiState( 4 | val startServiceButtonEnabled: Boolean = true, 5 | ) 6 | -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/viewmodels/ClientListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.viewmodels 2 | 3 | import android.app.Application 4 | import android.os.Build 5 | import android.os.VibrationEffect 6 | import android.os.Vibrator 7 | import androidx.lifecycle.AndroidViewModel 8 | import androidx.lifecycle.Observer 9 | import com.blazecode.tsviewer.wear.communication.WearDataManager 10 | import com.blazecode.tsviewer.wear.data.DataHolder 11 | import com.blazecode.tsviewer.wear.data.TsClient 12 | import com.blazecode.tsviewer.wear.uistate.ClientListUiState 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.StateFlow 15 | import kotlinx.coroutines.flow.asStateFlow 16 | 17 | class ClientListViewModel(val app: Application): AndroidViewModel(app) { 18 | 19 | // UI STATE 20 | private val _uiState = MutableStateFlow(ClientListUiState()) 21 | val uiState: StateFlow = _uiState.asStateFlow() 22 | 23 | init { 24 | loadData() 25 | 26 | val listObserver = Observer> { 27 | loadData() 28 | if(_uiState.value.isLoading) 29 | vibrate() 30 | 31 | _uiState.value = _uiState.value.copy( 32 | isLoading = false 33 | ) 34 | } 35 | DataHolder.list.observeForever(listObserver) 36 | } 37 | 38 | fun loadData(){ 39 | val list = if(DataHolder.list.value == null) mutableListOf() else DataHolder.list.value!! 40 | val time = if(DataHolder.time.value == null) 0 else ((System.currentTimeMillis() - DataHolder.time.value!!) / 1000 / 60) 41 | 42 | _uiState.value = _uiState.value.copy( 43 | clientListString = list.map{ it.nickname }.joinToString(), 44 | clientList = list, 45 | time = time, 46 | ) 47 | } 48 | 49 | fun requestRefresh(){ 50 | _uiState.value = _uiState.value.copy( 51 | isLoading = true 52 | ) 53 | 54 | WearDataManager(app).requestRefresh() 55 | } 56 | 57 | fun vibrate(){ 58 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ 59 | val vibrator = app.getSystemService(Vibrator::class.java) 60 | vibrator.vibrate(VibrationEffect.createOneShot(50, 120)) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/viewmodels/ErrorViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.viewmodels 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import com.blazecode.tsviewer.R 6 | import com.blazecode.tsviewer.wear.communication.WearDataManager 7 | import com.blazecode.tsviewer.wear.data.DataHolder 8 | import com.blazecode.tsviewer.wear.enum.ErrorCode 9 | import com.blazecode.tsviewer.wear.uistate.ErrorUiState 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.StateFlow 12 | import kotlinx.coroutines.flow.asStateFlow 13 | 14 | class ErrorViewModel(val app: Application): AndroidViewModel(app) { 15 | 16 | // UI STATE 17 | private val _uiState = MutableStateFlow(ErrorUiState()) 18 | val uiState: StateFlow = _uiState.asStateFlow() 19 | 20 | init { 21 | var errorString: String 22 | when (DataHolder.errorCode.value) { 23 | ErrorCode.NO_NETWORK -> { 24 | errorString = app.getString(R.string.no_network) 25 | } 26 | ErrorCode.NO_WIFI -> { 27 | errorString = app.getString(R.string.no_wifi) 28 | } 29 | ErrorCode.AIRPLANE_MODE -> { 30 | errorString = app.getString(R.string.airplane_mode) 31 | } 32 | else -> { 33 | errorString = "" 34 | } 35 | } 36 | 37 | _uiState.value = _uiState.value.copy( 38 | error = errorString 39 | ) 40 | } 41 | 42 | fun launchApp(){ 43 | WearDataManager(app).sendStartActivityRequest() 44 | } 45 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/viewmodels/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.viewmodels 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import com.blazecode.tsviewer.BuildConfig 6 | import com.blazecode.tsviewer.wear.communication.WearDataManager 7 | import com.blazecode.tsviewer.wear.uistate.HomeUiState 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.StateFlow 10 | import kotlinx.coroutines.flow.asStateFlow 11 | 12 | class HomeViewModel(val app: Application): AndroidViewModel(app) { 13 | 14 | // UI STATE 15 | private val _uiState = MutableStateFlow(HomeUiState()) 16 | val uiState: StateFlow = _uiState.asStateFlow() 17 | 18 | init { 19 | _uiState.value = _uiState.value.copy( 20 | version = BuildConfig.VERSION_NAME 21 | ) 22 | } 23 | 24 | fun launchApp(){ 25 | WearDataManager(app).sendStartActivityRequest() 26 | } 27 | } -------------------------------------------------------------------------------- /wear/src/main/java/com/blazecode/tsviewer/wear/viewmodels/ServiceOffViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.blazecode.tsviewer.wear.viewmodels 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import com.blazecode.tsviewer.wear.communication.WearDataManager 6 | import com.blazecode.tsviewer.wear.uistate.ServiceOffUiState 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.StateFlow 9 | import kotlinx.coroutines.flow.asStateFlow 10 | 11 | class ServiceOffViewModel(val app: Application): AndroidViewModel(app) { 12 | 13 | // UI STATE 14 | private val _uiState = MutableStateFlow(ServiceOffUiState()) 15 | val uiState: StateFlow = _uiState.asStateFlow() 16 | 17 | fun startService(){ 18 | WearDataManager(app).startService() 19 | _uiState.value = _uiState.value.copy( 20 | startServiceButtonEnabled = false 21 | ) 22 | } 23 | 24 | fun launchApp(){ 25 | WearDataManager(app).sendStartActivityRequest() 26 | } 27 | } -------------------------------------------------------------------------------- /wear/src/main/java/data/WearDataPackage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright (c) BlazeCode / Ralf Lehmann, 2023. 4 | * 5 | */ 6 | 7 | package data 8 | 9 | import com.blazecode.tsviewer.wear.data.TsClient 10 | 11 | data class WearDataPackage( 12 | val clients: List, 13 | val timestamp: Long 14 | ) -------------------------------------------------------------------------------- /wear/src/main/res/drawable/ic_clients.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /wear/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /wear/src/main/res/drawable/ic_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | 21 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /wear/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /wear/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | 23 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /wear/src/main/res/drawable/ic_open.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /wear/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /wear/src/main/res/mipmap-xxxhdpi/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /wear/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | #181A20 12 | #F2F4FF 13 | #BFC6FF 14 | #686FAA 15 | #184378 16 | #ffffff 17 | #6169AC 18 | #4E569A 19 | #184378 20 | #3F467E 21 | 22 | -------------------------------------------------------------------------------- /wear/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 8dp 10 | 16dp 11 | 24dp 12 | -------------------------------------------------------------------------------- /wear/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | TSViewer 9 | 10 | 11 | Launch app 12 | Clients 13 | 14 | 15 | 16 | %s Client connected 17 | %s Clients connected 18 | 19 | %s min ago 20 | Refresh 21 | Loading 22 | Done 23 | 24 | 25 | Service is off 26 | Start Service 27 | 28 | 29 | No Network 30 | No WiFi 31 | Airplane Mode 32 | 33 | -------------------------------------------------------------------------------- /wear/src/main/res/values/wear.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | wear 11 | 12 | --------------------------------------------------------------------------------