Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ android {
noCompress += "task"
}
packaging {
jniLibs {
useLegacyPackaging = false
}
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,31 @@
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.DriveSwipe">
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".RoutineTriggerActivity"
android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true"
android:theme="@style/Theme.DriveSwipe">
<intent-filter>
<action android:name="com.example.driveswipe.action.ROUTINE_START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.example.driveswipe.action.ROUTINE_STOP" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
Comment on lines +48 to +55
</activity>

<service
android:name=".GestureService"
android:exported="false"
Expand Down
24 changes: 24 additions & 0 deletions app/src/main/java/com/example/driveswipe/DriveSwipeApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,30 @@ private fun SettingsScreen(
}
}

Text("Autostart", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold)
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Samsung Routine", fontWeight = FontWeight.Bold)
Text(
"Use Samsung Modes and Routines to start DriveSwipe when your car Bluetooth connects.",
style = MaterialTheme.typography.bodyMedium
)
Text(
"Create a Routine with condition: Bluetooth device connected. For the action, choose DriveSwipe's Start DriveSwipe shortcut or app action.",
style = MaterialTheme.typography.bodySmall
)
Text(
"For auto-stop, create a second Routine with condition: Bluetooth device disconnected, then choose Stop DriveSwipe.",
style = MaterialTheme.typography.bodySmall
)
Text(
"Grant DriveSwipe camera and notification permissions before using the Routine.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

// Gestures
Text("Gestures", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold)
Text("Preset Configuration", fontWeight = FontWeight.SemiBold)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.example.driveswipe

import android.Manifest
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat

object GestureServiceController {
fun buildStartIntent(context: Context, settings: AppSettings): Intent {
return Intent(context, GestureService::class.java).apply {
putExtra(ServiceContract.EXTRA_NIGHT_MODE, settings.isNightMode)
putExtra(ServiceContract.EXTRA_ACTION_COOLDOWN_MS, settings.tuning.actionCooldownMs)
putExtra(ServiceContract.EXTRA_VOLUME_TICK_MS, settings.tuning.volumeTickMs)
putExtra(ServiceContract.EXTRA_PINCH_THRESHOLD, settings.tuning.pinchThreshold)
putExtra(ServiceContract.EXTRA_PINCH_RELEASE_THRESHOLD, settings.tuning.pinchReleaseThreshold)
putExtra(ServiceContract.EXTRA_SWIPE_THRESHOLD, settings.tuning.swipeThreshold)
putExtra(ServiceContract.EXTRA_SWIPE_TIMEOUT_MS, settings.tuning.swipeTimeoutMs)
putExtra(ServiceContract.EXTRA_ALERTING_BURST_MS, settings.tuning.alertingBurstMs)
putExtra(ServiceContract.EXTRA_ACTIVE_TIMEOUT_MS, settings.tuning.activeTimeoutMs)
putExtra(ServiceContract.EXTRA_IDLE_INFERENCE_INTERVAL_MS, settings.tuning.idleInferenceIntervalMs)
putExtra(ServiceContract.EXTRA_MAP_PINCH_RIGHT, settings.mappings.pinchDragRight.name)
putExtra(ServiceContract.EXTRA_MAP_PINCH_LEFT, settings.mappings.pinchDragLeft.name)
putExtra(ServiceContract.EXTRA_MAP_TWO_FINGER, settings.mappings.twoFingerPoint.name)
putExtra(ServiceContract.EXTRA_MAP_VOLUME_UP, settings.mappings.volumeUp.name)
putExtra(ServiceContract.EXTRA_MAP_VOLUME_DOWN, settings.mappings.volumeDown.name)
}
}

fun hasRequiredStartPermissions(context: Context): Boolean {
val hasCamera = ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
val hasNotifications = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
return hasCamera && hasNotifications
}

fun start(context: Context, settings: AppSettings) {
val intent = buildStartIntent(context, settings)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(context, intent)
} else {
context.startService(intent)
}
}

fun stop(context: Context) {
context.stopService(Intent(context, GestureService::class.java))
}

@Suppress("DEPRECATION")
fun isRunning(context: Context): Boolean {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
return activityManager.getRunningServices(Int.MAX_VALUE).any { serviceInfo ->
serviceInfo.service.className == GestureService::class.java.name
}
Comment on lines +62 to +66
}
}
30 changes: 7 additions & 23 deletions app/src/main/java/com/example/driveswipe/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,40 +133,23 @@ class MainActivity : ComponentActivity() {

private fun startGestureService() {
val settings = viewModel.uiState.value.settings
val intent = Intent(this, GestureService::class.java).apply {
putExtra(ServiceContract.EXTRA_NIGHT_MODE, settings.isNightMode)
putExtra(ServiceContract.EXTRA_ACTION_COOLDOWN_MS, settings.tuning.actionCooldownMs)
putExtra(ServiceContract.EXTRA_VOLUME_TICK_MS, settings.tuning.volumeTickMs)
putExtra(ServiceContract.EXTRA_PINCH_THRESHOLD, settings.tuning.pinchThreshold)
putExtra(ServiceContract.EXTRA_PINCH_RELEASE_THRESHOLD, settings.tuning.pinchReleaseThreshold)
putExtra(ServiceContract.EXTRA_SWIPE_THRESHOLD, settings.tuning.swipeThreshold)
putExtra(ServiceContract.EXTRA_SWIPE_TIMEOUT_MS, settings.tuning.swipeTimeoutMs)
putExtra(ServiceContract.EXTRA_ALERTING_BURST_MS, settings.tuning.alertingBurstMs)
putExtra(ServiceContract.EXTRA_ACTIVE_TIMEOUT_MS, settings.tuning.activeTimeoutMs)
putExtra(ServiceContract.EXTRA_IDLE_INFERENCE_INTERVAL_MS, settings.tuning.idleInferenceIntervalMs)
putExtra(ServiceContract.EXTRA_MAP_PINCH_RIGHT, settings.mappings.pinchDragRight.name)
putExtra(ServiceContract.EXTRA_MAP_PINCH_LEFT, settings.mappings.pinchDragLeft.name)
putExtra(ServiceContract.EXTRA_MAP_TWO_FINGER, settings.mappings.twoFingerPoint.name)
putExtra(ServiceContract.EXTRA_MAP_VOLUME_UP, settings.mappings.volumeUp.name)
putExtra(ServiceContract.EXTRA_MAP_VOLUME_DOWN, settings.mappings.volumeDown.name)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(this, intent)
} else {
startService(intent)
if (!GestureServiceController.hasRequiredStartPermissions(this)) {
checkPermissions()
return
}
GestureServiceController.start(this, settings)
viewModel.setServiceRunning(true)
}

private fun stopGestureService() {
val intent = Intent(this, GestureService::class.java)
stopService(intent)
GestureServiceController.stop(this)
viewModel.setServiceRunning(false)
}

override fun onResume() {
super.onResume()
refreshPermissionState()
viewModel.setServiceRunning(GestureServiceController.isRunning(this))
}

override fun onStart() {
Expand All @@ -180,6 +163,7 @@ class MainActivity : ComponentActivity() {
} else {
registerReceiver(serviceEventReceiver, filter)
}
viewModel.setServiceRunning(GestureServiceController.isRunning(this))
}

override fun onStop() {
Expand Down
80 changes: 80 additions & 0 deletions app/src/main/java/com/example/driveswipe/RoutineTriggerActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.example.driveswipe

import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch

class RoutineTriggerActivity : ComponentActivity() {
private val settingsRepository by lazy { SettingsRepository(applicationContext) }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleRoutineIntent(intent)
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleRoutineIntent(intent)
}

private fun handleRoutineIntent(intent: Intent?) {
when (intent?.action) {
ServiceContract.ACTION_ROUTINE_START -> startFromRoutine()
ServiceContract.ACTION_ROUTINE_STOP -> stopFromRoutine()
else -> openMainActivityAndFinish()
}
}

private fun startFromRoutine() {
if (!GestureServiceController.hasRequiredStartPermissions(this)) {
Toast.makeText(
this,
"DriveSwipe needs camera and notification permissions before Routine start.",
Toast.LENGTH_LONG
).show()
openMainActivityAndFinish()
return
}

lifecycleScope.launch {
val settings = settingsRepository.settingsFlow.first()
runCatching {
GestureServiceController.start(this@RoutineTriggerActivity, settings)
}.onSuccess {
Toast.makeText(
this@RoutineTriggerActivity,
"DriveSwipe listening started.",
Toast.LENGTH_SHORT
).show()
finish()
}.onFailure {
Toast.makeText(
this@RoutineTriggerActivity,
"DriveSwipe could not start from Routine. Opening app.",
Toast.LENGTH_LONG
).show()
openMainActivityAndFinish()
}
}
}

private fun stopFromRoutine() {
GestureServiceController.stop(this)
Toast.makeText(this, "DriveSwipe listening stopped.", Toast.LENGTH_SHORT).show()
finish()
}

private fun openMainActivityAndFinish() {
startActivity(
Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
)
finish()
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/example/driveswipe/ServiceContract.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ object ServiceContract {

const val ACTION_GESTURE_EVENT = "com.example.driveswipe.ACTION_GESTURE_EVENT"
const val ACTION_ENGINE_STATE_EVENT = "com.example.driveswipe.ACTION_ENGINE_STATE_EVENT"
const val ACTION_ROUTINE_START = "com.example.driveswipe.action.ROUTINE_START"
const val ACTION_ROUTINE_STOP = "com.example.driveswipe.action.ROUTINE_STOP"
const val EXTRA_EVENT_GESTURE = "EVENT_GESTURE"
const val EXTRA_EVENT_ACTION = "EVENT_ACTION"
const val EXTRA_ENGINE_STATE = "ENGINE_STATE"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<resources>
<string name="app_name">DriveSwipe</string>
<string name="shortcut_start_short_label">Start DriveSwipe</string>
<string name="shortcut_start_long_label">Start DriveSwipe listening</string>
<string name="shortcut_stop_short_label">Stop DriveSwipe</string>
<string name="shortcut_stop_long_label">Stop DriveSwipe listening</string>
</resources>
26 changes: 26 additions & 0 deletions app/src/main/res/xml/shortcuts.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:shortcutId="routine_start"
android:shortcutLongLabel="@string/shortcut_start_long_label"
android:shortcutShortLabel="@string/shortcut_start_short_label">
<intent
android:action="com.example.driveswipe.action.ROUTINE_START"
android:targetClass="com.example.driveswipe.RoutineTriggerActivity"
android:targetPackage="com.example.driveswipe" />
</shortcut>

<shortcut
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:shortcutId="routine_stop"
android:shortcutLongLabel="@string/shortcut_stop_long_label"
android:shortcutShortLabel="@string/shortcut_stop_short_label">
<intent
android:action="com.example.driveswipe.action.ROUTINE_STOP"
android:targetClass="com.example.driveswipe.RoutineTriggerActivity"
android:targetPackage="com.example.driveswipe" />
</shortcut>
</shortcuts>
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.1.1" apply false
id("com.android.application") version "8.6.1" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
}