Skip to content

Commit 0efdcdc

Browse files
Merge pull request #176 from andreknieriem/setting-wizard
Setting wizard
2 parents e11ce38 + b4ed392 commit 0efdcdc

30 files changed

Lines changed: 583 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
# Changelog
2+
### v.1.15.1
3+
- New Feature: Added **Auto-Optimization Wizard** to automatically find the best Resolution, DPI, and Codec settings for your hardware.
4+
- Bugfix: Fixed **Self Mode** failing to start in offline/hotspot scenarios (Network ID 0 fix).
5+
- Bugfix: Improved **Audio Routing**. The phone is now more likely to route audio to the headunit immediately upon connection by using an early-initialized MediaSession with remote playback metadata.
6+
- Bugfix: Fixed **GPS Speed** calculation. Speeds were previously doubled due to an incorrect unit conversion (knots instead of mm/s).
7+
- UI: Improved Settings readability on small screens by allowing multi-line descriptions.
8+
29
### v.1.15.0
310
- Added arabic language thanks to A5H0
411
- Added new intent for setting day/night mode for maps

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ adb shell am start -a android.intent.action.VIEW -d "headunit://connect?ip=192.1
6767
- **Google Maps in Portrait Mode:** Touch interactions (searching, scrolling) within Google Maps may not work as expected when using Portrait Mode. While visual feedback (like ripple effects) might appear, the map itself may remain unresponsive. This appears to be an internal Android Auto / Google Maps limitation or bug in vertical orientations.
6868

6969
## Changelog
70+
### v.1.15.1
71+
- New Feature: Added **Auto-Optimization Wizard** to automatically find the best Resolution, DPI, and Codec settings for your hardware.
72+
- Bugfix: Fixed **Self Mode** failing to start in offline/hotspot scenarios (Network ID 0 fix).
73+
- Bugfix: Improved **Audio Routing**. The phone is now more likely to route audio to the headunit immediately upon connection by using an early-initialized MediaSession with remote playback metadata.
74+
- Bugfix: Fixed **GPS Speed** calculation. Speeds were previously doubled due to an incorrect unit conversion (knots instead of mm/s).
75+
- UI: Improved Settings readability on small screens by allowing multi-line descriptions.
76+
7077
### v.1.15.0
7178
- Added arabic language thanks to A5H0
7279
- Added new intent for setting day/night mode for maps

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ android {
9696
minSdk = 16
9797
// minSdk = 21 // 21 only for google play console. App should work in minSDK 16
9898
targetSdk = 36
99-
versionCode = 48
100-
versionName = "1.15.0"
99+
versionCode = 49
100+
versionName = "1.15.1"
101101
setProperty("archivesBaseName", "${applicationId}_${versionName}")
102102
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
103103
multiDexEnabled = true

app/src/main/java/com/andrerinas/headunitrevived/aap/AapService.kt

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,23 @@ class AapService : Service(), UsbReceiver.Listener {
125125

126126
usbReceiver = UsbReceiver(this);
127127

128+
// Initialize MediaSession early to be ready for early focus requests
129+
mediaSession = MediaSessionCompat(this, "HeadunitRevived").apply {
130+
setCallback(object : MediaSessionCompat.Callback() {})
131+
setPlaybackToRemote(object : androidx.media.VolumeProviderCompat(
132+
androidx.media.VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, 100, 50
133+
) {
134+
override fun onAdjustVolume(direction: Int) {
135+
// Handle volume buttons from phone if needed
136+
}
137+
})
138+
setMetadata(android.support.v4.media.MediaMetadataCompat.Builder()
139+
.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_TITLE, "Android Auto")
140+
.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ARTIST, "Connected")
141+
.build())
142+
isActive = true
143+
}
144+
128145
nightModeManager = NightModeManager(this, App.provide(this).settings) { isNight ->
129146
AppLog.i("NightMode update: $isNight")
130147
App.provide(this).transport.send(NightModeEvent(isNight))
@@ -641,7 +658,7 @@ class AapService : Service(), UsbReceiver.Listener {
641658

642659
val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager;
643660
val activeNetwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) connectivityManager.activeNetwork else null;
644-
val networkToUse = activeNetwork ?: createFakeNetwork(99999);
661+
val networkToUse = activeNetwork ?: createFakeNetwork(0);
645662
val fakeWifiInfo = createFakeWifiInfo();
646663

647664
val magicalIntent = Intent().apply {
@@ -705,11 +722,6 @@ class AapService : Service(), UsbReceiver.Listener {
705722
updateNotification();
706723
sendBroadcast(ConnectedIntent());
707724

708-
// Create MediaSession to get higher audio priority
709-
mediaSession = MediaSessionCompat(this@AapService, "HeadunitRevived").apply {
710-
isActive = true
711-
}
712-
713725
transport.onAudioFocusStateChanged = { isPlaying ->
714726
updateMediaSessionState(isPlaying)
715727
}
@@ -761,13 +773,13 @@ class AapService : Service(), UsbReceiver.Listener {
761773
// Invalidate any in-flight attempts
762774
connectionAttemptId.incrementAndGet();
763775

764-
if (wirelessServer != null) {
776+
if (wirelessServer != null && !isClean && !isDestroying) {
765777
AppLog.i("AapService: Disconnected. Restarting discovery loop in 2s...");
766778
serviceScope.launch {
767779
delay(2000);
768780
if (!isConnected) startDiscovery();
769781
}
770-
} else if (!isClean) {
782+
} else if (!isClean && !isDestroying) {
771783
val mode = App.provide(this).settings.wifiConnectionMode
772784
val lastType = App.provide(this).settings.lastConnectionType
773785
if (mode == 1 && lastType == Settings.CONNECTION_TYPE_WIFI) { // Auto Mode, WiFi only

app/src/main/java/com/andrerinas/headunitrevived/aap/protocol/messages/LocationUpdateEvent.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class LocationUpdateEvent(location: Location)
1717
longitude = (location.longitude * 1E7).toInt()
1818
altitude = (location.altitude * 1E2).toInt()
1919
bearing = (location.bearing * 1E6).toInt()
20-
// AA expects speed in knots, so convert back
21-
speed = (location.speed * 1.94384 * 1E3).toInt()
20+
// AA expects speed in mm/s (m/s * 1000)
21+
speed = (location.speed * 1E3).toInt()
2222
accuracy = (location.accuracy * 1E3).toInt()
2323
}
2424
)

app/src/main/java/com/andrerinas/headunitrevived/main/HomeFragment.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,6 @@ class HomeFragment : Fragment() {
8484
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
8585
super.onViewCreated(view, savedInstanceState)
8686

87-
// Check Safety Disclaimer
88-
if (!App.provide(requireContext()).settings.hasAcceptedDisclaimer) {
89-
SafetyDisclaimerDialog.show(childFragmentManager)
90-
}
91-
9287
self_mode_button = view.findViewById(R.id.self_mode_button)
9388
usb = view.findViewById(R.id.usb_button)
9489
settings = view.findViewById(R.id.settings_button)

app/src/main/java/com/andrerinas/headunitrevived/main/MainActivity.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import com.andrerinas.headunitrevived.aap.AapService
1818
import com.andrerinas.headunitrevived.app.BaseActivity
1919
import com.andrerinas.headunitrevived.utils.AppLog
2020
import com.andrerinas.headunitrevived.utils.Settings
21+
import com.andrerinas.headunitrevived.utils.SetupWizard
22+
import com.andrerinas.headunitrevived.utils.SystemUI
2123

2224
class MainActivity : BaseActivity() {
2325

@@ -121,13 +123,15 @@ class MainActivity : BaseActivity() {
121123
private fun setFullscreen() {
122124
val root = findViewById<View>(R.id.root)
123125
val appSettings = Settings(this)
124-
com.andrerinas.headunitrevived.utils.SystemUI.apply(window, root, appSettings.startInFullscreenMode)
126+
SystemUI.apply(window, root, appSettings.startInFullscreenMode)
125127
}
126128

127129
override fun onResume() {
128130
super.onResume()
129131
setFullscreen()
130132

133+
checkSetupFlow()
134+
131135
// If an Android Auto session is active, bring the projection activity to front
132136
if (AapService.isConnected) {
133137
AppLog.i("MainActivity: Active session detected, bringing projection to front")
@@ -139,6 +143,18 @@ class MainActivity : BaseActivity() {
139143
}
140144
}
141145

146+
fun checkSetupFlow() {
147+
val appSettings = Settings(this)
148+
if (!appSettings.hasAcceptedDisclaimer) {
149+
SafetyDisclaimerDialog.show(supportFragmentManager)
150+
} else if (!appSettings.hasCompletedSetupWizard) {
151+
SetupWizard(this) {
152+
// Refresh activity after setup
153+
recreate()
154+
}.start()
155+
}
156+
}
157+
142158
override fun onWindowFocusChanged(hasFocus: Boolean) {
143159
super.onWindowFocusChanged(hasFocus)
144160
if (hasFocus) {
@@ -163,7 +179,7 @@ class MainActivity : BaseActivity() {
163179
super.onDestroy()
164180
if (isFinishing) {
165181
AppLog.i("MainActivity finishing, resetting auto-start flag.")
166-
com.andrerinas.headunitrevived.main.HomeFragment.resetAutoStart()
182+
HomeFragment.resetAutoStart()
167183
}
168184
}
169185

app/src/main/java/com/andrerinas/headunitrevived/main/SafetyDisclaimerDialog.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class SafetyDisclaimerDialog : DialogFragment() {
4141
// Save setting
4242
App.provide(requireContext()).settings.hasAcceptedDisclaimer = true
4343
dismiss()
44+
(activity as? MainActivity)?.checkSetupFlow()
4445
}
4546

4647
return view

app/src/main/java/com/andrerinas/headunitrevived/main/SettingsFragment.kt

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,18 @@ class SettingsFragment : Fragment() {
380380
// --- General Settings ---
381381
items.add(SettingItem.CategoryHeader("general", R.string.category_general))
382382

383+
// Auto-Optimize Wizard
384+
items.add(SettingItem.SettingEntry(
385+
stableId = "autoOptimize",
386+
nameResId = R.string.auto_optimize,
387+
value = getString(R.string.auto_optimize_desc),
388+
onClick = { _ ->
389+
com.andrerinas.headunitrevived.utils.SetupWizard(requireContext()) {
390+
requireActivity().recreate()
391+
}.start()
392+
}
393+
))
394+
383395
// Language Selector
384396
val availableLocales = LocaleHelper.getAvailableLocales(requireContext())
385397
val currentLocale = LocaleHelper.stringToLocale(pendingAppLanguage ?: "")
@@ -454,29 +466,20 @@ class SettingsFragment : Fragment() {
454466
nameResId = R.string.night_mode_threshold,
455467
value = "$currentValue $unit",
456468
onClick = { _ ->
457-
val editView = EditText(requireContext())
458-
editView.inputType = InputType.TYPE_CLASS_NUMBER
459-
editView.setText(currentValue.toString())
460-
461-
MaterialAlertDialogBuilder(requireContext(), R.style.DarkAlertDialog)
462-
.setTitle(R.string.enter_threshold_value)
463-
.setMessage(desc)
464-
.setView(editView)
465-
.setPositiveButton(android.R.string.ok) { dialog, _ ->
466-
val newVal = editView.text.toString().toIntOrNull()
467-
if (newVal != null && newVal >= 0) {
468-
if (isSensor) {
469-
pendingThresholdLux = newVal
470-
} else {
471-
pendingThresholdBrightness = newVal
472-
}
473-
checkChanges()
474-
updateSettingsList()
469+
showNumericInputDialog(
470+
title = getString(R.string.enter_threshold_value),
471+
message = desc,
472+
initialValue = currentValue ?: 0,
473+
onConfirm = { newVal ->
474+
if (isSensor) {
475+
pendingThresholdLux = newVal
476+
} else {
477+
pendingThresholdBrightness = newVal
475478
}
476-
dialog.dismiss()
479+
checkChanges()
480+
updateSettingsList()
477481
}
478-
.setNegativeButton(android.R.string.cancel, null)
479-
.show()
482+
)
480483
}
481484
))
482485
}
@@ -684,30 +687,16 @@ class SettingsFragment : Fragment() {
684687
nameResId = R.string.dpi,
685688
value = if (pendingDpi == 0) getString(R.string.auto) else pendingDpi.toString(),
686689
onClick = { _ ->
687-
val editView = EditText(requireContext())
688-
editView.inputType = InputType.TYPE_CLASS_NUMBER
689-
if (pendingDpi != 0) {
690-
editView.setText(pendingDpi.toString())
691-
}
692-
693-
AlertDialog.Builder(requireContext())
694-
.setTitle(R.string.enter_dpi_value)
695-
.setView(editView)
696-
.setPositiveButton(android.R.string.ok) { dialog, _ ->
697-
val inputText = editView.text.toString().trim()
698-
val newDpi = inputText.toIntOrNull()
699-
if (newDpi != null && newDpi >= 0) {
700-
pendingDpi = newDpi
701-
} else {
702-
pendingDpi = 0
703-
}
690+
showNumericInputDialog(
691+
title = getString(R.string.enter_dpi_value),
692+
message = null,
693+
initialValue = pendingDpi ?: 0,
694+
onConfirm = { newVal ->
695+
pendingDpi = newVal
704696
checkChanges()
705-
dialog.dismiss()
706697
updateSettingsList()
707698
}
708-
.setNegativeButton(R.string.cancel) { dialog, _ ->
709-
dialog.cancel()
710-
} .show()
699+
)
711700
}
712701
))
713702

@@ -1245,6 +1234,43 @@ class SettingsFragment : Fragment() {
12451234
.show()
12461235
}
12471236

1237+
private fun showNumericInputDialog(
1238+
title: String,
1239+
message: String?,
1240+
initialValue: Int,
1241+
onConfirm: (Int) -> Unit
1242+
) {
1243+
val context = requireContext()
1244+
val editView = EditText(context).apply {
1245+
inputType = InputType.TYPE_CLASS_NUMBER
1246+
setText(if (initialValue == 0 && title.contains("DPI", true)) "" else initialValue.toString())
1247+
}
1248+
1249+
// Use a container to add padding around the EditText
1250+
val container = android.widget.FrameLayout(context)
1251+
val params = android.widget.FrameLayout.LayoutParams(
1252+
android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
1253+
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
1254+
)
1255+
val margin = (24 * context.resources.displayMetrics.density).toInt()
1256+
params.setMargins(margin, 8, margin, 8)
1257+
container.addView(editView, params)
1258+
1259+
MaterialAlertDialogBuilder(context, R.style.DarkAlertDialog)
1260+
.setTitle(title)
1261+
.apply { if (message != null) setMessage(message) }
1262+
.setView(container)
1263+
.setPositiveButton(android.R.string.ok) { dialog, _ ->
1264+
val newVal = (editView.text.toString().toIntOrNull() ?: 0).coerceAtLeast(0)
1265+
onConfirm(newVal)
1266+
dialog.dismiss()
1267+
}
1268+
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
1269+
dialog.cancel()
1270+
}
1271+
.show()
1272+
}
1273+
12481274
companion object {
12491275
private val SAVE_ITEM_ID = 1001
12501276
}

app/src/main/java/com/andrerinas/headunitrevived/utils/Settings.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ class Settings(context: Context) {
205205
get() = prefs.getBoolean("has-accepted-disclaimer", false)
206206
set(value) { prefs.edit().putBoolean("has-accepted-disclaimer", value).apply() }
207207

208+
var hasCompletedSetupWizard: Boolean
209+
get() = prefs.getBoolean("has-completed-setup-wizard", false)
210+
set(value) { prefs.edit().putBoolean("has-completed-setup-wizard", value).apply() }
211+
208212
var autoConnectLastSession: Boolean
209213
get() = prefs.getBoolean("auto-connect-last-session", false)
210214
set(value) { prefs.edit().putBoolean("auto-connect-last-session", value).apply() }

0 commit comments

Comments
 (0)