diff --git a/embedded/android/PhoneController/.gitignore b/embedded/android/PhoneController/.gitignore new file mode 100644 index 0000000..5636690 --- /dev/null +++ b/embedded/android/PhoneController/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +key.properties \ No newline at end of file diff --git a/embedded/android/PhoneController/.idea/.gitignore b/embedded/android/PhoneController/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/embedded/android/PhoneController/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/embedded/android/PhoneController/.idea/codeStyles b/embedded/android/PhoneController/.idea/codeStyles new file mode 100644 index 0000000..320a4bc --- /dev/null +++ b/embedded/android/PhoneController/.idea/codeStyles @@ -0,0 +1,137 @@ + + + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/embedded/android/PhoneController/.idea/compiler.xml b/embedded/android/PhoneController/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/embedded/android/PhoneController/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/.idea/deploymentTargetSelector.xml b/embedded/android/PhoneController/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/embedded/android/PhoneController/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/.idea/gradle.xml b/embedded/android/PhoneController/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/embedded/android/PhoneController/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/.idea/inspectionProfiles/Project_Default.xml b/embedded/android/PhoneController/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..6806f5a --- /dev/null +++ b/embedded/android/PhoneController/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,53 @@ + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/.idea/kotlinc.xml b/embedded/android/PhoneController/.idea/kotlinc.xml new file mode 100644 index 0000000..bb44937 --- /dev/null +++ b/embedded/android/PhoneController/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/.idea/migrations.xml b/embedded/android/PhoneController/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/embedded/android/PhoneController/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/.idea/misc.xml b/embedded/android/PhoneController/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/embedded/android/PhoneController/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/.idea/vcs.xml b/embedded/android/PhoneController/.idea/vcs.xml new file mode 100644 index 0000000..c2365ab --- /dev/null +++ b/embedded/android/PhoneController/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/app/.gitignore b/embedded/android/PhoneController/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/embedded/android/PhoneController/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/embedded/android/PhoneController/app/build.gradle.kts b/embedded/android/PhoneController/app/build.gradle.kts new file mode 100644 index 0000000..4d4bdb8 --- /dev/null +++ b/embedded/android/PhoneController/app/build.gradle.kts @@ -0,0 +1,100 @@ +import java.util.Properties +import java.util.UUID + +fun generateDeviceId(): String { + return UUID.randomUUID().toString() +} + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.compiler) +} + +android { + namespace = "com.ndomx.phonecontroller" + compileSdk = 34 + + val file = rootProject.file("key.properties") + val properties = Properties().apply { + load(file.inputStream()) + } + + defaultConfig { + applicationId = "com.ndomx.phonecontroller" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + + buildConfigField("String", "MQTT_BROKER_URL", "\"${properties.getProperty("MQTT_BROKER_URL")}\"") + buildConfigField("int", "MQTT_BROKER_PORT", properties.getProperty("MQTT_BROKER_PORT")) + buildConfigField("String", "MQTT_USERNAME", "\"${properties.getProperty("MQTT_USERNAME")}\"") + buildConfigField("String", "MQTT_PASSWORD", "\"${properties.getProperty("MQTT_PASSWORD")}\"") + buildConfigField("String", "DEVICE_ID", "\"${generateDeviceId()}\"") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + buildConfig = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + // MQTT + implementation(libs.org.eclipse.paho.client.mqttv3) + implementation(libs.org.eclipse.paho.android.service) + + // Serialization + implementation(libs.kotlinx.serialization.json) + + // Icons + implementation(libs.androidx.material.icons.extended) +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/proguard-rules.pro b/embedded/android/PhoneController/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/embedded/android/PhoneController/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/androidTest/java/com/ndomx/phonecontroller/ExampleInstrumentedTest.kt b/embedded/android/PhoneController/app/src/androidTest/java/com/ndomx/phonecontroller/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..06db660 --- /dev/null +++ b/embedded/android/PhoneController/app/src/androidTest/java/com/ndomx/phonecontroller/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.ndomx.phonecontroller + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.ndomx.phonecontroller", appContext.packageName) + } +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/AndroidManifest.xml b/embedded/android/PhoneController/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..aa8b86e --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/MainActivity.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/MainActivity.kt new file mode 100644 index 0000000..9804e13 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/MainActivity.kt @@ -0,0 +1,112 @@ +package com.ndomx.phonecontroller + +import android.content.Intent +import android.os.Bundle +import android.telecom.TelecomManager +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.ndomx.phonecontroller.api.Command +import com.ndomx.phonecontroller.controllers.CallsController +import com.ndomx.phonecontroller.controllers.PreferenceController +import com.ndomx.phonecontroller.interfaces.StateHandler +import com.ndomx.phonecontroller.mqtt.MessageSubscriber +import com.ndomx.phonecontroller.mqtt.MqttManager +import com.ndomx.phonecontroller.services.MqttService +import com.ndomx.phonecontroller.ui.screens.HomeScreen +import com.ndomx.phonecontroller.ui.theme.PhoneControllerTheme + +class MainActivity : ComponentActivity(), StateHandler, MessageSubscriber { + override val subscriberId = "MainActivity" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + PhoneControllerTheme { + HomeScreen( + phoneNumber = PreferenceController.loadString( + this, + getString(R.string.pref_phone_number_key) + ), + stateHandler = this, + ) + } + } + + requestDefaultDialer() + } + + override fun onSaveClick(phoneNumber: String) { + PreferenceController.saveKey( + this, + getString(R.string.pref_phone_number_key), + phoneNumber + ) + } + + override fun onConnectClick() { + MqttManager.addSubscriber(this) + MqttManager.start(this) + } + + override fun onServiceClick() { + if (serviceStatus()) { + stopMqttService() + } else { + startMqttService() + } + } + + override fun onCommand(command: Command) { + val phoneNumber = PreferenceController.loadString( + this, + getString(R.string.pref_phone_number_key) + ) + + CallsController.makePhoneCall(this, phoneNumber) + } + + override fun serviceStatus(): Boolean { + return PreferenceController.loadBoolean( + this, + getString(R.string.pref_service_status_key) + ) + } + + private fun startMqttService() { + MqttManager.removeSubscriber(this) + startService( + Intent(this, MqttService::class.java) + ) + } + + private fun stopMqttService() { + stopService( + Intent(this, MqttService::class.java) + ) + } + + private fun requestDefaultDialer() { + if (isDefaultDialer()) { + Log.d("MainActivity", "App is already the default dialer") + return + } + + showDialerPrompt() + } + + private fun isDefaultDialer(): Boolean { + val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager + return telecomManager.defaultDialerPackage == packageName + } + + private fun showDialerPrompt() { + val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER).apply { + putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName) + } + + startActivity(intent) + } +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/api/Command.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/api/Command.kt new file mode 100644 index 0000000..66ead44 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/api/Command.kt @@ -0,0 +1,10 @@ +package com.ndomx.phonecontroller.api + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +data class Command( + val action: String, + val actionDetails: Map? = null +) diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/api/CommandResponse.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/api/CommandResponse.kt new file mode 100644 index 0000000..0d88cba --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/api/CommandResponse.kt @@ -0,0 +1,9 @@ +package com.ndomx.phonecontroller.api + +import kotlinx.serialization.Serializable + +@Serializable +data class CommandResponse( + val deviceId: String, + val status: Int +) diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/api/ExecuteCommandResult.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/api/ExecuteCommandResult.kt new file mode 100644 index 0000000..024814a --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/api/ExecuteCommandResult.kt @@ -0,0 +1,9 @@ +package com.ndomx.phonecontroller.api + +enum class ExecuteCommandResult(val value: Int) { + OK(0), + INVALID_PAYLOAD(2), + INVALID_COMMAND(3), + INVALID_ACTION(4), + UNKNOWN_ERROR(5) +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/controllers/CallsController.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/controllers/CallsController.kt new file mode 100644 index 0000000..e0b2a8a --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/controllers/CallsController.kt @@ -0,0 +1,47 @@ +package com.ndomx.phonecontroller.controllers + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.telecom.TelecomManager +import android.telephony.PhoneStateListener +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.core.content.PermissionChecker + +class CallsController(private val context: Context) : PhoneStateListener() { + companion object { + fun makePhoneCall(context: Context, phoneNumber: String) { + if (phoneNumber.isBlank()) { + showToast(context, "Phone number is empty!") + return + } + + if (!checkPermission(context)) { + showToast(context, "Call permission required!") + return + } + + val intent = Intent(Intent.ACTION_CALL).apply { + data = Uri.parse("tel:$phoneNumber") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + + context.startActivity(intent) + } + + private fun checkPermission(context: Context): Boolean { + val permission = android.Manifest.permission.CALL_PHONE + val res = ContextCompat.checkSelfPermission(context, permission) + return res == PermissionChecker.PERMISSION_GRANTED + } + + private fun showToast(context: Context, message: String) { + Handler(Looper.getMainLooper()).post { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + } + } +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/controllers/PreferenceController.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/controllers/PreferenceController.kt new file mode 100644 index 0000000..061a45d --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/controllers/PreferenceController.kt @@ -0,0 +1,34 @@ +package com.ndomx.phonecontroller.controllers + +import android.content.Context +import android.content.SharedPreferences +import com.ndomx.phonecontroller.R + +object PreferenceController { + fun loadString(context: Context, key: String): String { + val prefs = sharedPrefs(context) + val stored = prefs.getString(key, "") + + return stored!! + } + + fun loadBoolean(context: Context, key: String): Boolean { + val prefs = sharedPrefs(context) + return prefs.getBoolean(key, false) + } + + fun saveKey(context: Context, key: String, value: String) { + val prefs = sharedPrefs(context) + prefs.edit().putString(key, value).apply() + } + + fun saveKey(context: Context, key: String, value: Boolean) { + val prefs = sharedPrefs(context) + prefs.edit().putBoolean(key, value).apply() + } + + private fun sharedPrefs(context: Context): SharedPreferences { + val preferenceFileKey = context.getString(R.string.preference_file_key) + return context.getSharedPreferences(preferenceFileKey, Context.MODE_PRIVATE) + } +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/interfaces/StateHandler.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/interfaces/StateHandler.kt new file mode 100644 index 0000000..4c2c846 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/interfaces/StateHandler.kt @@ -0,0 +1,8 @@ +package com.ndomx.phonecontroller.interfaces + +interface StateHandler { + fun onSaveClick(phoneNumber: String) + fun onConnectClick() + fun onServiceClick() + fun serviceStatus(): Boolean +} diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/mqtt/ConnectionStatus.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/mqtt/ConnectionStatus.kt new file mode 100644 index 0000000..a08190b --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/mqtt/ConnectionStatus.kt @@ -0,0 +1,9 @@ +package com.ndomx.phonecontroller.mqtt + +enum class ConnectionStatus { + DISCONNECTED, + CONNECTING, + CONNECTED, + DISCONNECTING, + ERROR +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/mqtt/MessageSubscriber.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/mqtt/MessageSubscriber.kt new file mode 100644 index 0000000..3c4f58e --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/mqtt/MessageSubscriber.kt @@ -0,0 +1,8 @@ +package com.ndomx.phonecontroller.mqtt + +import com.ndomx.phonecontroller.api.Command + +interface MessageSubscriber { + val subscriberId: String + fun onCommand(command: Command) +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/mqtt/MqttManager.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/mqtt/MqttManager.kt new file mode 100644 index 0000000..72effbe --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/mqtt/MqttManager.kt @@ -0,0 +1,180 @@ +package com.ndomx.phonecontroller.mqtt + +import android.content.Context +import android.util.Log +import com.ndomx.phonecontroller.BuildConfig +import com.ndomx.phonecontroller.api.Command +import com.ndomx.phonecontroller.api.ExecuteCommandResult +import com.ndomx.phonecontroller.api.CommandResponse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.MissingFieldException +import kotlinx.serialization.json.Json +import org.eclipse.paho.client.mqttv3.* + +object MqttManager : MqttCallbackExtended { + private const val LOG_TAG = "MqttManager" + + private const val SUB_TOPIC = "node/${BuildConfig.DEVICE_ID}" + private const val PUB_TOPIC = "gate/ack" + + private val supportedActions = listOf("call") + + private var mqttClient: MqttClient? = null + + private val statusStateFlow = MutableStateFlow(ConnectionStatus.DISCONNECTED) + val status = statusStateFlow.asStateFlow() + + private val sendingStateFlow = MutableStateFlow(false) + val sending = sendingStateFlow.asStateFlow() + + private val subscribers = mutableListOf() + + fun start(context: Context) { + if (mqttClient?.isConnected == true) { + return + } + + createClient() + connect(context) + } + + fun disconnect() { + mqttClient?.disconnect() + statusStateFlow.value = ConnectionStatus.DISCONNECTED + mqttClient = null + } + + fun addSubscriber(subscriber: MessageSubscriber) { + this.subscribers.add(subscriber) + } + + fun removeSubscriber(subscriber: MessageSubscriber) { + this.subscribers.removeIf { s -> s.subscriberId == subscriber.subscriberId } + } + + override fun connectComplete(reconnect: Boolean, serverURI: String?) { + Log.i(LOG_TAG, "MQTT Connected to $serverURI") + statusStateFlow.value = ConnectionStatus.CONNECTED + subscribe() + } + + override fun connectionLost(cause: Throwable?) { + Log.e(LOG_TAG, "MQTT Connection lost: ${cause?.message}", cause) + statusStateFlow.value = ConnectionStatus.DISCONNECTED + } + + override fun messageArrived(topic: String?, message: MqttMessage?) { + Log.i(LOG_TAG, "MQTT Message received on $topic: ${message.toString()}") + } + + override fun deliveryComplete(token: IMqttDeliveryToken?) { + sendingStateFlow.value = false + } + + private fun connect(context: Context) = CoroutineScope(Dispatchers.IO).launch { + statusStateFlow.value = ConnectionStatus.CONNECTING + + val options = MqttConnectOptions().apply { + isCleanSession = true + userName = BuildConfig.MQTT_USERNAME + password = BuildConfig.MQTT_PASSWORD.toCharArray() + } + + try { + mqttClient?.connect(options) + } catch (e: Exception) { + Log.e(LOG_TAG, "MQTT Connection error: ${e.message}", e) + statusStateFlow.value = ConnectionStatus.ERROR + mqttClient = null + } + } + + private fun subscribe() { + Log.i(LOG_TAG, "MQTT Subscribing to $SUB_TOPIC") + + mqttClient?.subscribe(SUB_TOPIC) { _, message -> + onMessage(String(message.payload)) + } + } + + private fun createClient() { + if (mqttClient != null) { + mqttClient?.disconnect() + return + } + + val uri = buildConnectionUri() + mqttClient = MqttClient(uri, BuildConfig.DEVICE_ID, null).apply { + setCallback(this@MqttManager) + } + } + + private fun buildConnectionUri(): String { + val host = BuildConfig.MQTT_BROKER_URL + val port = BuildConfig.MQTT_BROKER_PORT + return "ssl://$host:$port" + } + + private fun publish(message: String) = CoroutineScope(Dispatchers.IO).launch { + if (mqttClient?.isConnected != true) { + Log.w(LOG_TAG, "MQTT not connected") + return@launch + } + + sendingStateFlow.value = true + mqttClient?.publish(PUB_TOPIC, MqttMessage(message.toByteArray())) + } + + private fun onMessage(message: String) { + val (result, command) = parseMessage(message) + sendAck(result) + + if (result == ExecuteCommandResult.OK) { + notifySubscribers(command!!) + } + } + + private fun sendAck(result: ExecuteCommandResult) { + val message = CommandResponse( + deviceId = BuildConfig.DEVICE_ID, + status = result.value + ) + + publish( + Json.encodeToString(message) + ) + } + + private fun notifySubscribers(command: Command) { + subscribers.forEach { subscriber -> + Log.i(LOG_TAG, "Notifying subscriber ${subscriber.subscriberId}") + subscriber.onCommand(command) + } + } + + @OptIn(ExperimentalSerializationApi::class) + private fun parseMessage(message: String): Pair { + var command: Command? = null + val result = try { + command = Json.decodeFromString(message) + if (supportedActions.contains(command.action)) { + ExecuteCommandResult.OK + } else { + ExecuteCommandResult.INVALID_ACTION + } + } catch (e: IllegalArgumentException) { + Log.e(LOG_TAG, "invalid payload: $message", e) + ExecuteCommandResult.INVALID_PAYLOAD + } catch (e: MissingFieldException) { + Log.e(LOG_TAG, "invalid command: $message", e) + ExecuteCommandResult.INVALID_COMMAND + } + + return Pair(result, command) + } +} diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/services/MqttService.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/services/MqttService.kt new file mode 100644 index 0000000..fed63e5 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/services/MqttService.kt @@ -0,0 +1,101 @@ +package com.ndomx.phonecontroller.services + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import com.ndomx.phonecontroller.controllers.PreferenceController +import com.ndomx.phonecontroller.R +import com.ndomx.phonecontroller.api.Command +import com.ndomx.phonecontroller.controllers.CallsController +import com.ndomx.phonecontroller.mqtt.MessageSubscriber +import com.ndomx.phonecontroller.mqtt.MqttManager + +class MqttService : Service(), MessageSubscriber { + companion object { + private const val CHANNEL_ID = "MqttServiceChannel" + private const val LOG_TAG = "MqttService" + } + + override val subscriberId = "MqttService" + private var loadedPhoneNumber: String? = null + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + + Log.i(LOG_TAG, "MQTT Service Started") + + MqttManager.addSubscriber(this) + MqttManager.start(this) + + startForeground(1, createNotification()) + PreferenceController.saveKey( + this, + getString(R.string.pref_service_status_key), + true + ) + } + + override fun onDestroy() { + super.onDestroy() + + MqttManager.disconnect() + MqttManager.removeSubscriber(this) + + Log.i(LOG_TAG, "MQTT Service Stopped") + + PreferenceController.saveKey( + this, + getString(R.string.pref_service_status_key), + false + ) + } + + override fun onCommand(command: Command) { + val phoneNumber = loadPhoneNumber() + CallsController.makePhoneCall(this, phoneNumber) + } + + override fun onBind(intent: Intent): IBinder? = null + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + + val channel = NotificationChannel( + CHANNEL_ID, + "MQTT Service", + NotificationManager.IMPORTANCE_LOW + ) + + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(channel) + } + + private fun createNotification(): Notification { + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("MQTT Running in Background") + .setSmallIcon(android.R.drawable.stat_notify_sync) + .build() + } + + private fun loadPhoneNumber(): String { + if (loadedPhoneNumber != null) { + return loadedPhoneNumber!! + } + + val stored = PreferenceController.loadString( + this, getString(R.string.pref_phone_number_key) + ) + + loadedPhoneNumber = stored + return stored + } +} diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/BottomPanel.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/BottomPanel.kt new file mode 100644 index 0000000..a5b07e3 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/BottomPanel.kt @@ -0,0 +1,47 @@ +package com.ndomx.phonecontroller.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ndomx.phonecontroller.interfaces.StateHandler +import com.ndomx.phonecontroller.mqtt.MqttManager + +@Composable +fun BottomPanel(stateHandler: StateHandler) { + val mqttStatus by MqttManager.status.collectAsState() + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) { + ConnectButton(mqttStatus, Modifier.fillMaxWidth(0.5f), stateHandler::onConnectClick) + Button( + stateHandler::onServiceClick, + shape = MaterialTheme.shapes.small, + modifier = Modifier.fillMaxWidth() + ) { + Text(if (stateHandler.serviceStatus()) { + "Stop Service" + } else { + "Start Service" + }) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun BottomPanelPreview() { + BottomPanel(object : StateHandler { + override fun onSaveClick(phoneNumber: String) {} + override fun onConnectClick() {} + override fun onServiceClick() {} + override fun serviceStatus(): Boolean = true + }) +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/ConnectButton.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/ConnectButton.kt new file mode 100644 index 0000000..bcd3b00 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/ConnectButton.kt @@ -0,0 +1,66 @@ +package com.ndomx.phonecontroller.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ndomx.phonecontroller.mqtt.ConnectionStatus + +private data class ButtonStatus( + val enabled: Boolean, + val text: String, + val showProgressIndicator: Boolean = false, +) + +@Composable +fun ConnectButton(status: ConnectionStatus, modifier: Modifier = Modifier, onClick: () -> Unit) { + val buttonStatusMap = mapOf( + ConnectionStatus.CONNECTING to ButtonStatus( + false, "Connecting...", true + ), + ConnectionStatus.CONNECTED to ButtonStatus(true, "Disconnect"), + ConnectionStatus.DISCONNECTED to ButtonStatus(true, "Connect"), + ConnectionStatus.ERROR to ButtonStatus(true, "Error"), + ConnectionStatus.DISCONNECTING to ButtonStatus( + false, "Disconnecting...", true + ) + ) + + val buttonStatus = buttonStatusMap[status] + + Button( + onClick = onClick, + enabled = buttonStatus!!.enabled, + modifier = modifier, + shape = MaterialTheme.shapes.small + ) { + Text(buttonStatus.text) + + if (buttonStatus.showProgressIndicator) { + Spacer(modifier.size(ButtonDefaults.IconSpacing)) + + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp + ) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun ConnectButtonPreview() { + Column { + ConnectButton(ConnectionStatus.CONNECTING) {} + ConnectButton(ConnectionStatus.CONNECTED) {} + } +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/ConnectionStatusIndicator.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/ConnectionStatusIndicator.kt new file mode 100644 index 0000000..50cc1f9 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/ConnectionStatusIndicator.kt @@ -0,0 +1,43 @@ +package com.ndomx.phonecontroller.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun ConnectionStatusIndicator(label: String, isConnected: Boolean) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text("$label: ", style = MaterialTheme.typography.bodyLarge) + Box( + modifier = Modifier + .size(12.dp) + .padding(start = 8.dp), + contentAlignment = Alignment.Center + ) { + Surface( + shape = MaterialTheme.shapes.small, + color = if (isConnected) Color.Green else Color.Red, + modifier = Modifier.size(12.dp) + ) {} + } + } +} + +@Composable +@Preview(showBackground = true) +private fun ConnectionStatusIndicatorPreview() { + ConnectionStatusIndicator("Connection Status", true) +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/DeviceIdView.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/DeviceIdView.kt new file mode 100644 index 0000000..8db2f90 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/DeviceIdView.kt @@ -0,0 +1,50 @@ +package com.ndomx.phonecontroller.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ndomx.phonecontroller.BuildConfig + +@Composable +fun DeviceIdView( + displayMessage: (message: String) -> Unit, +) { + val clipboardManager = LocalClipboardManager.current + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(0.8f) + ) { + SensitiveText(BuildConfig.DEVICE_ID, true) + Button(onClick = { + clipboardManager.setText(AnnotatedString(BuildConfig.DEVICE_ID)) + displayMessage("Device ID copied to clipboard") + }, colors = ButtonDefaults.outlinedButtonColors()) { + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Text("Copy to clipboard") + Icon(Icons.Default.ContentCopy, contentDescription = "Copy to clipboard") + } + } + } +} + +@Composable +@Preview(showBackground = true) +private fun DeviceIdViewPreview() { + DeviceIdView {} +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/PhoneInput.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/PhoneInput.kt new file mode 100644 index 0000000..3980849 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/PhoneInput.kt @@ -0,0 +1,61 @@ +package com.ndomx.phonecontroller.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ndomx.phonecontroller.ui.theme.PhoneControllerTheme + +@Composable +fun PhoneInput(phoneNumber: String, onSave: (String) -> Unit) { + var phone by remember { mutableStateOf(phoneNumber) } + + Column( + modifier = Modifier.fillMaxWidth(0.8f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + OutlinedTextField( + value = phone, + onValueChange = { phone = it }, + label = { Text("Phone Number") }, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Phone, + imeAction = ImeAction.Done + ), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { onSave(phone) }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Save Number") + } + } +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + PhoneControllerTheme { + PhoneInput("+56912345678", onSave = {}) + } +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/SensitiveText.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/SensitiveText.kt new file mode 100644 index 0000000..7583b6f --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/components/SensitiveText.kt @@ -0,0 +1,66 @@ +package com.ndomx.phonecontroller.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun SensitiveText(text: String, show: Boolean) { + var isRevealed by remember { mutableStateOf(show) } + + if (isRevealed) { + VisibleText(text) { isRevealed = false } + } else { + HiddenText(text) { isRevealed = true } + } +} + +@Composable() +@Preview(showBackground = true) +private fun SensitiveTextPreview() { + Column { + SensitiveText("example", true) + SensitiveText("example", false) + } +} + +@Composable +private fun HiddenText(text: String, onClick: () -> Unit) { + OutlinedTextField( + value = text, + onValueChange = {}, + visualTransformation = PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = onClick) { + Icon(Icons.Default.Visibility, contentDescription = "Toggle password visibility") + } + }, + enabled = false, + ) +} + +@Composable +private fun VisibleText(text: String, onClick: () -> Unit) { + OutlinedTextField( + value = text, + onValueChange = {}, + trailingIcon = { + IconButton(onClick = onClick) { + Icon(Icons.Default.VisibilityOff, contentDescription = "Toggle password visibility") + } + }, + enabled = false, + singleLine = true, + ) +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/screens/HomeScreen.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/screens/HomeScreen.kt new file mode 100644 index 0000000..4f08fb0 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/screens/HomeScreen.kt @@ -0,0 +1,98 @@ +package com.ndomx.phonecontroller.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ndomx.phonecontroller.interfaces.StateHandler +import com.ndomx.phonecontroller.mqtt.ConnectionStatus +import com.ndomx.phonecontroller.mqtt.MqttManager +import com.ndomx.phonecontroller.ui.components.BottomPanel +import com.ndomx.phonecontroller.ui.components.ConnectionStatusIndicator +import com.ndomx.phonecontroller.ui.components.DeviceIdView +import com.ndomx.phonecontroller.ui.components.PhoneInput +import com.ndomx.phonecontroller.ui.theme.PhoneControllerTheme +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen( + phoneNumber: String, + stateHandler: StateHandler +) { + val mqttStatus by MqttManager.status.collectAsState() + + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(snackbarHostState) + }, + topBar = { + TopAppBar( + title = { Text("Phone Controller") }, + ) + }, + ) { innerPadding -> + Column( + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(innerPadding) + .padding(16.dp) + .fillMaxSize() + ) { + Column { + ConnectionStatusIndicator( + "Internet", + mqttStatus == ConnectionStatus.CONNECTED + ) + + ConnectionStatusIndicator( + "MQTT Broker", + mqttStatus == ConnectionStatus.CONNECTED + ) + } + + PhoneInput(phoneNumber, onSave = stateHandler::onSaveClick) + + DeviceIdView { + scope.launch { + snackbarHostState.showSnackbar("Device ID copied to clipboard") + } + } + + BottomPanel(stateHandler) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun HomeScreenPreview() { + PhoneControllerTheme { + HomeScreen("+56912345678", object : StateHandler { + override fun onSaveClick(phoneNumber: String) {} + override fun onConnectClick() {} + override fun onServiceClick() {} + override fun serviceStatus(): Boolean = true + }) + } +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/theme/Color.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/theme/Color.kt new file mode 100644 index 0000000..b94fecf --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.ndomx.phonecontroller.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/theme/Theme.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/theme/Theme.kt new file mode 100644 index 0000000..64c938a --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/theme/Theme.kt @@ -0,0 +1,57 @@ +package com.ndomx.phonecontroller.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun PhoneControllerTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/theme/Type.kt b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/theme/Type.kt new file mode 100644 index 0000000..33f2df6 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/java/com/ndomx/phonecontroller/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.ndomx.phonecontroller.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/res/drawable/ic_launcher_background.xml b/embedded/android/PhoneController/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/embedded/android/PhoneController/app/src/main/res/drawable/ic_launcher_foreground.xml b/embedded/android/PhoneController/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/embedded/android/PhoneController/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/embedded/android/PhoneController/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/embedded/android/PhoneController/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/embedded/android/PhoneController/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/embedded/android/PhoneController/app/src/main/res/values/colors.xml b/embedded/android/PhoneController/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/res/values/strings.xml b/embedded/android/PhoneController/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..29bb78e --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + PhoneController + prefs + phone_number + service_status + \ No newline at end of file diff --git a/embedded/android/PhoneController/app/src/main/res/values/themes.xml b/embedded/android/PhoneController/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..900375a --- /dev/null +++ b/embedded/android/PhoneController/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +