From 9a4c03e2ece2de7304df8b726f1357cab9c19aee Mon Sep 17 00:00:00 2001 From: baileileicn Date: Tue, 29 Sep 2026 09:58:27 +0800 Subject: [PATCH] i18n P2: resourceize 361 hardcoded UI strings in 3 activities (en + zh-rCN) - Replace UI literals with getString(R.string.*) across DiPlayActivity, CarPlayHostActivity, ImageCropActivity (316 keys + 15 parameterized keys) - Generate values/strings.xml (338 entries) and values-zh-rCN/strings.xml - Parameterize templates (SSID/version/suffix prefixes) with format args - Merge concatenated Android 9/MFi hint strings into single resources - Protect leading/trailing spaces via \\u0020 so AAPT does not trim them - Rename reserved resource name 'native' to 'resolution_native' --- .../shilapi/xcertplay/CarPlayHostActivity.kt | 338 ++++++++--------- .../com/shilapi/xcertplay/DiPlayActivity.kt | 358 +++++++++--------- .../shilapi/xcertplay/ImageCropActivity.kt | 17 +- common/src/main/res/values-zh-rCN/strings.xml | 340 +++++++++++++++++ common/src/main/res/values/strings.xml | 331 ++++++++++++++++ 5 files changed, 1026 insertions(+), 358 deletions(-) create mode 100644 common/src/main/res/values-zh-rCN/strings.xml diff --git a/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt b/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt index bedd3cb..b038c70 100644 --- a/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt @@ -160,7 +160,7 @@ class CarPlayHostActivity : ComponentActivity() { vpnReady = true maybeStartCarPlay() } else { - setStatus("VPN consent was denied") + setStatus(getString(R.string.vpn_consent_was_denied)) } } private val wirelessPermissions = @@ -777,38 +777,38 @@ class CarPlayHostActivity : ComponentActivity() { isClickable = true } panel.addView(ImageView(this).apply { - setImageResource(R.drawable.ic_carplay); contentDescription = "CarPlay" + setImageResource(R.drawable.ic_carplay); contentDescription = getString(R.string.carplay) }, LinearLayout.LayoutParams(dp(88), dp(88))) panel.addView(TextView(this).apply { - text = "DiPlay"; textSize = 34f; setTextColor(Color.rgb(241, 245, 252)) + text = getString(R.string.diplay); textSize = 34f; setTextColor(Color.rgb(241, 245, 252)) gravity = Gravity.CENTER; setPadding(0, dp(18), 0, dp(14)) typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL) }) val stage = TextView(this).apply { - text = "Getting CarPlay ready…"; textSize = 22f; gravity = Gravity.CENTER + text = getString(R.string.getting_carplay_ready); textSize = 22f; gravity = Gravity.CENTER setTextColor(Color.rgb(241, 245, 252)) } panel.addView(stage) panel.addView(TextView(this).apply { - text = if (wirelessEnabled) "Keep your iPhone nearby with Bluetooth and Wi-Fi on.\nAllow CarPlay if your iPhone asks." - else "Use a USB data cable and unlock your iPhone.\nAllow Trust and CarPlay if your iPhone asks." + text = if (wirelessEnabled) getString(R.string.keep_your_iphone_nearby_with_bluetooth_and_wi_fi_on_allow) + else getString(R.string.use_a_usb_data_cable_and_unlock_your_iphone_allow_trust_an) textSize = 17f; gravity = Gravity.CENTER; setTextColor(Color.rgb(168, 182, 202)) setPadding(0, dp(14), 0, dp(24)) }) panel.addView(Button(this).apply { - text = "Reset CarPlay Wi-Fi"; isAllCaps = false; textSize = 18f + text = getString(R.string.reset_carplay_wi_fi); isAllCaps = false; textSize = 18f visibility = View.GONE setOnClickListener { showDiPlayHome("wireless-recovery") } wifiRecoveryButton = this }, LinearLayout.LayoutParams(dp(300), dp(64)).apply { bottomMargin = dp(12) }) panel.addView(Button(this).apply { - text = "Back to DiPlay"; isAllCaps = false; textSize = 18f + text = getString(R.string.back_to_diplay); isAllCaps = false; textSize = 18f setTextColor(Color.rgb(12, 17, 27)) background = GradientDrawable().apply { setColor(Color.rgb(166, 200, 255)); cornerRadius = dp(20).toFloat() } setOnClickListener { showDiPlayHome() } }, LinearLayout.LayoutParams(dp(300), dp(64))) panel.addView(TextView(this).apply { - text = "In CarPlay, swipe down with three fingers to open DiPlay settings." + text = getString(R.string.in_carplay_swipe_down_with_three_fingers_to_open_diplay_se) textSize = 13f; gravity = Gravity.CENTER; setTextColor(Color.rgb(168, 182, 202)); setPadding(0, dp(20), 0, 0) }) root.addView(panel, FrameLayout.LayoutParams(-1, -1)) @@ -834,7 +834,7 @@ class CarPlayHostActivity : ComponentActivity() { setPadding(dp(48), dp(36), dp(48), dp(36)) } content.addView( - menuText("CarPlay settings", 32f, Color.WHITE, bold = true).apply { + menuText(getString(R.string.carplay_settings), 32f, Color.WHITE, bold = true).apply { setPadding(dp(56), 0, 0, 0) }, LinearLayout.LayoutParams( @@ -843,7 +843,7 @@ class CarPlayHostActivity : ComponentActivity() { ), ) content.addView( - settingsCategoryHeader("Connection"), + settingsCategoryHeader(getString(R.string.connection)), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -863,12 +863,12 @@ class CarPlayHostActivity : ComponentActivity() { gravity = Gravity.CENTER_VERTICAL } wirelessRow.addView( - menuText("Wireless CarPlay", 20f, MENU_SECONDARY), + menuText(getString(R.string.wireless_carplay_2), 20f, MENU_SECONDARY), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f), ) val wirelessSwitch = Switch(this).apply { isChecked = wirelessEnabled - contentDescription = "Wireless CarPlay transport" + contentDescription = getString(R.string.wireless_carplay_transport) showText = false thumbTintList = ColorStateList( arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), @@ -914,7 +914,7 @@ class CarPlayHostActivity : ComponentActivity() { ) content.addView( - menuText("Hotspot status", 20f, MENU_SECONDARY), + menuText(getString(R.string.hotspot_status), 20f, MENU_SECONDARY), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -930,7 +930,7 @@ class CarPlayHostActivity : ComponentActivity() { ) content.addView( - settingsCategoryHeader("Location"), + settingsCategoryHeader(getString(R.string.location)), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -945,7 +945,7 @@ class CarPlayHostActivity : ComponentActivity() { ) content.addView( - settingsCategoryHeader("Startup"), + settingsCategoryHeader(getString(R.string.startup)), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -953,9 +953,9 @@ class CarPlayHostActivity : ComponentActivity() { ) content.addView( settingsSwitchRow( - label = "Auto-start on boot", + label = getString(R.string.auto_start_on_boot), checked = autoStartOnBoot, - description = "Start CarPlay automatically after device boot", + description = getString(R.string.start_carplay_automatically_after_device_boot), ) { checked -> autoStartOnBoot = checked appendLog("Boot auto-start ${if (checked) "enabled" else "disabled"}") @@ -968,7 +968,7 @@ class CarPlayHostActivity : ComponentActivity() { if (advancedAudioChannelMappingSupported) { content.addView( - settingsCategoryHeader("Audio"), + settingsCategoryHeader(getString(R.string.audio)), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -976,9 +976,9 @@ class CarPlayHostActivity : ComponentActivity() { ) content.addView( settingsSwitchRow( - label = "Advanced audio channel mapping", + label = getString(R.string.advanced_audio_channel_mapping), checked = advancedAudioChannelMapping, - description = "Use usage / content-type routing instead of stream type", + description = getString(R.string.use_usage_content_type_routing_instead_of_stream_type), ) { checked -> advancedAudioChannelMapping = checked appendLog( @@ -995,7 +995,7 @@ class CarPlayHostActivity : ComponentActivity() { } content.addView( - settingsCategoryHeader("Identity & appearance"), + settingsCategoryHeader(getString(R.string.identity_appearance)), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1023,7 +1023,7 @@ class CarPlayHostActivity : ComponentActivity() { ).apply { topMargin = dp(26) }, ) content.addView( - settingsCategoryHeader("Display & video"), + settingsCategoryHeader(getString(R.string.display_video)), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1035,7 +1035,7 @@ class CarPlayHostActivity : ComponentActivity() { gravity = Gravity.CENTER_VERTICAL } resolutionHeader.addView( - menuText("Resolution", 20f, MENU_SECONDARY), + menuText(getString(R.string.resolution), 20f, MENU_SECONDARY), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f), ) val resolutionValue = menuText( @@ -1091,11 +1091,11 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.HORIZONTAL } range.addView( - menuText("0.3x", 15f, MENU_SECONDARY), + menuText(getString(R.string.s_0_3x), 15f, MENU_SECONDARY), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f), ) range.addView( - menuText("1.0x", 15f, MENU_SECONDARY), + menuText(getString(R.string.s_1_0x), 15f, MENU_SECONDARY), LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1111,7 +1111,7 @@ class CarPlayHostActivity : ComponentActivity() { content.addView( buildStepSliderSection( - title = "Frame rate", + title = getString(R.string.frame_rate), values = ( AirPlayDisplaySettings.MIN_FPS..AirPlayDisplaySettings.MAX_FPS step AirPlayDisplaySettings.FPS_STEP @@ -1131,10 +1131,10 @@ class CarPlayHostActivity : ComponentActivity() { content.addView( settingsChoiceRow( - label = "Physical size basis", + label = getString(R.string.physical_size_basis), options = listOf( - AirPlayPhysicalSizeBasis.WIDTH to "Widest width", - AirPlayPhysicalSizeBasis.HEIGHT to "Longest height", + AirPlayPhysicalSizeBasis.WIDTH to getString(R.string.widest_width), + AirPlayPhysicalSizeBasis.HEIGHT to getString(R.string.longest_height), ), selected = physicalSizeBasis, ) { value -> @@ -1149,7 +1149,7 @@ class CarPlayHostActivity : ComponentActivity() { content.addView( buildStepSliderSection( - title = "Physical length", + title = getString(R.string.physical_length), values = ( AirPlayDisplaySettings.MIN_WIDTH_PHYSICAL_MM.. AirPlayDisplaySettings.MAX_WIDTH_PHYSICAL_MM @@ -1178,7 +1178,7 @@ class CarPlayHostActivity : ComponentActivity() { ) val hevcSwitch = Switch(this).apply { isChecked = hevcEnabled - contentDescription = "HEVC H.265 video transport" + contentDescription = getString(R.string.hevc_h_265_video_transport) showText = false thumbTintList = ColorStateList( arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), @@ -1218,12 +1218,12 @@ class CarPlayHostActivity : ComponentActivity() { gravity = Gravity.CENTER_VERTICAL } softwareHevcRow.addView( - menuText("HEVC software decoder", 20f, MENU_SECONDARY), + menuText(getString(R.string.hevc_software_decoder), 20f, MENU_SECONDARY), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f), ) val softwareHevcSwitch = Switch(this).apply { isChecked = hevcSoftwareDecoderEnabled - contentDescription = "Use software HEVC decoder" + contentDescription = getString(R.string.use_software_hevc_decoder) showText = false thumbTintList = ColorStateList( arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), @@ -1269,7 +1269,7 @@ class CarPlayHostActivity : ComponentActivity() { ) content.addView( - settingsCategoryHeader("Window"), + settingsCategoryHeader(getString(R.string.window)), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1284,7 +1284,7 @@ class CarPlayHostActivity : ComponentActivity() { ) content.addView( - settingsCategoryHeader("Diagnostics"), + settingsCategoryHeader(getString(R.string.diagnostics)), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1300,7 +1300,7 @@ class CarPlayHostActivity : ComponentActivity() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { content.addView( - settingsCategoryHeader("Android 9 compatibility"), + settingsCategoryHeader(getString(R.string.android_9_compatibility)), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1308,10 +1308,7 @@ class CarPlayHostActivity : ComponentActivity() { ) content.addView( menuText( - "The following settings are unavailable and hidden on Android 9 " + - "(API 28):\n" + - "• Wi-Fi P2P (5 GHz) — LocalOnlyHotspot is used instead.\n" + - "• HEVC software decoder — hardware decoding is used instead.", + getString(R.string.settings_android9_compat), 16f, MENU_SECONDARY, ), @@ -1332,7 +1329,7 @@ class CarPlayHostActivity : ComponentActivity() { ) val save = Button(this).apply { - text = "Save and reconnect" + text = getString(R.string.save_and_reconnect) isAllCaps = false textSize = 17f setTextColor(MENU_BUTTON_TEXT) @@ -1349,7 +1346,7 @@ class CarPlayHostActivity : ComponentActivity() { ) val exitApplicationButton = Button(this).apply { - text = "EXIT APPLICATION" + text = getString(R.string.exit_application) isAllCaps = false textSize = 17f setTextColor(Color.WHITE) @@ -1389,7 +1386,7 @@ class CarPlayHostActivity : ComponentActivity() { textSize = 22f setTextColor(Color.WHITE) backgroundTintList = ColorStateList.valueOf(MENU_TRACK_OFF) - contentDescription = "Discard changes and exit settings" + contentDescription = getString(R.string.discard_changes_and_exit_settings) minWidth = 0 minHeight = 0 setPadding(0, 0, 0, 0) @@ -1518,11 +1515,11 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL } val targetChoice = settingsChoiceRow( - label = "MFI certificate & signing target", + label = getString(R.string.mfi_certificate_signing_target), options = listOf( - MfiTarget.USB_CH341 to "USB/CH341", - MfiTarget.I2C to "I2C", - MfiTarget.REMOTE to "Remote", + MfiTarget.USB_CH341 to getString(R.string.usb_ch341), + MfiTarget.I2C to getString(R.string.i2c), + MfiTarget.REMOTE to getString(R.string.remote), ), selected = mfiTarget, ) { target -> @@ -1544,7 +1541,7 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL addView( settingsInputRow( - "I2C device", + getString(R.string.i2c_device), mfiI2cPath, onInputCreated = { mfiI2cPathInput = it }, ) { value -> @@ -1557,7 +1554,7 @@ class CarPlayHostActivity : ComponentActivity() { ), ) addView( - menuText("Linux device path, for example /dev/i2c-1.", 14f, MENU_SECONDARY), + menuText(getString(R.string.linux_device_path_for_example_dev_i2c_1), 14f, MENU_SECONDARY), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1577,7 +1574,7 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL addView( settingsInputRow( - "Server address", + getString(R.string.server_address), remoteMfiServer, onInputCreated = { remoteMfiServerInput = it }, ) { value -> @@ -1590,7 +1587,7 @@ class CarPlayHostActivity : ComponentActivity() { ) addView( settingsInputRow( - "Token (optional)", + getString(R.string.token_optional), remoteMfiToken, password = true, onInputCreated = { remoteMfiTokenInput = it }, @@ -1604,8 +1601,7 @@ class CarPlayHostActivity : ComponentActivity() { ) addView( menuText( - "Address must start with http:// or https://. Token is optional and sent " + - "as an Authorization bearer token.", + getString(R.string.settings_mfi_address_hint), 14f, MENU_SECONDARY, ), @@ -1665,10 +1661,10 @@ class CarPlayHostActivity : ComponentActivity() { } private fun mfiTargetLabel(target: MfiTarget): String = when (target) { - MfiTarget.LOCAL -> "Local offline" - MfiTarget.USB_CH341 -> "USB/CH341" - MfiTarget.I2C -> "I2C" - MfiTarget.REMOTE -> "Remote" + MfiTarget.LOCAL -> getString(R.string.local_offline) + MfiTarget.USB_CH341 -> getString(R.string.usb_ch341) + MfiTarget.I2C -> getString(R.string.i2c) + MfiTarget.REMOTE -> getString(R.string.remote) } private fun buildIdentitySettingsSection(): View { @@ -1676,7 +1672,7 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL } section.addView( - settingsInputRow("Manufacturer", manufacturer) { value -> + settingsInputRow(getString(R.string.manufacturer), manufacturer) { value -> manufacturer = value updateResolutionMenu() }, @@ -1686,7 +1682,7 @@ class CarPlayHostActivity : ComponentActivity() { ), ) section.addView( - settingsInputRow("Model", model) { value -> + settingsInputRow(getString(R.string.model), model) { value -> model = value updateResolutionMenu() }, @@ -1696,7 +1692,7 @@ class CarPlayHostActivity : ComponentActivity() { ).apply { topMargin = dp(10) }, ) section.addView( - settingsInputRow("OEM label", oemLabel) { value -> + settingsInputRow(getString(R.string.oem_label), oemLabel) { value -> oemLabel = value updateResolutionMenu() }, @@ -1719,12 +1715,12 @@ class CarPlayHostActivity : ComponentActivity() { gravity = Gravity.CENTER_VERTICAL } row.addView( - menuText("Report location to iPhone", 20f, MENU_SECONDARY), + menuText(getString(R.string.report_location_to_iphone), 20f, MENU_SECONDARY), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f), ) val switch = Switch(this@CarPlayHostActivity).apply { isChecked = locationReportingEnabled - contentDescription = "Report Android location to the iPhone" + contentDescription = getString(R.string.report_android_location_to_the_iphone) showText = false thumbTintList = ColorStateList( arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), @@ -1755,7 +1751,7 @@ class CarPlayHostActivity : ComponentActivity() { ) addView( menuText( - "Sends precise Android location as CarPlay GPS data when the iPhone requests it.", + getString(R.string.sends_precise_android_location_as_carplay_gps_data_when_th), 14f, MENU_SECONDARY, ), @@ -1781,9 +1777,9 @@ class CarPlayHostActivity : ComponentActivity() { private fun buildDebugLogsSection(): View = settingsSwitchRow( - label = "Debug logs", + label = getString(R.string.debug_logs), checked = debugLogsEnabled, - description = "Show on-screen debug logs", + description = getString(R.string.show_on_screen_debug_logs), ) { checked -> debugLogsEnabled = checked appendLog("Debug logs ${if (debugLogsEnabled) "enabled" else "disabled"}") @@ -1860,7 +1856,7 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL } section.addView( - menuText("AirPlay icon", 20f, MENU_SECONDARY), + menuText(getString(R.string.airplay_icon), 20f, MENU_SECONDARY), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1887,7 +1883,7 @@ class CarPlayHostActivity : ComponentActivity() { } actions.addView( Button(this).apply { - text = "Choose image" + text = getString(R.string.choose_image) isAllCaps = false setOnClickListener { externalActivityInProgress = true @@ -1901,7 +1897,7 @@ class CarPlayHostActivity : ComponentActivity() { ) actions.addView( Button(this).apply { - text = "Default icon" + text = getString(R.string.default_icon) isAllCaps = false setOnClickListener { AirPlayPersistence.clearCustomAirPlayIcon(this@CarPlayHostActivity) @@ -1947,7 +1943,7 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL } section.addView( - menuText("Driving side", 20f, MENU_SECONDARY), + menuText(getString(R.string.driving_side), 20f, MENU_SECONDARY), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1958,13 +1954,13 @@ class CarPlayHostActivity : ComponentActivity() { } val left = RadioButton(this).apply { id = View.generateViewId() - text = "Left-hand drive" + text = getString(R.string.left_hand_drive) setTextColor(Color.WHITE) isChecked = !rightHandDrive } val right = RadioButton(this).apply { id = View.generateViewId() - text = "Right-hand drive" + text = getString(R.string.right_hand_drive) setTextColor(Color.WHITE) isChecked = rightHandDrive } @@ -1989,7 +1985,7 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL } section.addView( - menuText("Fullscreen", 20f, MENU_SECONDARY), + menuText(getString(R.string.fullscreen), 20f, MENU_SECONDARY), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -1997,9 +1993,9 @@ class CarPlayHostActivity : ComponentActivity() { ) section.addView( settingsSwitchRow( - label = "Hide top bar", + label = getString(R.string.hide_top_bar), checked = hideTopBar, - description = "Hide the status bar", + description = getString(R.string.hide_the_status_bar), ) { checked -> hideTopBar = checked applyFullscreenMode() @@ -2012,9 +2008,9 @@ class CarPlayHostActivity : ComponentActivity() { ) section.addView( settingsSwitchRow( - label = "Hide bottom bar", + label = getString(R.string.hide_bottom_bar), checked = hideBottomBar, - description = "Hide the navigation bar", + description = getString(R.string.hide_the_navigation_bar), ) { checked -> hideBottomBar = checked applyFullscreenMode() @@ -2033,7 +2029,7 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL } section.addView( - menuText("Safe area", 20f, MENU_SECONDARY), + menuText(getString(R.string.safe_area), 20f, MENU_SECONDARY), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -2052,7 +2048,7 @@ class CarPlayHostActivity : ComponentActivity() { } buttons.addView( Button(this).apply { - text = "Set" + text = getString(R.string.set) isAllCaps = false setOnClickListener { openSafeAreaEditor() } }, @@ -2060,7 +2056,7 @@ class CarPlayHostActivity : ComponentActivity() { ) buttons.addView( Button(this).apply { - text = "Reset" + text = getString(R.string.reset) isAllCaps = false setOnClickListener { resetSafeAreaForCurrentSize() } }, @@ -2077,9 +2073,9 @@ class CarPlayHostActivity : ComponentActivity() { ) section.addView( settingsSwitchRow( - label = "Draw outside safe area", + label = getString(R.string.draw_outside_safe_area), checked = safeAreaDrawOutside, - description = "Allow CarPlay UI outside the safe area", + description = getString(R.string.allow_carplay_ui_outside_the_safe_area), ) { checked -> safeAreaDrawOutside = checked updateResolutionMenu() @@ -2108,7 +2104,7 @@ class CarPlayHostActivity : ComponentActivity() { ), ) overlay.addView( - menuText("Safe area", 24f, Color.WHITE, bold = true).apply { + menuText(getString(R.string.safe_area), 24f, Color.WHITE, bold = true).apply { setPadding(dp(16), dp(12), dp(16), dp(8)) }, FrameLayout.LayoutParams( @@ -2124,7 +2120,7 @@ class CarPlayHostActivity : ComponentActivity() { } controls.addView( Button(this).apply { - text = "Cancel" + text = getString(R.string.cancel) isAllCaps = false setOnClickListener { closeSafeAreaEditor() } }, @@ -2132,7 +2128,7 @@ class CarPlayHostActivity : ComponentActivity() { ) controls.addView( Button(this).apply { - text = "Save" + text = getString(R.string.save) isAllCaps = false setOnClickListener { saveSafeAreaEditor() } }, @@ -2258,7 +2254,7 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL } section.addView( - menuText("Wi-Fi session", 20f, MENU_SECONDARY), + menuText(getString(R.string.wi_fi_session), 20f, MENU_SECONDARY), LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, @@ -2271,7 +2267,7 @@ class CarPlayHostActivity : ComponentActivity() { } val modes = buildList { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - add(WirelessHotspotMode.WIFI_P2P to "Wi-Fi P2P (5 GHz)") + add(WirelessHotspotMode.WIFI_P2P to getString(R.string.wi_fi_p2p_5_ghz)) } add(WirelessHotspotMode.MANUAL to "Built-in car hotspot") } @@ -2325,7 +2321,7 @@ class CarPlayHostActivity : ComponentActivity() { orientation = LinearLayout.VERTICAL } manualFields.addView( - settingsInputRow("Hotspot SSID", manualHotspotSsid) { value -> + settingsInputRow(getString(R.string.hotspot_ssid), manualHotspotSsid) { value -> manualHotspotSsid = value manualHotspotErrorView?.visibility = View.GONE }, @@ -2337,11 +2333,11 @@ class CarPlayHostActivity : ComponentActivity() { manualFields.addView( settingsChoiceRow( - label = "Band", + label = getString(R.string.band), options = listOf( - ManualHotspotBand.AUTO to "Auto", - ManualHotspotBand.GHZ_2_4 to "2.4 GHz", - ManualHotspotBand.GHZ_5 to "5 GHz", + ManualHotspotBand.AUTO to getString(R.string.auto), + ManualHotspotBand.GHZ_2_4 to getString(R.string.s_2_4_ghz), + ManualHotspotBand.GHZ_5 to getString(R.string.s_5_ghz), ), selected = manualHotspotBand, ) { value -> @@ -2356,7 +2352,7 @@ class CarPlayHostActivity : ComponentActivity() { manualFields.addView( settingsInputRow( - label = "Channel (0 = auto)", + label = getString(R.string.channel_0_auto), value = manualHotspotChannel.toString(), numeric = true, ) { value -> @@ -2371,7 +2367,7 @@ class CarPlayHostActivity : ComponentActivity() { manualFields.addView( settingsInputRow( - label = "Hotspot password", + label = getString(R.string.hotspot_password), value = manualHotspotPassphrase, password = true, ) { value -> @@ -2386,12 +2382,12 @@ class CarPlayHostActivity : ComponentActivity() { manualFields.addView( settingsChoiceRow( - label = "Security", + label = getString(R.string.security), options = listOf( - ManualHotspotSecurity.OPEN to "Open", - ManualHotspotSecurity.WPA2 to "WPA2", - ManualHotspotSecurity.WPA3_TRANSITION to "WPA3 transition", - ManualHotspotSecurity.WPA3 to "WPA3", + ManualHotspotSecurity.OPEN to getString(R.string.open), + ManualHotspotSecurity.WPA2 to getString(R.string.wpa2), + ManualHotspotSecurity.WPA3_TRANSITION to getString(R.string.wpa3_transition), + ManualHotspotSecurity.WPA3 to getString(R.string.wpa3), ), selected = manualHotspotSecurity, ) { value -> @@ -2437,16 +2433,16 @@ class CarPlayHostActivity : ComponentActivity() { private fun validateMfiSettings(): Boolean { val error = when { mfiTarget == MfiTarget.I2C && mfiI2cPath.isBlank() -> - "I2C device path is required" + getString(R.string.i2c_device_path_is_required) mfiTarget == MfiTarget.REMOTE && remoteMfiServer.isBlank() -> - "Remote server address is required" + getString(R.string.remote_server_address_is_required) mfiTarget == MfiTarget.REMOTE && !remoteMfiServer.trim().startsWith("http://") && !remoteMfiServer.trim().startsWith("https://") -> - "Remote server address must start with http:// or https://" - '\u0000' in mfiI2cPath -> "I2C device path contains U+0000" - '\u0000' in remoteMfiServer -> "Remote server address contains U+0000" - '\u0000' in remoteMfiToken -> "Remote token contains U+0000" + getString(R.string.remote_server_address_must_start_with_http_or_https) + '\u0000' in mfiI2cPath -> getString(R.string.i2c_device_path_contains_u_0000) + '\u0000' in remoteMfiServer -> getString(R.string.remote_server_address_contains_u_0000) + '\u0000' in remoteMfiToken -> getString(R.string.remote_token_contains_u_0000) else -> null } mfiErrorView?.text = error.orEmpty() @@ -2457,21 +2453,21 @@ class CarPlayHostActivity : ComponentActivity() { private fun validateManualHotspotSettings(): Boolean { if (wirelessHotspotMode != WirelessHotspotMode.MANUAL) return true val error = when { - manualHotspotSsid.isBlank() -> "Hotspot SSID is required" + manualHotspotSsid.isBlank() -> getString(R.string.hotspot_ssid_is_required) manualHotspotSsid.encodeToByteArray().size > 32 -> - "Hotspot SSID must be at most 32 UTF-8 bytes" - '\u0000' in manualHotspotSsid -> "Hotspot SSID contains U+0000" - manualHotspotChannel !in 0..196 -> "Channel must be 0 or 1-196" + getString(R.string.hotspot_ssid_must_be_at_most_32_utf_8_bytes) + '\u0000' in manualHotspotSsid -> getString(R.string.hotspot_ssid_contains_u_0000) + manualHotspotChannel !in 0..196 -> getString(R.string.channel_must_be_0_or_1_196) manualHotspotChannel != 0 && !isManualHotspotChannelCompatible(manualHotspotBand, manualHotspotChannel) -> - "Channel is not valid for the selected band" - '\u0000' in manualHotspotPassphrase -> "Hotspot password contains U+0000" + getString(R.string.channel_is_not_valid_for_the_selected_band) + '\u0000' in manualHotspotPassphrase -> getString(R.string.hotspot_password_contains_u_0000) manualHotspotSecurity == ManualHotspotSecurity.OPEN && manualHotspotPassphrase.isNotEmpty() -> - "Password must be empty when security is Open" + getString(R.string.password_must_be_empty_when_security_is_open) manualHotspotSecurity != ManualHotspotSecurity.OPEN && manualHotspotPassphrase.length !in 8..63 -> - "WPA2/WPA3 password must be 8-63 characters" + getString(R.string.wpa2_wpa3_password_must_be_8_63_characters) else -> null } manualHotspotErrorView?.text = error.orEmpty() @@ -2480,9 +2476,9 @@ class CarPlayHostActivity : ComponentActivity() { } private fun hotspotModeLabel(mode: WirelessHotspotMode): String = when (mode) { - WirelessHotspotMode.WIFI_P2P -> "Wi-Fi P2P (5 GHz)" - WirelessHotspotMode.LOCAL_ONLY_HOTSPOT -> "LocalOnlyHotspot" - WirelessHotspotMode.MANUAL -> "Manual hotspot" + WirelessHotspotMode.WIFI_P2P -> getString(R.string.wi_fi_p2p_5_ghz) + WirelessHotspotMode.LOCAL_ONLY_HOTSPOT -> getString(R.string.localonlyhotspot) + WirelessHotspotMode.MANUAL -> getString(R.string.manual_hotspot) } private fun menuText( @@ -2501,25 +2497,25 @@ class CarPlayHostActivity : ComponentActivity() { private fun updateHotspotStatus(status: CarPlayStatus) { if (!wirelessEnabled) return hotspotStatus = when (status) { - CarPlayStatus.StartingHotspot -> HotspotStatus(state = "Starting") + CarPlayStatus.StartingHotspot -> HotspotStatus(state = getString(R.string.starting)) is CarPlayStatus.HotspotReady -> HotspotStatus( - state = "Ready", + state = getString(R.string.ready), ssid = status.ssid, band = status.band, channel = status.channel, backend = status.backend, ) CarPlayStatus.WaitingForPairedIphone -> - hotspotStatus.copy(state = "Waiting for paired iPhone") + hotspotStatus.copy(state = getString(R.string.waiting_for_paired_iphone)) CarPlayStatus.ConnectingBluetooth -> - hotspotStatus.copy(state = "Connecting Bluetooth") + hotspotStatus.copy(state = getString(R.string.connecting_bluetooth)) CarPlayStatus.RunningWireless -> - hotspotStatus.copy(state = "Running") + hotspotStatus.copy(state = getString(R.string.running)) CarPlayStatus.WirelessActive -> - hotspotStatus.copy(state = "Active") + hotspotStatus.copy(state = getString(R.string.active)) CarPlayStatus.AttachingNetwork -> - hotspotStatus.copy(state = "Starting AirPlay service") - is CarPlayStatus.Failed -> hotspotStatus.copy(state = "Error") + hotspotStatus.copy(state = getString(R.string.starting_airplay_service)) + is CarPlayStatus.Failed -> hotspotStatus.copy(state = getString(R.string.error)) else -> return } updateHotspotStatusBlock() @@ -2527,7 +2523,7 @@ class CarPlayHostActivity : ComponentActivity() { private fun updateHotspotStatusBlock() { if (!wirelessEnabled) { - hotspotStatusView?.text = "Wireless hotspot: off" + hotspotStatusView?.text = getString(R.string.wireless_hotspot_off) return } val status = hotspotStatus @@ -2602,7 +2598,7 @@ class CarPlayHostActivity : ComponentActivity() { resolutionValueView?.text = CarPlayDisplayScale.label(displayScaleTenths) val native = activeDisplaySize ?: currentActivitySize() val resolution = if (native == null) { - "Handshake resolution: waiting for display" + getString(R.string.handshake_resolution_waiting_for_display) } else { val negotiated = CarPlayDisplayScale.apply( AirPlayDisplayConfig( @@ -2613,7 +2609,7 @@ class CarPlayHostActivity : ComponentActivity() { ), displayScaleTenths, ) - "Handshake resolution: ${native.width} x ${native.height} -> " + + "${getString(R.string.resolution_handshake_prefix)}${native.width} x ${native.height} -> " + "${negotiated.widthPixels} x ${negotiated.heightPixels}" } val transport = if (!hevcEnabled) { @@ -2727,7 +2723,7 @@ class CarPlayHostActivity : ComponentActivity() { appendLog("Larger CarPlay canvas unavailable reason=${support.reason}; using Default icon and text size") runOnUiThread { android.widget.Toast.makeText(this, - "This head unit cannot use the smaller size at this resolution. Using Default.", + getString(R.string.this_head_unit_cannot_use_the_smaller_size_at_this_resolut), android.widget.Toast.LENGTH_LONG).show() } } @@ -2814,7 +2810,7 @@ class CarPlayHostActivity : ComponentActivity() { val bitmap = customBitmap ?: BitmapFactory.decodeResource(resources, R.raw.placeholder_icon) preview.setImageBitmap(bitmap) iconStatusView?.text = - if (customBitmap != null) "Custom 1:1 icon" else "Default placeholder icon" + if (customBitmap != null) getString(R.string.custom_1_1_icon) else getString(R.string.default_placeholder_icon) } private fun currentActivitySize(): DisplaySize? { @@ -2836,12 +2832,12 @@ class CarPlayHostActivity : ComponentActivity() { ) private fun safeAreaSummary(): String { - val size = currentActivitySize() ?: return "Safe area: waiting for activity size" + val size = currentActivitySize() ?: return getString(R.string.safe_area_waiting_for_activity_size) val mapping = AirPlayPersistence.loadSafeAreaRect(this, size.width, size.height) return if (mapping == null) { - "Safe area: full screen at ${size.width} x ${size.height}" + "${getString(R.string.safe_area_full_screen_at)}${size.width} x ${size.height}" } else { - "Safe area: ${mapping.width} x ${mapping.height} at " + + "${getString(R.string.safe_area_prefix)}${mapping.width} x ${mapping.height} at " + "(${mapping.left}, ${mapping.top}) in ${size.width} x ${size.height}" } } @@ -2973,7 +2969,7 @@ class CarPlayHostActivity : ComponentActivity() { return@runOnUiThread } activeScreenStreamTypes.clear() - setConnectionStage("CarPlay session ended; reconnecting") + setConnectionStage(getString(R.string.carplay_session_ended_reconnecting)) appendLog("AirPlay session ended; reconnecting from scratch") reconnectAfterLoss("AirPlay session ended") } @@ -2985,7 +2981,7 @@ class CarPlayHostActivity : ComponentActivity() { return@runOnUiThread } activeScreenStreamTypes.clear() - setConnectionStage("Transport error; reconnecting") + setConnectionStage(getString(R.string.transport_error_reconnecting)) appendLog("CarPlay transport error: $message; reconnecting from scratch") reconnectAfterLoss("CarPlay transport error: $message") } @@ -3064,9 +3060,9 @@ class CarPlayHostActivity : ComponentActivity() { ) setConnectionStage( if (serviceReused) { - "CarPlay service already running" + getString(R.string.carplay_service_already_running) } else { - "CarPlay session already running" + getString(R.string.carplay_session_already_running) }, ) updateDebugOverlays() @@ -3147,7 +3143,7 @@ class CarPlayHostActivity : ComponentActivity() { } catch (error: RuntimeException) { appendLog("Connection could not start: ${error.javaClass.simpleName}") shutdown(false, "foreground service could not start") - setConnectionStage("Could not start CarPlay. Return to DiPlay and check app permissions.") + setConnectionStage(getString(R.string.could_not_start_carplay_return_to_diplay_and_check_app_per)) } } @@ -3489,21 +3485,21 @@ class CarPlayHostActivity : ComponentActivity() { } private fun friendlyStage(message: String): String = when { - message.contains("Turn on Wi-Fi", true) -> "Turn on Wi-Fi in the head unit’s settings to connect." - message.contains("Allow precise Location", true) -> "Allow precise Location for DiPlay in the head unit’s app permissions." - message.contains("Allow Nearby devices", true) -> "Allow Nearby devices for DiPlay in the head unit’s app permissions." - message.contains("createGroup failed", true) -> "The head unit couldn’t start CarPlay Wi-Fi. Check Wi-Fi and close other projection apps. Retrying…" - message.contains("needs a reset", true) -> "A previous Wi-Fi Direct connection is still running. Reset it to connect." - message.contains("socket", true) || message.contains("RFCOMM", true) -> "Your iPhone isn’t available. Unlock it and check Bluetooth." - message.contains("unsupported", true) || message.contains("not supported", true) -> "This head unit may not support wireless CarPlay. Try a USB connection." - message.contains("denied", true) || message.contains("permission", true) -> "Allow the connection permission to continue" - message.contains("Failed", true) || message.contains("error", true) -> "Connection interrupted. Retrying…" - message.contains("Waiting for iPhone", true) || message.contains("Discovering iPhone", true) -> "Connect your iPhone with a USB cable" - message.contains("paired", true) -> "Looking for your paired iPhone…" - message.contains("Bluetooth", true) -> "Connecting to your iPhone…" - message.contains("reconnect", true) || message.contains("ended", true) -> "Reconnecting to your iPhone…" - message.contains("active", true) || message.contains("running", true) -> "Opening CarPlay…" - else -> "Getting CarPlay ready…" + message.contains("Turn on Wi-Fi", true) -> getString(R.string.turn_on_wi_fi_in_the_head_unit_s_settings_to_connect) + message.contains("Allow precise Location", true) -> getString(R.string.allow_precise_location_for_diplay_in_the_head_unit_s_app_p) + message.contains("Allow Nearby devices", true) -> getString(R.string.allow_nearby_devices_for_diplay_in_the_head_unit_s_app_per) + message.contains("createGroup failed", true) -> getString(R.string.the_head_unit_couldn_t_start_carplay_wi_fi_check_wi_fi_and) + message.contains("needs a reset", true) -> getString(R.string.a_previous_wi_fi_direct_connection_is_still_running_reset) + message.contains("socket", true) || message.contains("RFCOMM", true) -> getString(R.string.your_iphone_isn_t_available_unlock_it_and_check_bluetooth) + message.contains("unsupported", true) || message.contains("not supported", true) -> getString(R.string.this_head_unit_may_not_support_wireless_carplay_try_a_usb) + message.contains("denied", true) || message.contains("permission", true) -> getString(R.string.allow_the_connection_permission_to_continue) + message.contains("Failed", true) || message.contains("error", true) -> getString(R.string.connection_interrupted_retrying) + message.contains("Waiting for iPhone", true) || message.contains("Discovering iPhone", true) -> getString(R.string.connect_your_iphone_with_a_usb_cable) + message.contains("paired", true) -> getString(R.string.looking_for_your_paired_iphone) + message.contains("Bluetooth", true) -> getString(R.string.connecting_to_your_iphone) + message.contains("reconnect", true) || message.contains("ended", true) -> getString(R.string.reconnecting_to_your_iphone) + message.contains("active", true) || message.contains("running", true) -> getString(R.string.opening_carplay) + else -> getString(R.string.getting_carplay_ready) } private fun appendLog(message: String) { @@ -3575,30 +3571,30 @@ class CarPlayHostActivity : ComponentActivity() { private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() private fun CarPlayStatus.describe(): String = when (this) { - CarPlayStatus.DiscoveringMfi -> "Preparing MFi authentication" - CarPlayStatus.WaitingForMfi -> "Waiting for MFi coprocessor" - CarPlayStatus.RequestingMfiPermission -> "Requesting MFi USB permission" - CarPlayStatus.MfiReady -> "MFi authentication ready" - CarPlayStatus.StartingHotspot -> "Starting wireless hotspot" + CarPlayStatus.DiscoveringMfi -> getString(R.string.preparing_mfi_authentication) + CarPlayStatus.WaitingForMfi -> getString(R.string.waiting_for_mfi_coprocessor) + CarPlayStatus.RequestingMfiPermission -> getString(R.string.requesting_mfi_usb_permission) + CarPlayStatus.MfiReady -> getString(R.string.mfi_authentication_ready) + CarPlayStatus.StartingHotspot -> getString(R.string.starting_wireless_hotspot) is CarPlayStatus.HotspotReady -> "Hotspot ready: $backend, $ssid, $band, " + "channel ${if (channel == 0) "auto" else channel}" - CarPlayStatus.WaitingForPairedIphone -> "Waiting for paired iPhone" - CarPlayStatus.ConnectingBluetooth -> "Connecting Bluetooth" - CarPlayStatus.RunningWireless -> "Wireless CarPlay control running" - CarPlayStatus.WirelessActive -> "Wireless CarPlay active" - CarPlayStatus.DiscoveringIphone -> "Discovering iPhone" - CarPlayStatus.WaitingForIphone -> "Waiting for iPhone over USB" - CarPlayStatus.RequestingIphonePermission -> "Requesting iPhone USB permission" + CarPlayStatus.WaitingForPairedIphone -> getString(R.string.waiting_for_paired_iphone) + CarPlayStatus.ConnectingBluetooth -> getString(R.string.connecting_bluetooth) + CarPlayStatus.RunningWireless -> getString(R.string.wireless_carplay_control_running) + CarPlayStatus.WirelessActive -> getString(R.string.wireless_carplay_active) + CarPlayStatus.DiscoveringIphone -> getString(R.string.discovering_iphone) + CarPlayStatus.WaitingForIphone -> getString(R.string.waiting_for_iphone_over_usb) + CarPlayStatus.RequestingIphonePermission -> getString(R.string.requesting_iphone_usb_permission) CarPlayStatus.WaitingForReenumeration -> "Waiting for iPhone re-enumeration" - CarPlayStatus.SelectingConfiguration -> "Selecting CarPlay configuration" - CarPlayStatus.OpeningDataPaths -> "Opening USB data paths" - CarPlayStatus.Pairing -> "Pairing with iPhone" - CarPlayStatus.ConnectingControl -> "Connecting iAP2 control" + CarPlayStatus.SelectingConfiguration -> getString(R.string.selecting_carplay_configuration) + CarPlayStatus.OpeningDataPaths -> getString(R.string.opening_usb_data_paths) + CarPlayStatus.Pairing -> getString(R.string.pairing_with_iphone) + CarPlayStatus.ConnectingControl -> getString(R.string.connecting_iap2_control) CarPlayStatus.AttachingNetwork -> - if (wirelessEnabled) "Starting AirPlay service" else "Attaching NCM/AirPlay network" - CarPlayStatus.RunningControl -> "CarPlay control running" - CarPlayStatus.ControlEnded -> "CarPlay control window ended" + if (wirelessEnabled) getString(R.string.starting_airplay_service) else "Attaching NCM/AirPlay network" + CarPlayStatus.RunningControl -> getString(R.string.carplay_control_running) + CarPlayStatus.ControlEnded -> getString(R.string.carplay_control_window_ended) is CarPlayStatus.Failed -> "Failed: ${message}" } diff --git a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt index 5c7363e..3f412d4 100644 --- a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt @@ -65,7 +65,7 @@ class DiPlayActivity : ComponentActivity() { override fun run() { refreshStatus(); handler.postDelayed(this, 1000) } } private val bluetoothPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> - if (granted) choosePhone() else permissionHelp("Nearby devices", "Allow Nearby devices so DiPlay can connect to your paired iPhone.") + if (granted) choosePhone() else permissionHelp(getString(R.string.nearby_devices), getString(R.string.allow_nearby_devices_so_diplay_can_connect_to_your_paired)) } private val export = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> if (uri != null) exportDiagnostics(uri) @@ -127,9 +127,9 @@ class DiPlayActivity : ComponentActivity() { val content = column().apply { setPadding(dp(32), dp(24), dp(32), dp(32)) } scroll.addView(content) val header = row().apply { gravity = Gravity.CENTER_VERTICAL } - header.addView(ImageView(this).apply { setImageResource(R.drawable.ic_carplay); contentDescription = "CarPlay" }, LinearLayout.LayoutParams(dp(36), dp(36))) - header.addView(label("DiPlay", 26, TEXT, true).apply { setPadding(dp(12), 0, 0, 0) }, LinearLayout.LayoutParams(0, dp(56), 1f)) - header.addView(button(if (page == "home") "Car home" else "Back", false) { + header.addView(ImageView(this).apply { setImageResource(R.drawable.ic_carplay); contentDescription = getString(R.string.carplay) }, LinearLayout.LayoutParams(dp(36), dp(36))) + header.addView(label(getString(R.string.diplay), 26, TEXT, true).apply { setPadding(dp(12), 0, 0, 0) }, LinearLayout.LayoutParams(0, dp(56), 1f)) + header.addView(button(if (page == "home") getString(R.string.car_home) else getString(R.string.back), false) { if (page == "home") startActivity(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)) else { page = "home"; render() } }, LinearLayout.LayoutParams(dp(130), dp(56))) @@ -149,14 +149,14 @@ class DiPlayActivity : ComponentActivity() { val wide = resources.configuration.screenWidthDp >= 850 val body = column() val left = column() - left.addView(label("YOUR PHONE. YOUR DRIVE.", 12, ACCENT, true).apply { letterSpacing = .16f }) - left.addView(label("A familiar drive.", if (wide) 42 else 36, TEXT, true).apply { setPadding(0, dp(12), 0, dp(10)) }) - left.addView(label("Your maps, music and conversations.\nCarPlay, right here on your car display.", 19, MUTED)) + left.addView(label(getString(R.string.your_phone_your_drive), 12, ACCENT, true).apply { letterSpacing = .16f }) + left.addView(label(getString(R.string.a_familiar_drive), if (wide) 42 else 36, TEXT, true).apply { setPadding(0, dp(12), 0, dp(10)) }) + left.addView(label(getString(R.string.your_maps_music_and_conversations_carplay_right_here_on_yo), 19, MUTED)) val card = card() - card.addView(label("WIRELESS CARPLAY", 12, ACCENT, true).apply { letterSpacing = .12f }) - status = label("Ready when you are", 24, TEXT, true).apply { setPadding(0, dp(10), 0, dp(16)) } + card.addView(label(getString(R.string.wireless_carplay), 12, ACCENT, true).apply { letterSpacing = .12f }) + status = label(getString(R.string.ready_when_you_are), 24, TEXT, true).apply { setPadding(0, dp(10), 0, dp(16)) } card.addView(status) - connectButton = button("Connect phone", true) { + connectButton = button(getString(R.string.connect_phone), true) { if (CarPlayBackgroundSession.hasSession()) openProjection() else connect(true) } @@ -168,11 +168,11 @@ class DiPlayActivity : ComponentActivity() { } card.addView(label(connectionHint, 15, MUTED).apply { setPadding(0, dp(14), 0, 0) }) if (carHotspotOff()) { - card.addView(label("The car hotspot “${AirPlayPersistence.loadManualHotspotSsid(this)}” is off. Turn it on in the car settings before connecting.", 15, WARNING).apply { setPadding(0, dp(14), 0, 0) }) - card.addView(button("Open car hotspot settings", false) { openCarWifiSettings() }, matchButton(10, 56)) + card.addView(label(getString(R.string.msg_car_hotspot_off, AirPlayPersistence.loadManualHotspotSsid(this)), 15, WARNING).apply { setPadding(0, dp(14), 0, 0) }) + card.addView(button(getString(R.string.open_car_hotspot_settings), false) { openCarWifiSettings() }, matchButton(10, 56)) } - card.addView(button("Choose iPhone", false) { choosePhone() }, matchButton(16, 56)) - disconnectButton = button("Disconnect", false) { + card.addView(button(getString(R.string.choose_iphone), false) { choosePhone() }, matchButton(16, 56)) + disconnectButton = button(getString(R.string.disconnect), false) { disconnectButton?.isEnabled = false CarPlayBackgroundSession.stop { runOnUiThread { refreshStatus() } } }.apply { visibility = View.GONE } @@ -180,18 +180,18 @@ class DiPlayActivity : ComponentActivity() { val right = column().apply { gravity = Gravity.CENTER_HORIZONTAL } val logo = ImageView(this).apply { setImageResource(R.drawable.ic_carplay) - contentDescription = "CarPlay icon" + contentDescription = getString(R.string.carplay_icon) scaleType = ImageView.ScaleType.FIT_CENTER } val branding = column().apply { gravity = Gravity.CENTER addView(logo, LinearLayout.LayoutParams(dp(96), dp(96))) } - right.addView(button("Connect with USB", false) { connect(false) }, matchButton()) - right.addView(label("Plug your iPhone into a USB data port.\nAllow CarPlay when your iPhone asks.", 14, MUTED).apply { gravity = Gravity.CENTER; setPadding(dp(8), dp(10), dp(8), dp(24)) }) - right.addView(button("Settings", false) { page = "settings"; render() }, matchButton()) - right.addView(label("Make DiPlay feel right for your car.", 14, MUTED).apply { gravity = Gravity.CENTER; setPadding(0, dp(10), 0, dp(24)) }) - right.addView(label("PUBLIC PREVIEW · ${version()}", 12, MUTED).apply { letterSpacing = .08f }) + right.addView(button(getString(R.string.connect_with_usb), false) { connect(false) }, matchButton()) + right.addView(label(getString(R.string.plug_your_iphone_into_a_usb_data_port_allow_carplay_when_y), 14, MUTED).apply { gravity = Gravity.CENTER; setPadding(dp(8), dp(10), dp(8), dp(24)) }) + right.addView(button(getString(R.string.settings), false) { page = "settings"; render() }, matchButton()) + right.addView(label(getString(R.string.make_diplay_feel_right_for_your_car), 14, MUTED).apply { gravity = Gravity.CENTER; setPadding(0, dp(10), 0, dp(24)) }) + right.addView(label("${getString(R.string.home_public_preview)}${version()}", 12, MUTED).apply { letterSpacing = .08f }) if (wide) { // Both rows share column widths. The USB button starts at the wireless // card's top edge, independently of hero wrapping or font scaling. @@ -218,50 +218,50 @@ class DiPlayActivity : ComponentActivity() { } private fun settings(content: LinearLayout) { - content.addView(label("Your drive, your way.", 34, TEXT, true)) - content.addView(label("Apply reconnects CarPlay for size, resolution, music buffer and frame rate. Other changes apply to your next connection.", 17, MUTED).apply { setPadding(0, dp(8), 0, dp(24)) }) + content.addView(label(getString(R.string.your_drive_your_way), 34, TEXT, true)) + content.addView(label(getString(R.string.apply_reconnects_carplay_for_size_resolution_music_buffer), 17, MUTED).apply { setPadding(0, dp(8), 0, dp(24)) }) languageSettings(content) - section(content, "Connection setup", R.drawable.ic_dp_connection) { card -> - card.addView(label("Choose how to connect, follow the setup steps and save your car hotspot details.", 16, MUTED)) - card.addView(button("Open connection setup", false) { page = "connection"; render() }, matchButton(12, 60)) + section(content, getString(R.string.connection_setup), R.drawable.ic_dp_connection) { card -> + card.addView(label(getString(R.string.choose_how_to_connect_follow_the_setup_steps_and_save_your), 16, MUTED)) + card.addView(button(getString(R.string.open_connection_setup), false) { page = "connection"; render() }, matchButton(12, 60)) } - section(content, "Diagnostics", R.drawable.ic_dp_diagnostics) { card -> - exportButton = button(if (exportInProgress) "Saving report…" else "Save diagnostic report", false) { + section(content, getString(R.string.diagnostics), R.drawable.ic_dp_diagnostics) { card -> + exportButton = button(if (exportInProgress) getString(R.string.saving_report) else getString(R.string.save_diagnostic_report), false) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) exportDiagnostics() else chooseReportDestination() }.apply { isEnabled = !exportInProgress } card.addView(exportButton, matchButton(10, 60)) - card.addView(button("Choose save location", false) { chooseReportDestination() }, matchButton(10, 60)) - val destination = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) "Reports save to Downloads/DiPlay. " else "Choose where to save your report. " - card.addView(label(destination + "Nothing is sent automatically. Protocol payloads and credentials are excluded.", 14, MUTED).apply { setPadding(0, dp(12), 0, 0) }) + card.addView(button(getString(R.string.choose_save_location), false) { chooseReportDestination() }, matchButton(10, 60)) + val destination = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) getString(R.string.reports_save_to_downloads_diplay) else getString(R.string.choose_where_to_save_your_report) + card.addView(label(destination + getString(R.string.nothing_is_sent_automatically_protocol_payloads_and_creden), 14, MUTED).apply { setPadding(0, dp(12), 0, 0) }) } - section(content, "Automatic connection", R.drawable.ic_dp_automation) { card -> - toggle(card, "Connect when DiPlay opens", "Use your last connection type and selected iPhone.", DiPlayPreferences.autoConnect(this)) { DiPlayPreferences.saveAutoConnect(this, it) } - toggle(card, "Open after the car starts", "Availability depends on your head unit’s startup settings.", AirPlayPersistence.loadAutoStartOnBoot(this)) { AirPlayPersistence.saveAutoStartOnBoot(this, it) } - card.addView(button("Choose iPhone · ${DiPlayPreferences.phoneName(this)}", false) { choosePhone() }, matchButton(12, 60)) + section(content, getString(R.string.automatic_connection), R.drawable.ic_dp_automation) { card -> + toggle(card, getString(R.string.connect_when_diplay_opens), getString(R.string.use_your_last_connection_type_and_selected_iphone), DiPlayPreferences.autoConnect(this)) { DiPlayPreferences.saveAutoConnect(this, it) } + toggle(card, getString(R.string.open_after_the_car_starts), getString(R.string.availability_depends_on_your_head_unit_s_startup_settings), AirPlayPersistence.loadAutoStartOnBoot(this)) { AirPlayPersistence.saveAutoStartOnBoot(this, it) } + card.addView(button("${getString(R.string.choose_iphone_prefix)}${DiPlayPreferences.phoneName(this)}", false) { choosePhone() }, matchButton(12, 60)) } - section(content, "Display and performance", R.drawable.ic_dp_display) { card -> + section(content, getString(R.string.display_and_performance), R.drawable.ic_dp_display) { card -> carPlaySizeControl(card) - choice(card, "Resolution", listOf("Native", "80% · lighter load", "60% · lightest load"), listOf(10, 8, 6).indexOf(AirPlayPersistence.loadDisplayScaleTenths(this)).coerceAtLeast(0)) { AirPlayPersistence.saveDisplayScaleTenths(this, listOf(10, 8, 6)[it]) } + choice(card, getString(R.string.resolution), listOf(getString(R.string.resolution_native), getString(R.string.s_80_lighter_load), getString(R.string.s_60_lightest_load)), listOf(10, 8, 6).indexOf(AirPlayPersistence.loadDisplayScaleTenths(this)).coerceAtLeast(0)) { AirPlayPersistence.saveDisplayScaleTenths(this, listOf(10, 8, 6)[it]) } val bufferPresets = com.shilapi.xcertplay.media.MediaAudioBuffer.presets - choice(card, "Music buffer", listOf("300 ms · default", "500 ms", "1000 ms · most stable"), + choice(card, getString(R.string.music_buffer), listOf(getString(R.string.s_300_ms_default), getString(R.string.s_500_ms), getString(R.string.s_1000_ms_most_stable)), bufferPresets.indexOf(AirPlayPersistence.loadMediaBufferMillis(this)).coerceAtLeast(0)) { AirPlayPersistence.saveMediaBufferMillis(this, bufferPresets[it]) } - choice(card, "Frame rate", listOf("30 fps · lighter load", "60 fps · smoother motion"), if (AirPlayPersistence.loadFps(this) == 60) 1 else 0) { AirPlayPersistence.saveFps(this, if (it == 1) 60 else 30) } - toggle(card, "Efficient video", "Use HEVC. Leave off for the widest head-unit compatibility.", AirPlayPersistence.loadHevcEnabled(this)) { AirPlayPersistence.saveHevcEnabled(this, it) } - toggle(card, "Right-hand drive", "Place CarPlay’s controls closer to the driver.", AirPlayPersistence.loadRightHandDrive(this)) { AirPlayPersistence.saveRightHandDrive(this, it) } - toggle(card, "Full screen", "Hide the car’s system bars while CarPlay is open.", AirPlayPersistence.loadHideTopBar(this) && AirPlayPersistence.loadHideBottomBar(this)) { + choice(card, getString(R.string.frame_rate), listOf(getString(R.string.s_30_fps_lighter_load), getString(R.string.s_60_fps_smoother_motion)), if (AirPlayPersistence.loadFps(this) == 60) 1 else 0) { AirPlayPersistence.saveFps(this, if (it == 1) 60 else 30) } + toggle(card, getString(R.string.efficient_video), getString(R.string.use_hevc_leave_off_for_the_widest_head_unit_compatibility), AirPlayPersistence.loadHevcEnabled(this)) { AirPlayPersistence.saveHevcEnabled(this, it) } + toggle(card, getString(R.string.right_hand_drive), getString(R.string.place_carplay_s_controls_closer_to_the_driver), AirPlayPersistence.loadRightHandDrive(this)) { AirPlayPersistence.saveRightHandDrive(this, it) } + toggle(card, getString(R.string.full_screen), getString(R.string.hide_the_car_s_system_bars_while_carplay_is_open), AirPlayPersistence.loadHideTopBar(this) && AirPlayPersistence.loadHideBottomBar(this)) { AirPlayPersistence.saveHideTopBar(this, it); AirPlayPersistence.saveHideBottomBar(this, it) } } - section(content, "Audio routing") { card -> - val channelTitle = label("Navigation stream type", 18, TEXT, true) - val channelHint = label("Tap a number to test. 14 = driver speaker on BYD.", 14, MUTED) + section(content, getString(R.string.audio_routing)) { card -> + val channelTitle = label(getString(R.string.navigation_stream_type), 18, TEXT, true) + val channelHint = label(getString(R.string.tap_a_number_to_test_14_driver_speaker_on_byd), 14, MUTED) val grid = channelSelector() - val saveButton = button("Save", true) { + val saveButton = button(getString(R.string.save), true) { AirPlayPersistence.saveNavigationStreamType(this, navigationStreamType) - toast("Saved: $navigationStreamType") + toast(getString(R.string.audio_saved_value, navigationStreamType)) } fun applyChannelEnabled(enabled: Boolean) { val alpha = if (enabled) 1f else 0.4f @@ -278,8 +278,8 @@ class DiPlayActivity : ComponentActivity() { } val aaosSupported = resources.getBoolean(R.bool.config_advanced_audio_channel_mapping) if (aaosSupported) { - toggle(card, "Advanced audio channel mapping", - "Use usage / content-type routing instead of stream type", + toggle(card, getString(R.string.advanced_audio_channel_mapping), + getString(R.string.use_usage_content_type_routing_instead_of_stream_type), AirPlayPersistence.loadAdvancedAudioChannelMapping(this)) { AirPlayPersistence.saveAdvancedAudioChannelMapping(this, it) applyChannelEnabled(!it) @@ -291,59 +291,59 @@ class DiPlayActivity : ComponentActivity() { card.addView(saveButton, matchButton(12, 56)) if (aaosSupported) applyChannelEnabled(!AirPlayPersistence.loadAdvancedAudioChannelMapping(this)) } - if (com.shilapi.xcertplay.hud.BydOutputSettings.available(this)) section(content, "BYD navigation", R.drawable.ic_dp_navigation) { card -> - toggle(card, "Navigation on HUD and instrument cluster", - "Show phone navigation arrows, distance and street names on supported BYD displays. Vehicle compatibility varies.", + if (com.shilapi.xcertplay.hud.BydOutputSettings.available(this)) section(content, getString(R.string.byd_navigation), R.drawable.ic_dp_navigation) { card -> + toggle(card, getString(R.string.navigation_on_hud_and_instrument_cluster), + getString(R.string.show_phone_navigation_arrows_distance_and_street_names_on), com.shilapi.xcertplay.hud.BydOutputSettings.enabled(this)) { com.shilapi.xcertplay.hud.BydOutputSettings.setEnabled(this, it) } if (ClusterMapPresentation.findDisplay(this) != null) { - toggle(card, "CarPlay map on instrument cluster · experimental", - "Shows the iPhone's cluster map on the instrument cluster. Choose Small or Full screen navi in the cluster's steering-wheel menu.", + toggle(card, getString(R.string.carplay_map_on_instrument_cluster_experimental), + getString(R.string.shows_the_iphone_s_cluster_map_on_the_instrument_cluster_c), AirPlayPersistence.loadClusterMapEnabled(this)) { AirPlayPersistence.saveClusterMapEnabled(this, it) reconnectForClusterMap() } if (DiLink51ClusterLayout.supported()) { val automatic = DiLink51ClusterLayout.automatic(this) - toggle(card, "Follow instrument theme and map card", - "Show the side map only when its card is open, and switch to the full map in Map theme. Theme and card changes keep CarPlay connected.", automatic) { + toggle(card, getString(R.string.follow_instrument_theme_and_map_card), + getString(R.string.show_the_side_map_only_when_its_card_is_open_and_switch_to), automatic) { DiLink51ClusterLayout.saveAutomatic(this, it) render() reconnectForClusterMap() } val allowed = DiLink51ClusterMonitor.hasAccess(this) - card.addView(label(if (allowed) "Usage Access: enabled" - else "Usage Access: setup needed for automatic mode", 14, if (allowed) MUTED else WARNING)) - card.addView(button("Automatic map setup · ADB", false) { showClusterAccessSetup() }, matchButton(10, 56)) + card.addView(label(if (allowed) getString(R.string.usage_access_enabled) + else getString(R.string.usage_access_setup_needed_for_automatic_mode), 14, if (allowed) MUTED else WARNING)) + card.addView(button(getString(R.string.automatic_map_setup_adb), false) { showClusterAccessSetup() }, matchButton(10, 56)) if (!automatic) { val themes = DiLink51ClusterLayout.Theme.entries - choice(card, "Instrument theme", themes.map { it.label }, themes.indexOf(DiLink51ClusterLayout.theme(this))) { + choice(card, getString(R.string.instrument_theme), themes.map { it.label }, themes.indexOf(DiLink51ClusterLayout.theme(this))) { DiLink51ClusterLayout.saveTheme(this, themes[it]) reconnectForClusterMap() } - card.addView(label("Manual mode: match the cluster theme here. The map cannot follow card visibility without Usage Access.", 14, MUTED)) + card.addView(label(getString(R.string.manual_mode_match_the_cluster_theme_here_the_map_cannot_fo), 14, MUTED)) } val contrasts = DiLink51ClusterLayout.Contrast.entries - choice(card, "Instrument contrast", contrasts.map { it.label }, contrasts.indexOf(DiLink51ClusterLayout.contrast(this))) { + choice(card, getString(R.string.instrument_contrast), contrasts.map { it.label }, contrasts.indexOf(DiLink51ClusterLayout.contrast(this))) { DiLink51ClusterLayout.saveContrast(this, contrasts[it]) reconnectForClusterMap() } } else { val sizes = CarPlayClusterDisplay.scalePresets - choice(card, "Cluster map size", listOf("Standard · sharpest", "Larger · default", "Largest"), + choice(card, getString(R.string.cluster_map_size), listOf("Standard · sharpest", "Larger · default", "Largest"), sizes.indexOf(AirPlayPersistence.loadClusterMapScalePercent(this)).coerceAtLeast(0)) { AirPlayPersistence.saveClusterMapScalePercent(this, sizes[it]) } val across = CarPlayClusterDisplay.horizontalSteps.toList() - choice(card, "Car marker · horizontal", across.map { markerStepLabel(it, "Left", "Right") }, + choice(card, getString(R.string.car_marker_horizontal), across.map { markerStepLabel(it, "Left", "Right") }, across.indexOf(AirPlayPersistence.loadClusterMarkerHorizontalStep(this)).coerceAtLeast(0)) { AirPlayPersistence.saveClusterMarkerHorizontalStep(this, across[it]) } val upDown = CarPlayClusterDisplay.verticalSteps.toList() - choice(card, "Car marker · vertical", upDown.map { markerStepLabel(it, "Up", "Down") }, + choice(card, getString(R.string.car_marker_vertical), upDown.map { markerStepLabel(it, "Up", "Down") }, upDown.indexOf(AirPlayPersistence.loadClusterMarkerVerticalStep(this)).coerceAtLeast(0)) { AirPlayPersistence.saveClusterMarkerVerticalStep(this, upDown[it]) } - card.addView(button("Reset car marker to centre", false) { + card.addView(button(getString(R.string.reset_car_marker_to_centre), false) { AirPlayPersistence.saveClusterMarkerHorizontalStep(this, 0) AirPlayPersistence.saveClusterMarkerVerticalStep(this, 0) render() @@ -352,25 +352,25 @@ class DiPlayActivity : ComponentActivity() { } } } - section(content, "Permissions and connection help", R.drawable.ic_dp_permissions) { card -> - card.addView(label("Nearby devices connects your iPhone. Microphone enables Siri and calls. Older Android versions also require Location for wireless setup. USB mode may ask for a local VPN connection.", 16, MUTED)) - card.addView(button("App permissions", false) { openSystem(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName"))) }, matchButton(16, 60)) - card.addView(button("Bluetooth settings", false) { openSystem(Intent(Settings.ACTION_BLUETOOTH_SETTINGS)) }, matchButton(10, 60)) - card.addView(button("Wireless connection help", false) { wirelessHelp() }, matchButton(10, 60)) + section(content, getString(R.string.permissions_and_connection_help), R.drawable.ic_dp_permissions) { card -> + card.addView(label(getString(R.string.nearby_devices_connects_your_iphone_microphone_enables_sir), 16, MUTED)) + card.addView(button(getString(R.string.app_permissions), false) { openSystem(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName"))) }, matchButton(16, 60)) + card.addView(button(getString(R.string.bluetooth_settings), false) { openSystem(Intent(Settings.ACTION_BLUETOOTH_SETTINGS)) }, matchButton(10, 60)) + card.addView(button(getString(R.string.wireless_connection_help), false) { wirelessHelp() }, matchButton(10, 60)) } - section(content, "About", R.drawable.ic_dp_about) { card -> - card.addView(button("About DiPlay", false) { page = "about"; render() }, matchButton(0, 60)) + section(content, getString(R.string.about), R.drawable.ic_dp_about) { card -> + card.addView(button(getString(R.string.about_diplay), false) { page = "about"; render() }, matchButton(0, 60)) } } private fun about(content: LinearLayout) { - content.addView(label("DiPlay", 40, TEXT, true)) - content.addView(label("CarPlay, at home in your car.", 20, MUTED).apply { setPadding(0, dp(8), 0, dp(24)) }) - section(content, "Public preview · ${version()}") { card -> - card.addView(label("An independent CarPlay receiver for Android head units. Wired and wireless connections run on the head unit, with local authentication. A standard iPhone can connect without a jailbreak, Mac, dongle or sign-in.\n\nThis preview uses an experimental accessory identity. Compatibility with every iPhone and head unit is still being tested. It is not an Apple-certified product.", 17, TEXT)) + content.addView(label(getString(R.string.diplay), 40, TEXT, true)) + content.addView(label(getString(R.string.carplay_at_home_in_your_car), 20, MUTED).apply { setPadding(0, dp(8), 0, dp(24)) }) + section(content, "${getString(R.string.about_public_preview_prefix)}${version()}") { card -> + card.addView(label(getString(R.string.an_independent_carplay_receiver_for_android_head_units_wir), 17, TEXT)) } - section(content, "Made possible by open source") { card -> - card.addView(label("Receiver based on xcertplay, licensed under GPL-3.0. DiPlay’s interface follows DiAuto’s design, licensed under AGPL-3.0.\n\nIncludes AndroidX, Bouncy Castle, JmDNS and SLF4J. Source and license notices accompany this release.\n\nCarPlay and the CarPlay icon belong to Apple Inc. DiPlay is an independent project.", 16, MUTED)) + section(content, getString(R.string.made_possible_by_open_source)) { card -> + card.addView(label(getString(R.string.receiver_based_on_xcertplay_licensed_under_gpl_3_0_diplay), 16, MUTED)) } } @@ -380,11 +380,11 @@ class DiPlayActivity : ComponentActivity() { com.shilapi.xcertplay.network.CarHotspotStatus.isEnabled(this) == false private fun carHotspotOffDialog() { - AlertDialog.Builder(this).setTitle("Car hotspot is off") - .setMessage("DiPlay connects through the car hotspot “${AirPlayPersistence.loadManualHotspotSsid(this)}”. Turn it on in the car settings, then connect.") - .setPositiveButton("Open car settings") { _, _ -> openCarWifiSettings() } - .setNeutralButton("Connect") { _, _ -> connect(true) } - .setNegativeButton("Cancel", null).show() + AlertDialog.Builder(this).setTitle(getString(R.string.car_hotspot_is_off)) + .setMessage(getString(R.string.msg_car_hotspot_connect, AirPlayPersistence.loadManualHotspotSsid(this))) + .setPositiveButton(getString(R.string.open_car_settings)) { _, _ -> openCarWifiSettings() } + .setNeutralButton(getString(R.string.connect)) { _, _ -> connect(true) } + .setNegativeButton(getString(R.string.cancel), null).show() } // BYD maps the AOSP tether action to its own hotspot screen; other firmware falls back to Wi-Fi settings. @@ -413,23 +413,23 @@ class DiPlayActivity : ComponentActivity() { } private fun connectionSetup(content: LinearLayout) { - content.addView(label("Connection setup", 34, TEXT, true)) - content.addView(label("Set up once. Your details stay saved for the next drive. Changes apply to your next connection.", 17, MUTED).apply { setPadding(0, dp(8), 0, dp(24)) }) - section(content, "1 · Choose your connection") { card -> wirelessLinkControls(card) } - section(content, "2 · Pair your iPhone") { card -> - card.addView(label("Keep Bluetooth and Wi-Fi on your iPhone. Pair with the car’s Bluetooth, then select your iPhone here. Allow Nearby devices when asked.", 16, MUTED)) - card.addView(button("Choose iPhone · ${DiPlayPreferences.phoneName(this)}", false) { choosePhone() }, matchButton(12, 60)) - card.addView(button("Review app permissions", false) { + content.addView(label(getString(R.string.connection_setup), 34, TEXT, true)) + content.addView(label(getString(R.string.set_up_once_your_details_stay_saved_for_the_next_drive_cha), 17, MUTED).apply { setPadding(0, dp(8), 0, dp(24)) }) + section(content, getString(R.string.s_1_choose_your_connection)) { card -> wirelessLinkControls(card) } + section(content, getString(R.string.s_2_pair_your_iphone)) { card -> + card.addView(label(getString(R.string.keep_bluetooth_and_wi_fi_on_your_iphone_pair_with_the_car), 16, MUTED)) + card.addView(button("${getString(R.string.choose_iphone_prefix)}${DiPlayPreferences.phoneName(this)}", false) { choosePhone() }, matchButton(12, 60)) + card.addView(button(getString(R.string.review_app_permissions), false) { openSystem(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName"))) }, matchButton(12, 60)) } - section(content, "3 · Connect") { card -> - card.addView(label("Return from car settings to DiPlay, then connect. Accept the CarPlay prompt on your iPhone. A car internet plan is not required; mobile data availability depends on your phone’s network settings.", 16, MUTED)) - card.addView(button("Connect phone", true) { connect(true) }, matchButton(12, 60)) + section(content, getString(R.string.s_3_connect)) { card -> + card.addView(label(getString(R.string.return_from_car_settings_to_diplay_then_connect_accept_the), 16, MUTED)) + card.addView(button(getString(R.string.connect_phone), true) { connect(true) }, matchButton(12, 60)) } - section(content, "Prefer a cable?") { card -> - card.addView(label("Use a USB data cable and the car’s USB data port. Unlock your iPhone and allow CarPlay. No hotspot setup is needed.", 16, MUTED)) - card.addView(button("Connect with USB", false) { connect(false) }, matchButton(12, 60)) + section(content, getString(R.string.prefer_a_cable)) { card -> + card.addView(label(getString(R.string.use_a_usb_data_cable_and_the_car_s_usb_data_port_unlock_yo), 16, MUTED)) + card.addView(button(getString(R.string.connect_with_usb), false) { connect(false) }, matchButton(12, 60)) } } @@ -461,20 +461,20 @@ class DiPlayActivity : ComponentActivity() { option.addView(label(descriptions[index], 15, MUTED).apply { setPadding(0, dp(6), 0, dp(12)) }) } if (mode == WirelessHotspotMode.MANUAL) { - parent.addView(label("Hotspot setup", 22, TEXT, true)) - parent.addView(label("1. Open car hotspot settings, turn the hotspot on and select 5 GHz if available.\n2. Copy its name and password below exactly.\n3. Leave the car hotspot on when connecting. DiPlay sends the details over Bluetooth; your iPhone joins automatically.", 16, MUTED).apply { setPadding(0, dp(8), 0, dp(12)) }) - parent.addView(button("Open car hotspot settings", false) { openCarWifiSettings() }, matchButton(0, 60)) - parent.addView(button(if (pendingCarHotspotSetup) "Save hotspot details and use this mode" else "Edit saved hotspot · ${storedSsid()}", false) { + parent.addView(label(getString(R.string.hotspot_setup), 22, TEXT, true)) + parent.addView(label(getString(R.string.s_1_open_car_hotspot_settings_turn_the_hotspot_on_and_sele), 16, MUTED).apply { setPadding(0, dp(8), 0, dp(12)) }) + parent.addView(button(getString(R.string.open_car_hotspot_settings), false) { openCarWifiSettings() }, matchButton(0, 60)) + parent.addView(button(if (pendingCarHotspotSetup) getString(R.string.save_hotspot_details_and_use_this_mode) else "${getString(R.string.edit_saved_hotspot_prefix)}${storedSsid()}", false) { askHotspotCredentials { ssid, password -> saveHotspotCredentials(ssid, password) pendingCarHotspotSetup = false applyWirelessLink(WirelessHotspotMode.MANUAL) } }, matchButton(12, 60)) - parent.addView(label(if (pendingCarHotspotSetup) "Finish setup · Save your hotspot details to use this mode." else if (carHotspotOff()) "Hotspot is off · Turn it on in car settings." else "Details saved · Check that the car hotspot is on before connecting.", 15, if (carHotspotOff()) WARNING else MUTED).apply { setPadding(0, dp(12), 0, 0) }) + parent.addView(label(if (pendingCarHotspotSetup) getString(R.string.finish_setup_save_your_hotspot_details_to_use_this_mode) else if (carHotspotOff()) "Hotspot is off · Turn it on in car settings." else "Details saved · Check that the car hotspot is on before connecting.", 15, if (carHotspotOff()) WARNING else MUTED).apply { setPadding(0, dp(12), 0, 0) }) } else { - parent.addView(label("Turn the car’s Wi-Fi switch on. Allow Location / Nearby devices if requested and enable Location when Android asks. If playback stutters, try the built-in car hotspot at 5 GHz.", 16, MUTED)) - parent.addView(button("Open car Wi-Fi settings", false) { openCarClientWifiSettings() }, matchButton(12, 60)) + parent.addView(label(getString(R.string.turn_the_car_s_wi_fi_switch_on_allow_location_nearby_devic), 16, MUTED)) + parent.addView(button(getString(R.string.open_car_wi_fi_settings), false) { openCarClientWifiSettings() }, matchButton(12, 60)) } } @@ -494,10 +494,10 @@ class DiPlayActivity : ComponentActivity() { private fun askHotspotCredentials(done: (String, String) -> Unit) { val fields = column().apply { setPadding(dp(24), dp(12), dp(24), dp(12)) } - fields.addView(label("Copy these from the car’s hotspot settings. Use 5 GHz if available. Saving here does not change the car’s hotspot.", 16, MUTED)) - val ssid = EditText(this).apply { hint = "Hotspot name"; setText(storedSsid()); setSingleLine() } + fields.addView(label(getString(R.string.copy_these_from_the_car_s_hotspot_settings_use_5_ghz_if_av), 16, MUTED)) + val ssid = EditText(this).apply { hint = getString(R.string.hotspot_name); setText(storedSsid()); setSingleLine() } val password = EditText(this).apply { - hint = "Hotspot password"; setText(storedPassword()); setSingleLine() + hint = getString(R.string.hotspot_password); setText(storedPassword()); setSingleLine() inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD } ssid.imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_NEXT or android.view.inputmethod.EditorInfo.IME_FLAG_NO_EXTRACT_UI @@ -516,7 +516,7 @@ class DiPlayActivity : ComponentActivity() { } fields.addView(ssid); fields.addView(password) fields.addView(CheckBox(this).apply { - text = "Show password" + text = getString(R.string.show_password) setOnCheckedChangeListener { _, checked -> password.transformationMethod = if (checked) null else android.text.method.PasswordTransformationMethod.getInstance() password.setSelection(password.text.length) @@ -525,10 +525,10 @@ class DiPlayActivity : ComponentActivity() { val error = label("", 14, WARNING) error.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_POLITE fields.addView(error) - val dialog = AlertDialog.Builder(this).setTitle("Car hotspot details") + val dialog = AlertDialog.Builder(this).setTitle(getString(R.string.car_hotspot_details)) .setView(ScrollView(this).apply { addView(fields) }) - .setPositiveButton("Save details", null).setNegativeButton("Cancel") { _, _ -> hideKeyboard() } - .setNeutralButton("Hide keyboard", null).create() + .setPositiveButton(getString(R.string.save_details), null).setNegativeButton(getString(R.string.cancel)) { _, _ -> hideKeyboard() } + .setNeutralButton(getString(R.string.hide_keyboard), null).create() dialog.setOnShowListener { dialog.window?.setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) dialog.getButton(android.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener { hideKeyboard() } @@ -553,27 +553,27 @@ class DiPlayActivity : ComponentActivity() { private fun showClusterAccessSetup() { val command = "adb shell appops set $packageName GET_USAGE_STATS allow" val body = column().apply { setPadding(dp(24), dp(12), dp(24), dp(12)) } - body.addView(label("One-time setup on this car", 20, TEXT, true)) - body.addView(label("Usage Access lets DiPlay follow the instrument theme and map card. Only BYD cluster activity events are processed locally. This car does not provide a working permission settings page.", 15, MUTED)) - body.addView(label("1. Connect a computer with ADB installed to the car using your existing USB or wireless debugging connection. Approve debugging on the car if asked.\n\n2. Run this command in the computer’s terminal:", 16, TEXT)) + body.addView(label(getString(R.string.one_time_setup_on_this_car), 20, TEXT, true)) + body.addView(label(getString(R.string.usage_access_lets_diplay_follow_the_instrument_theme_and_m), 15, MUTED)) + body.addView(label(getString(R.string.s_1_connect_a_computer_with_adb_installed_to_the_car_using), 16, TEXT)) body.addView(label(command, 16, TEXT).apply { typeface = android.graphics.Typeface.MONOSPACE setTextIsSelectable(true) setPadding(0, dp(16), 0, dp(16)) }) - body.addView(button("Copy command", false) { + body.addView(button(getString(R.string.copy_command), false) { getSystemService(android.content.ClipboardManager::class.java).setPrimaryClip( android.content.ClipData.newPlainText("DiPlay Usage Access", command)) - toast("Copied to the car clipboard. Run the command on your computer.") + toast(getString(R.string.copied_to_the_car_clipboard_run_the_command_on_your_comput)) }, matchButton(0, 56)) - body.addView(label("If ADB lists several devices, use adb -s shell appops set $packageName GET_USAGE_STATS allow. Find the ID with adb devices.", 14, MUTED)) - body.addView(label("3. Tap Check and enable below. This enables the cluster map and automatic theme following. An active CarPlay session reconnects once. You can then disconnect the computer.", 16, TEXT)) + body.addView(label(getString(R.string.cluster_adb_multi_device, packageName), 14, MUTED)) + body.addView(label(getString(R.string.s_3_tap_check_and_enable_below_this_enables_the_cluster_ma), 16, TEXT)) val status = label(if (DiLink51ClusterMonitor.hasAccess(this)) "Permission enabled · ready to use" else "Permission not enabled yet", 16, TEXT) body.addView(status) - val dialog = AlertDialog.Builder(this).setTitle("Automatic cluster map setup") + val dialog = AlertDialog.Builder(this).setTitle(getString(R.string.automatic_cluster_map_setup)) .setView(ScrollView(this).apply { addView(body) }) - .setNegativeButton("Close", null) - .setPositiveButton("Check and enable", null).create() + .setNegativeButton(getString(R.string.close), null) + .setPositiveButton(getString(R.string.check_and_enable), null).create() dialog.setOnShowListener { dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { if (DiLink51ClusterMonitor.hasAccess(this)) { @@ -581,10 +581,10 @@ class DiPlayActivity : ComponentActivity() { DiLink51ClusterLayout.saveAutomatic(this, true) dialog.dismiss() render() - toast("Automatic map enabled. Open the cluster map card or select Map theme.") + toast(getString(R.string.automatic_map_enabled_open_the_cluster_map_card_or_select)) reconnectForClusterMap() } else { - status.text = "Still waiting for Usage Access. Check that the command ran successfully for this car, then try again." + status.text = getString(R.string.still_waiting_for_usage_access_check_that_the_command_ran) } } } @@ -592,7 +592,7 @@ class DiPlayActivity : ComponentActivity() { } // The cluster screen is described at connection time, so a running session reconnects over - // its current link. The position choices need no call: "Apply and reconnect" already does it. + // its current link. The position choices need no call: getString(R.string.apply_and_reconnect) already does it. private fun reconnectForClusterMap() { if (CarPlayBackgroundSession.hasSession()) connect(AirPlayPersistence.loadWirelessEnabled(this)) } @@ -600,7 +600,7 @@ class DiPlayActivity : ComponentActivity() { private fun applyWirelessLink(mode: WirelessHotspotMode) { AirPlayPersistence.saveWirelessHotspotMode(this, mode) render() - toast("Saved for your next connection") + toast(getString(R.string.saved_for_your_next_connection)) } private fun textInput(title: String, current: String, secret: Boolean, save: (String) -> Unit) { @@ -614,30 +614,30 @@ class DiPlayActivity : ComponentActivity() { } } AlertDialog.Builder(this).setTitle(title).setView(input) - .setPositiveButton("Save") { _, _ -> save(input.text.toString().let { if (secret) it else it.trim() }) } - .setNegativeButton("Cancel", null).show() + .setPositiveButton(getString(R.string.save)) { _, _ -> save(input.text.toString().let { if (secret) it else it.trim() }) } + .setNegativeButton(getString(R.string.cancel), null).show() } private fun carPlaySizeControl(parent: LinearLayout) { val sizes = com.shilapi.xcertplay.airplay.CarPlaySize.entries val current = com.shilapi.xcertplay.airplay.CarPlaySize.fromWidthMillimeters(AirPlayPersistence.loadWidthPhysicalMm(this)) - choice(parent, "CarPlay size", sizes.map { it.label }, sizes.indexOf(current)) { + choice(parent, getString(R.string.carplay_size), sizes.map { it.label }, sizes.indexOf(current)) { AirPlayPersistence.saveWidthPhysicalMm(this, sizes[it].widthMillimeters) } - parent.addView(label("Changes the size of CarPlay icons and text. Applying a size reconnects CarPlay.", 14, MUTED).apply { + parent.addView(label(getString(R.string.changes_the_size_of_carplay_icons_and_text_applying_a_size), 14, MUTED).apply { setPadding(0, 0, 0, dp(18)) }) } private fun connect(wireless: Boolean) { - if (wireless && pendingCarHotspotSetup) { toast("Save your hotspot details in Connection setup first"); page = "connection"; render(); return } + if (wireless && pendingCarHotspotSetup) { toast(getString(R.string.save_your_hotspot_details_in_connection_setup_first)); page = "connection"; render(); return } if (setupError != null) { toast(setupError!!); return } if (wireless && AirPlayPersistence.loadWirelessHotspotMode(this) == WirelessHotspotMode.MANUAL && hotspotError(storedSsid(), storedPassword()) != null) { pendingCarHotspotSetup = true page = "connection" render() - toast("Save the name and password from the car’s hotspot settings first") + toast(getString(R.string.save_the_name_and_password_from_the_car_s_hotspot_settings)) return } if (wireless && carHotspotOff()) { carHotspotOffDialog(); return } @@ -667,21 +667,21 @@ class DiPlayActivity : ComponentActivity() { } val adapter = getSystemService(BluetoothManager::class.java)?.adapter if (adapter == null || !adapter.isEnabled) { - AlertDialog.Builder(this).setTitle("Turn on Bluetooth") - .setMessage("Enable the car’s Bluetooth and pair your iPhone first.") - .setPositiveButton("Open Bluetooth") { _, _ -> openSystem(Intent(Settings.ACTION_BLUETOOTH_SETTINGS)) } - .setNegativeButton("Later", null).show(); return + AlertDialog.Builder(this).setTitle(getString(R.string.turn_on_bluetooth)) + .setMessage(getString(R.string.enable_the_car_s_bluetooth_and_pair_your_iphone_first)) + .setPositiveButton(getString(R.string.open_bluetooth)) { _, _ -> openSystem(Intent(Settings.ACTION_BLUETOOTH_SETTINGS)) } + .setNegativeButton(getString(R.string.later), null).show(); return } val devices = runCatching { adapter.bondedDevices.sortedBy { it.name ?: "" } }.getOrDefault(emptyList()) if (devices.isEmpty()) { - AlertDialog.Builder(this).setTitle("Pair your iPhone") - .setMessage("On your iPhone, open Settings → Bluetooth and pair with the car. Then return to DiPlay and choose Connect phone.") - .setPositiveButton("Open Bluetooth") { _, _ -> openSystem(Intent(Settings.ACTION_BLUETOOTH_SETTINGS)) } - .setNegativeButton("Got it", null).show(); return + AlertDialog.Builder(this).setTitle(getString(R.string.pair_your_iphone)) + .setMessage(getString(R.string.on_your_iphone_open_settings_bluetooth_and_pair_with_the_c)) + .setPositiveButton(getString(R.string.open_bluetooth)) { _, _ -> openSystem(Intent(Settings.ACTION_BLUETOOTH_SETTINGS)) } + .setNegativeButton(getString(R.string.got_it), null).show(); return } - AlertDialog.Builder(this).setTitle("Choose your iPhone") + AlertDialog.Builder(this).setTitle(getString(R.string.choose_your_iphone)) .setItems(devices.map { device -> - val name = device.name ?: "Paired device" + val name = device.name ?: getString(R.string.paired_device) if (devices.count { it.name == device.name } > 1) "$name · ${device.address.takeLast(5)}" else name }.toTypedArray()) { _, index -> val device = devices[index] @@ -689,15 +689,15 @@ class DiPlayActivity : ComponentActivity() { val start = pendingWireless; pendingWireless = false render() if (start) connect(true) - }.setNeutralButton("Pair another") { _, _ -> openSystem(Intent(Settings.ACTION_BLUETOOTH_SETTINGS)) } - .setNegativeButton("Cancel") { _, _ -> pendingWireless = false }.show() + }.setNeutralButton(getString(R.string.pair_another)) { _, _ -> openSystem(Intent(Settings.ACTION_BLUETOOTH_SETTINGS)) } + .setNegativeButton(getString(R.string.cancel)) { _, _ -> pendingWireless = false }.show() } private fun wirelessHelp() { - AlertDialog.Builder(this).setTitle("Wireless connection help") - .setMessage("Pair your iPhone with the car’s Bluetooth, keep Wi-Fi on, and allow CarPlay on the iPhone. Close any other phone-projection app.\n\nIf a previous projection app left its connection running, reset CarPlay Wi-Fi below and connect again. Your car’s normal internet Wi-Fi stays on.") - .setPositiveButton("Got it", null) - .setNeutralButton("Reset CarPlay Wi-Fi") { _, _ -> + AlertDialog.Builder(this).setTitle(getString(R.string.wireless_connection_help)) + .setMessage(getString(R.string.pair_your_iphone_with_the_car_s_bluetooth_keep_wi_fi_on_an)) + .setPositiveButton(getString(R.string.got_it), null) + .setNeutralButton(getString(R.string.reset_carplay_wi_fi)) { _, _ -> confirmWirelessReset() }.show() } @@ -709,16 +709,16 @@ class DiPlayActivity : ComponentActivity() { } private fun confirmWirelessReset() { - AlertDialog.Builder(this).setTitle("Reset CarPlay Wi-Fi?") - .setMessage("This ends the existing Wi-Fi Direct connection, including one left behind after reinstalling. Close other projection apps first. Your car’s internet Wi-Fi stays on.") - .setPositiveButton("Reset and connect") { _, _ -> + AlertDialog.Builder(this).setTitle(getString(R.string.reset_carplay_wi_fi_2)) + .setMessage(getString(R.string.this_ends_the_existing_wi_fi_direct_connection_including_o)) + .setPositiveButton(getString(R.string.reset_and_connect)) { _, _ -> CarPlayBackgroundSession.stop { runOnUiThread { resetWirelessGroup() } } - }.setNegativeButton("Cancel", null).show() + }.setNegativeButton(getString(R.string.cancel), null).show() } private fun resetWirelessGroup() { val manager = getSystemService(android.net.wifi.p2p.WifiP2pManager::class.java) - if (manager == null) { toast("This head unit does not support Wi-Fi Direct."); return } + if (manager == null) { toast(getString(R.string.this_head_unit_does_not_support_wi_fi_direct)); return } val channel = manager.initialize(this, mainLooper, null) try { manager.requestGroupInfo(channel) { group -> @@ -731,7 +731,7 @@ class DiPlayActivity : ComponentActivity() { when { remaining == null -> { channel.close(); if (!isFinishing && !isDestroyed) connect(true) } android.os.SystemClock.elapsedRealtime() >= deadline -> { - channel.close(); toast("Wi-Fi Direct is still busy. Close the other projection app and try again.") + channel.close(); toast(getString(R.string.wi_fi_direct_is_still_busy_close_the_other_projection_app)) } else -> handler.postDelayed({ waitUntilRemoved() }, 200) } @@ -739,25 +739,25 @@ class DiPlayActivity : ComponentActivity() { } waitUntilRemoved() } - override fun onFailure(reason: Int) { channel.close(); toast("Could not reset Wi-Fi Direct. Close the other projection app and try again.") } + override fun onFailure(reason: Int) { channel.close(); toast(getString(R.string.could_not_reset_wi_fi_direct_close_the_other_projection_ap)) } }) } } catch (_: SecurityException) { - channel.close(); permissionHelp("Wireless permissions", "Allow Nearby devices and, on older Android versions, Location before resetting CarPlay Wi-Fi.") + channel.close(); permissionHelp(getString(R.string.wireless_permissions), getString(R.string.allow_nearby_devices_and_on_older_android_versions_locatio)) } } private fun refreshStatus() { val running = CarPlayBackgroundSession.hasSession() status?.text = when { - setupError != null -> "Setup needs attention" - CarPlayBackgroundSession.active -> "CarPlay connected" - running -> "Connecting to your iPhone…" - DiPlayPreferences.phoneAddress(this) != null -> "Ready for ${DiPlayPreferences.phoneName(this)}" - else -> "Ready when you are" + setupError != null -> getString(R.string.setup_needs_attention) + CarPlayBackgroundSession.active -> getString(R.string.carplay_connected) + running -> getString(R.string.connecting_to_your_iphone) + DiPlayPreferences.phoneAddress(this) != null -> "${getString(R.string.status_ready_for_prefix)}${DiPlayPreferences.phoneName(this)}" + else -> getString(R.string.ready_when_you_are) } if (lastRunning != running) { - connectButton?.text = if (running) "Open CarPlay" else "Connect phone" + connectButton?.text = if (running) getString(R.string.open_carplay) else getString(R.string.connect_phone) disconnectButton?.visibility = if (running) View.VISIBLE else View.GONE disconnectButton?.isEnabled = true lastRunning = running @@ -771,15 +771,15 @@ class DiPlayActivity : ComponentActivity() { // the result callback and the background writer's exception handler ever run. runCatching { export.launch(reportFileName()) }.onFailure { toast(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - "This head unit could not open a save location. Please try saving to Downloads again." - else "This head unit has no available file picker to save the report.") + getString(R.string.this_head_unit_could_not_open_a_save_location_please_try_s) + else getString(R.string.this_head_unit_has_no_available_file_picker_to_save_the_re)) } } private fun exportDiagnostics(uri: Uri? = null) { if (exportInProgress) return exportInProgress = true - exportButton?.apply { isEnabled = false; text = "Saving report…" } + exportButton?.apply { isEnabled = false; text = getString(R.string.saving_report) } val appContext = applicationContext val fileName = reportFileName() Thread({ @@ -816,36 +816,36 @@ class DiPlayActivity : ComponentActivity() { runOnUiThread { exportInProgress = false if (isFinishing || isDestroyed) return@runOnUiThread - exportButton?.apply { isEnabled = true; text = "Save diagnostic report" } + exportButton?.apply { isEnabled = true; text = getString(R.string.save_diagnostic_report) } if (result.isSuccess) { val savedUri = result.getOrThrow() - AlertDialog.Builder(this).setTitle("Diagnostic report saved") - .setMessage(if (uri == null) "Downloads/DiPlay/$fileName" else "Your report was saved to the selected location.") - .setPositiveButton("Done", null) - .setNeutralButton("Share") { _, _ -> + AlertDialog.Builder(this).setTitle(getString(R.string.diagnostic_report_saved)) + .setMessage(if (uri == null) "Downloads/DiPlay/$fileName" else getString(R.string.your_report_was_saved_to_the_selected_location)) + .setPositiveButton(getString(R.string.done), null) + .setNeutralButton(getString(R.string.share)) { _, _ -> runCatching { startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply { type = "text/plain"; putExtra(Intent.EXTRA_STREAM, savedUri) clipData = android.content.ClipData.newRawUri("Diagnostic report", savedUri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }, "Share diagnostic report")) - }.onFailure { toast("Report saved. Open it from your file manager to share it.") } + }.onFailure { toast(getString(R.string.report_saved_open_it_from_your_file_manager_to_share_it)) } }.show() } else { - AlertDialog.Builder(this).setTitle("Could not save the report") - .setMessage("Check that storage is available, or choose another save location.") - .setPositiveButton("Choose location") { _, _ -> chooseReportDestination() } - .setNegativeButton("Close", null).show() + AlertDialog.Builder(this).setTitle(getString(R.string.could_not_save_the_report)) + .setMessage(getString(R.string.check_that_storage_is_available_or_choose_another_save_loc)) + .setPositiveButton(getString(R.string.choose_location)) { _, _ -> chooseReportDestination() } + .setNegativeButton(getString(R.string.close), null).show() } } }, "diplay-export").start() } private fun permissionHelp(title: String, body: String) { - AlertDialog.Builder(this).setTitle(title).setMessage(body).setPositiveButton("App settings") { _, _ -> + AlertDialog.Builder(this).setTitle(title).setMessage(body).setPositiveButton(getString(R.string.app_settings)) { _, _ -> openSystem(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName"))) - }.setNegativeButton("Later", null).show() + }.setNegativeButton(getString(R.string.later), null).show() } - private fun openSystem(intent: Intent) { runCatching { startActivity(intent) }.onFailure { toast("Open this setting from your car’s Settings app.") } } + private fun openSystem(intent: Intent) { runCatching { startActivity(intent) }.onFailure { toast(getString(R.string.open_this_setting_from_your_car_s_settings_app)) } } private fun toast(message: String) { Toast.makeText(this, message, Toast.LENGTH_LONG).show() } private fun playTestTone(streamType: Int) { @@ -865,7 +865,7 @@ class DiPlayActivity : ComponentActivity() { if (track.state != AudioTrack.STATE_INITIALIZED) { Log.w("DiPlay", "playTestTone streamType=$streamType state=${track.state}") track.release() - toast("Stream type $streamType is unavailable here (state=${track.state})") + toast(getString(R.string.audio_stream_unavailable, streamType, track.state)) return } track.write(pcm, 0, pcm.size) @@ -982,7 +982,7 @@ class DiPlayActivity : ComponentActivity() { var pendingSelection = selection AlertDialog.Builder(this).setTitle(title) .setSingleChoiceItems(options.toTypedArray(), selection) { _, index -> pendingSelection = index } - .setPositiveButton(if (CarPlayBackgroundSession.hasSession()) "Apply and reconnect" else "Save") { _, _ -> + .setPositiveButton(if (CarPlayBackgroundSession.hasSession()) getString(R.string.apply_and_reconnect) else getString(R.string.save)) { _, _ -> if (pendingSelection != selection) { selection = pendingSelection save(selection) @@ -991,7 +991,7 @@ class DiPlayActivity : ComponentActivity() { connect(AirPlayPersistence.loadWirelessEnabled(this)) } } - }.setNegativeButton("Cancel", null).show() + }.setNegativeButton(getString(R.string.cancel), null).show() } parent.addView(button, matchButton(0, 60)); parent.addView(space(12)) } diff --git a/common/src/main/java/com/shilapi/xcertplay/ImageCropActivity.kt b/common/src/main/java/com/shilapi/xcertplay/ImageCropActivity.kt index 1d8f850..3496b1e 100644 --- a/common/src/main/java/com/shilapi/xcertplay/ImageCropActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/ImageCropActivity.kt @@ -20,6 +20,7 @@ import android.widget.Button import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView +import com.shilapi.xcertplay.host.R import java.io.ByteArrayOutputStream import java.util.concurrent.Executors import kotlin.math.ceil @@ -51,7 +52,7 @@ class ImageCropActivity : Activity() { textSize = 16f gravity = Gravity.CENTER setPadding(dp(12), dp(8), dp(12), dp(8)) - text = "Loading image" + text = getString(R.string.loading_image) } val controls = LinearLayout(this).apply { @@ -61,7 +62,7 @@ class ImageCropActivity : Activity() { } controls.addView( Button(this).apply { - text = "Cancel" + text = getString(R.string.cancel) isAllCaps = false setOnClickListener { setResult(RESULT_CANCELED) @@ -72,7 +73,7 @@ class ImageCropActivity : Activity() { ) controls.addView( Button(this).apply { - text = "Save 1:1" + text = getString(R.string.save_1_1) isAllCaps = false setOnClickListener { saveCrop() } }, @@ -121,10 +122,10 @@ class ImageCropActivity : Activity() { return@runOnUiThread } if (bitmap == null) { - statusView.text = "Could not decode image" + statusView.text = getString(R.string.could_not_decode_image) } else { cropView.setBitmap(bitmap) - statusView.text = "Drag to move, pinch to zoom" + statusView.text = getString(R.string.drag_to_move_pinch_to_zoom) } } } @@ -138,13 +139,13 @@ class ImageCropActivity : Activity() { private fun saveCrop() { val cropped = cropView.cropToSquare() ?: run { - statusView.text = "Image is not ready" + statusView.text = getString(R.string.image_is_not_ready) return } val encoded = ByteArrayOutputStream().use { output -> if (!cropped.compress(Bitmap.CompressFormat.PNG, 100, output)) { cropped.recycle() - statusView.text = "Could not encode image" + statusView.text = getString(R.string.could_not_encode_image) return } cropped.recycle() @@ -158,7 +159,7 @@ class ImageCropActivity : Activity() { ) finish() } catch (_: Exception) { - statusView.text = "Could not save image" + statusView.text = getString(R.string.could_not_save_image) } } diff --git a/common/src/main/res/values-zh-rCN/strings.xml b/common/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000..f504165 --- /dev/null +++ b/common/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,340 @@ + + 车机热点“%1$s”未开启。请在车机设置中开启后再连接。 + 公开预览 ·\u0020\u0020 + 选择 iPhone ·\u0020 + 已保存:%1$d + 公开预览 ·\u0020 + DiPlay 通过车机热点“%1$s”连接。请在车机设置中开启热点后连接。 + 编辑已保存的热点 ·\u0020 + 如果 ADB 列出多个设备,请使用 adb -s <car-device-ID> shell appops set %1$s GET_USAGE_STATS allow。用 adb devices 查询 ID。 + 已准备连接: + 流类型 %1$d 在此不可用(状态=%2$s) + 握手分辨率: + 安全区域:全屏, + 安全区域: + 以下设置在 Android 9(API 28)上不可用且已隐藏:\n• Wi-Fi P2P(5 GHz)— 将改用 LocalOnlyHotspot。\n• HEVC 软解码器 — 将改用硬件解码。 + 地址必须以 http:// 或 https:// 开头。令牌为可选项,将作为 Authorization bearer token 发送。 + 熟悉的驾乘体验。 + 之前的 Wi-Fi Direct 连接仍在运行。请重置后再连接。 + 关于 + 关于 DiPlay + 已启用 + 高级音频通道映射 + AirPlay 图标 + 允许 CarPlay 界面超出安全区域 + 重置 CarPlay Wi-Fi 前,请授予“附近的设备”权限;较旧的 Android 版本还需要位置权限。 + 请在车机应用权限中允许 DiPlay 访问附近的设备。 + 请允许 DiPlay 访问附近的设备,以连接已配对的 iPhone。 + 请在车机应用权限中允许 DiPlay 使用精确位置信息。 + 请授予连接权限以继续 + 面向 Android 车机的独立 CarPlay 接收器。有线和无线连接均在车机上运行,并使用本地认证。普通 iPhone 无需越狱、Mac、转接器或登录即可连接。\n\n此预览版使用实验性配件身份,仍在测试与不同 iPhone 和车机的兼容性。本产品未经 Apple 认证。 + Android 9 兼容性 + DiPlay + 应用权限 + 应用设置 + 应用并重新连接 + 应用尺寸、分辨率、音乐缓冲和帧率设置时会重新连接 CarPlay。其他改动将在下次连接时生效。 + 音频 + 音频路由 + 自动 + 开机后自动启动 + 自动仪表盘地图设置 + 自动连接 + 自动地图已启用。打开仪表盘地图卡片或选择“地图主题”。 + 自动地图设置 · ADB + 是否可用取决于车机的启动设置。 + 返回 + 返回 DiPlay + 频段 + 蓝牙设置 + 比亚迪导航 + 取消 + 车机主页 + 车机热点详情 + 车机热点未开启 + 车辆标记 · 水平 + 车辆标记 · 垂直 + CarPlay + 让 CarPlay 融入车机。 + CarPlay 已连接 + CarPlay 控制通道运行中 + CarPlay 控制会话已结束 + CarPlay 图标 + 仪表盘显示 CarPlay 地图 · 实验性 + CarPlay 服务已在运行 + CarPlay 会话已在运行 + CarPlay 会话已结束;正在重新连接 + CarPlay 设置 + CarPlay 尺寸 + 更改 CarPlay 图标和文字的大小。应用尺寸后会重新连接 CarPlay。 + 信道(0 = 自动) + 所选频段不支持此信道 + 信道必须为 0 或 1–196 + 检查并启用 + 请检查存储空间,或选择其他保存位置。 + 选择连接方式,按步骤操作并保存车机热点信息。 + 选择图片 + 选择 iPhone + 选择位置 + 选择保存位置 + 选择报告的保存位置。 + 选择 iPhone + 关闭 + 仪表盘地图尺寸 + 取消 + 连接 + 连接手机 + 打开 DiPlay 时连接 + 通过 USB 连接 + 请使用 USB 数据线连接 iPhone + 正在连接蓝牙 + 正在连接 iAP2 控制通道 + 正在连接 iPhone… + 连接 + 连接中断,正在重试… + 连接设置 + 已复制到车机剪贴板。在电脑上运行该命令。 + 复制命令 + 从车机热点设置中复制以下内容。如有 5 GHz 请选择。在此保存不会更改车机的热点。 + 无法读取图片 + 无法编码图片 + 无法重置 Wi-Fi Direct。请关闭其他投屏应用后重试。 + 无法保存图片 + 无法保存报告 + 无法启动 CarPlay。请返回 DiPlay 检查应用权限。 + 自定义 1:1 图标 + 调试日志 + 默认图标 + 默认占位图标 + 诊断报告已保存 + 诊断 + DiPlay + 放弃更改并退出设置 + 断开连接 + 正在查找 iPhone + 显示与性能 + 显示与视频 + 完成 + 拖动以移动,双指缩放 + 允许绘制到安全区域外 + 驾驶方向 + 高效视频 + 请先打开车机蓝牙并配对 iPhone。 + 错误 + 退出应用 + 完成设置 · 保存热点信息以使用此模式。 + 跟随仪表盘主题和地图卡片 + 帧率 + 全屏显示 + 全屏 + 正在准备 CarPlay… + 知道了 + 握手分辨率:正在等待显示屏 + HEVC H.265 视频传输 + HEVC 软件解码器 + 隐藏底部导航栏 + 隐藏键盘 + 打开 CarPlay 时隐藏车机系统栏。 + 隐藏导航栏 + 隐藏状态栏 + 隐藏顶部状态栏 + 热点名称 + 热点密码 + 热点密码包含 U+0000 + 热点设置 + 热点 SSID + 热点 SSID 包含 U+0000 + 必须填写热点 SSID + 热点 SSID 最多为 32 个 UTF-8 字节 + 热点状态 + I2C + I2C 设备 + I2C 设备路径包含 U+0000 + 必须填写 I2C 设备路径 + 身份与外观 + 图片尚未准备好 + 在 CarPlay 界面用三指向下滑动,可打开 DiPlay 设置。 + 仪表盘对比度 + 仪表盘主题 + 保持 iPhone 的蓝牙和 Wi-Fi 开启。先与车机蓝牙配对,然后在此选择你的 iPhone。出现提示时允许“附近的设备”。 + 请将 iPhone 放在附近并开启蓝牙和 Wi-Fi。\niPhone 出现提示时允许使用 CarPlay。 + 应用语言 + 应用 + 选择 DiPlay 的语言。“跟随系统”将使用设备语言。 + 语言 + 跟随系统 + 稍后 + 左舵 + Linux 设备路径,例如 /dev/i2c-1。 + 正在加载图片 + 本地离线 + LocalOnlyHotspot + 位置 + 较长边 + 正在查找已配对的 iPhone… + 开源项目鸣谢 + 按需调整 DiPlay 设置。 + 手动热点 + 手动模式:在此匹配仪表盘主题。没有使用访问权限时,地图无法跟随卡片显示状态。 + 制造商 + MFi 认证已就绪 + MFi 证书与签名目标 + 型号 + 音乐缓冲 + 原生 + 在 HUD 和仪表盘显示导航 + 导航音频流类型 + 附近的设备 + “附近的设备”权限用于连接 iPhone;麦克风权限用于 Siri 和通话。较旧的 Android 版本还需要位置权限来设置无线连接。USB 模式可能会请求建立本地 VPN 连接。 + 报告不会自动发送,不包含协议数据和凭据。 + OEM 标签 + 在 iPhone 上打开“设置 → 蓝牙”并与车机配对。然后返回 DiPlay,点击“连接手机”。 + 在此车机上一次性设置 + 开放 + 车机启动后打开 + 打开蓝牙 + 打开车机热点设置 + 打开车机设置 + 打开车机 Wi-Fi 设置 + 打开 CarPlay + 打开连接设置 + 请在车机的设置应用中打开此设置。 + 正在打开 CarPlay… + 正在打开 USB 数据通道 + 配对其他设备 + 配对 iPhone + 在车机蓝牙设置中配对 iPhone,保持 Wi-Fi 开启,并在 iPhone 上允许 CarPlay。请关闭其他手机投屏应用。\n\n如果之前的投屏应用残留了连接,请先重置下方的 CarPlay Wi-Fi,再重新连接。车机正常的互联网 Wi-Fi 不受影响。 + 已配对设备 + 正在与 iPhone 配对 + 安全类型为“开放”时,密码必须留空 + 权限与连接帮助 + 物理长度 + 物理尺寸依据 + 将 CarPlay 控件移至更靠近驾驶员的一侧。 + 将 iPhone 连接到支持数据传输的 USB 接口。\n手机出现提示时允许使用 CarPlay。 + 想用数据线? + 正在准备 MFi 认证 + 就绪 + 一切就绪 + 接收器基于采用 GPL-3.0 许可的 xcertplay。DiPlay 界面沿用采用 AGPL-3.0 许可的 DiAuto 设计。\n\n项目包含 AndroidX、Bouncy Castle、JmDNS 和 SLF4J。本版本附有源代码和许可声明。\n\nCarPlay 和 CarPlay 图标归 Apple Inc. 所有。DiPlay 是独立项目。 + 正在重新连接 iPhone… + 远程 + 远程服务器地址包含 U+0000 + 必须填写远程服务器地址 + 远程服务器地址必须以 http:// 或 https:// 开头 + 远程令牌包含 U+0000 + 向 iPhone 报告 Android 位置 + 向 iPhone 报告位置 + 报告已保存。在文件管理器中打开即可分享。 + 报告将保存到 Downloads/DiPlay。 + 正在请求 iPhone USB 权限 + 正在请求 MFi USB 权限 + 重置 + 重置并连接 + 将车辆标记重置到居中 + 重置 CarPlay Wi-Fi + 重置 CarPlay Wi-Fi? + 分辨率 + 从车机设置返回 DiPlay,然后连接。在 iPhone 上接受 CarPlay 提示。无需车机流量套餐;移动数据可用性取决于手机的网络设置。 + 查看应用权限 + 右舵 + 运行中 + 0.3x + 1000 毫秒 · 更稳定 + 1.0x + 1 · 选择连接方式 + 1. 用装有 ADB 的电脑,通过现有的 USB 或无线调试连接连接到车机。如有调试授权提示请批准。\n\n2. 在电脑终端中运行此命令: + 1. 打开车机热点设置,开启热点,如有 5 GHz 请选择。\n2. 在下方准确复制热点名称和密码。\n3. 连接时保持车机热点开启。DiPlay 通过蓝牙发送热点信息,iPhone 会自动加入。 + 2.4 GHz + 2 · 配对你的 iPhone + 300 毫秒 · 默认 + 30 帧/秒 · 减轻负载 + 3 · 连接 + 3. 点击下方“检查并启用”。这将启用仪表盘地图和自动主题跟随。正在进行的 CarPlay 会话会重新连接一次,之后即可断开电脑。 + 500 毫秒 + 5 GHz + 60 帧/秒 · 更流畅 + 60% · 最低负载 + 80% · 减轻负载 + 安全区域 + 安全区域:正在等待活动窗口尺寸 + 保存 + 保存 1:1 图片 + 保存并重新连接 + 保存详情 + 保存诊断报告 + 保存热点信息并使用此模式 + 请先保存车机热点设置中的名称和密码 + 请先在“连接设置”中保存热点信息 + 已保存,将在下次连接时生效 + 正在保存报告… + 安全类型 + 正在选择 CarPlay 配置 + iPhone 请求时,将 Android 精确位置作为 CarPlay GPS 数据发送。 + 服务器地址 + 设置 + 只需设置一次,信息会保存供下次驾驶使用。更改将在下次连接时生效。 + 设置 + 设置需要处理 + 分享 + 在屏幕上显示调试日志 + 显示密码 + 在支持的比亚迪显示屏上显示手机导航箭头、距离和道路名称。不同车型的兼容性有所差异。 + 仅在地图卡片展开时显示侧方地图,并在“地图主题”中切换为完整地图。更改主题或卡片不会中断 CarPlay 连接。 + 在仪表盘上显示 iPhone 的地图。可在方向盘菜单中选择“小”或“全屏导航”。 + 设备启动后自动启动 CarPlay + 正在启动 + 正在启动 AirPlay 服务 + 正在启动无线热点 + 启动 + 仍在等待使用访问权限。请确认命令已在本车机上成功运行,然后重试。 + 点按数字测试。14 = 比亚迪主驾扬声器。 + 车机无法启动 CarPlay Wi-Fi。请检查 Wi-Fi 并关闭其他投屏应用,正在重试… + 这会结束现有的 Wi-Fi Direct 连接,包括重新安装后遗留的连接。请先关闭其他投屏应用。车机的互联网 Wi-Fi 不受影响。 + 此车机在当前分辨率下无法使用更小的尺寸,将使用默认尺寸。 + 车机无法打开保存位置,请重试保存到 Downloads。 + 此车机不支持 Wi-Fi Direct。 + 此车机没有可用于保存报告的文件选择器。 + 此车机可能不支持无线 CarPlay,请尝试 USB 连接。 + 令牌(可选) + 传输错误;正在重新连接 + 打开蓝牙 + 请在车机设置中打开 Wi-Fi 后再连接。 + 打开车机的 Wi-Fi 开关。如提示请允许位置/附近的设备权限,并在 Android 询问时启用位置。如果播放卡顿,请尝试使用 5 GHz 的内置车机热点。 + 使用访问权限:已启用 + 使用访问权限让 DiPlay 跟随仪表盘主题和地图卡片。仅在本地处理 BYD 仪表盘活动事件。本车机没有可用的权限设置页面。 + 使用访问权限:自动模式需要设置 + USB/CH341 + 使用 USB 数据线连接车机的 USB 数据接口。解锁 iPhone 并允许 CarPlay。无需设置热点。 + 请使用 USB 数据线并解锁 iPhone。\niPhone 出现提示时允许“信任”和使用 CarPlay。 + 使用 HEVC。关闭后可兼容更多车机。 + 使用 HEVC 软件解码器 + 使用 usage / content-type 路由代替流类型 + 使用上次的连接方式和已选 iPhone。 + VPN 授权被拒绝 + 正在等待 USB 连接的 iPhone + 正在等待 MFi 协处理器 + 正在等待已配对的 iPhone + Wi-Fi Direct 仍被占用。请关闭其他投屏应用后重试。 + Wi-Fi P2P(5 GHz) + Wi-Fi 会话 + 较宽边 + 窗口 + 无线 CarPlay + 无线 CarPlay + 无线 CarPlay 已启用 + 无线 CarPlay 控制通道运行中 + 无线 CarPlay 连接 + 无线连接帮助 + 无线热点:关闭 + 无线连接权限 + WPA2 + WPA2/WPA3 密码长度必须为 8–63 个字符 + WPA3 + WPA3 过渡模式 + 按你的习惯设置。 + 暂时找不到 iPhone。请解锁手机并检查蓝牙。 + 地图、音乐与通话,\n在车机屏幕上畅享 CarPlay。 + 你的手机,畅享旅程。 + 报告已保存到所选位置。 + diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 9412c5a..7a9f400 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,4 +1,19 @@ + The car hotspot “%1$s” is off. Turn it on in the car settings before connecting. + PUBLIC PREVIEW ·\u0020\u0020 + Choose iPhone ·\u0020 + Saved: %1$d + Public preview ·\u0020 + DiPlay connects through the car hotspot “%1$s”. Turn it on in the car settings, then connect. + Edit saved hotspot ·\u0020 + If ADB lists several devices, use adb -s <car-device-ID> shell appops set %1$s GET_USAGE_STATS allow. Find the ID with adb devices. + Ready for\u0020 + Stream type %1$d is unavailable here (state=%2$s) + Handshake resolution:\u0020 + Safe area: full screen at\u0020 + Safe area:\u0020 + The following settings are unavailable and hidden on Android 9 (API 28):\n• Wi-Fi P2P (5 GHz) — LocalOnlyHotspot is used instead.\n• HEVC software decoder — hardware decoding is used instead. + Address must start with http:// or https://. Token is optional and sent as an Authorization bearer token. DiPlay Language App language @@ -6,4 +21,320 @@ Choose a language for DiPlay. System default follows the device language. Apply Cancel + A familiar drive. + A previous Wi-Fi Direct connection is still running. Reset it to connect. + About + About DiPlay + Active + Advanced audio channel mapping + AirPlay icon + Allow CarPlay UI outside the safe area + Allow Nearby devices and, on older Android versions, Location before resetting CarPlay Wi-Fi. + Allow Nearby devices for DiPlay in the head unit’s app permissions. + Allow Nearby devices so DiPlay can connect to your paired iPhone. + Allow precise Location for DiPlay in the head unit’s app permissions. + Allow the connection permission to continue + An independent CarPlay receiver for Android head units. Wired and wireless connections run on the head unit, with local authentication. A standard iPhone can connect without a jailbreak, Mac, dongle or sign-in.\n\nThis preview uses an experimental accessory identity. Compatibility with every iPhone and head unit is still being tested. It is not an Apple-certified product. + Android 9 compatibility + App permissions + App settings + Apply and reconnect + Apply reconnects CarPlay for size, resolution, music buffer and frame rate. Other changes apply to your next connection. + Audio + Audio routing + Auto + Auto-start on boot + Automatic cluster map setup + Automatic connection + Automatic map enabled. Open the cluster map card or select Map theme. + Automatic map setup · ADB + Availability depends on your head unit’s startup settings. + Back + Back to DiPlay + Band + Bluetooth settings + BYD navigation + Cancel + Car home + Car hotspot details + Car hotspot is off + Car marker · horizontal + Car marker · vertical + CarPlay + CarPlay, at home in your car. + CarPlay connected + CarPlay control running + CarPlay control window ended + CarPlay icon + CarPlay map on instrument cluster · experimental + CarPlay service already running + CarPlay session already running + CarPlay session ended; reconnecting + CarPlay settings + CarPlay size + Changes the size of CarPlay icons and text. Applying a size reconnects CarPlay. + Channel (0 = auto) + Channel is not valid for the selected band + Channel must be 0 or 1-196 + Check and enable + Check that storage is available, or choose another save location. + Choose how to connect, follow the setup steps and save your car hotspot details. + Choose image + Choose iPhone + Choose location + Choose save location + Choose where to save your report.\u0020 + Choose your iPhone + Close + Cluster map size + Connect + Connect phone + Connect when DiPlay opens + Connect with USB + Connect your iPhone with a USB cable + Connecting Bluetooth + Connecting iAP2 control + Connecting to your iPhone… + Connection + Connection interrupted. Retrying… + Connection setup + Copied to the car clipboard. Run the command on your computer. + Copy command + Copy these from the car’s hotspot settings. Use 5 GHz if available. Saving here does not change the car’s hotspot. + Could not decode image + Could not encode image + Could not reset Wi-Fi Direct. Close the other projection app and try again. + Could not save image + Could not save the report + Could not start CarPlay. Return to DiPlay and check app permissions. + Custom 1:1 icon + Debug logs + Default icon + Default placeholder icon + Diagnostic report saved + Diagnostics + DiPlay + Discard changes and exit settings + Disconnect + Discovering iPhone + Display and performance + Display & video + Done + Drag to move, pinch to zoom + Draw outside safe area + Driving side + Efficient video + Enable the car’s Bluetooth and pair your iPhone first. + Error + EXIT APPLICATION + Finish setup · Save your hotspot details to use this mode. + Follow instrument theme and map card + Frame rate + Full screen + Fullscreen + Getting CarPlay ready… + Got it + Handshake resolution: waiting for display + HEVC H.265 video transport + HEVC software decoder + Hide bottom bar + Hide keyboard + Hide the car’s system bars while CarPlay is open. + Hide the navigation bar + Hide the status bar + Hide top bar + Hotspot name + Hotspot password + Hotspot password contains U+0000 + Hotspot setup + Hotspot SSID + Hotspot SSID contains U+0000 + Hotspot SSID is required + Hotspot SSID must be at most 32 UTF-8 bytes + Hotspot status + I2C + I2C device + I2C device path contains U+0000 + I2C device path is required + Identity & appearance + Image is not ready + In CarPlay, swipe down with three fingers to open DiPlay settings. + Instrument contrast + Instrument theme + Keep Bluetooth and Wi-Fi on your iPhone. Pair with the car’s Bluetooth, then select your iPhone here. Allow Nearby devices when asked. + Keep your iPhone nearby with Bluetooth and Wi-Fi on.\nAllow CarPlay if your iPhone asks. + Later + Left-hand drive + Linux device path, for example /dev/i2c-1. + Loading image + Local offline + LocalOnlyHotspot + Location + Longest height + Looking for your paired iPhone… + Made possible by open source + Make DiPlay feel right for your car. + Manual hotspot + Manual mode: match the cluster theme here. The map cannot follow card visibility without Usage Access. + Manufacturer + MFi authentication ready + MFI certificate & signing target + Model + Music buffer + Native + Navigation on HUD and instrument cluster + Navigation stream type + Nearby devices + Nearby devices connects your iPhone. Microphone enables Siri and calls. Older Android versions also require Location for wireless setup. USB mode may ask for a local VPN connection. + Nothing is sent automatically. Protocol payloads and credentials are excluded. + OEM label + On your iPhone, open Settings → Bluetooth and pair with the car. Then return to DiPlay and choose Connect phone. + One-time setup on this car + Open + Open after the car starts + Open Bluetooth + Open car hotspot settings + Open car settings + Open car Wi-Fi settings + Open CarPlay + Open connection setup + Open this setting from your car’s Settings app. + Opening CarPlay… + Opening USB data paths + Pair another + Pair your iPhone + Pair your iPhone with the car’s Bluetooth, keep Wi-Fi on, and allow CarPlay on the iPhone. Close any other phone-projection app.\n\nIf a previous projection app left its connection running, reset CarPlay Wi-Fi below and connect again. Your car’s normal internet Wi-Fi stays on. + Paired device + Pairing with iPhone + Password must be empty when security is Open + Permissions and connection help + Physical length + Physical size basis + Place CarPlay’s controls closer to the driver. + Plug your iPhone into a USB data port.\nAllow CarPlay when your iPhone asks. + Prefer a cable? + Preparing MFi authentication + Ready + Ready when you are + Receiver based on xcertplay, licensed under GPL-3.0. DiPlay’s interface follows DiAuto’s design, licensed under AGPL-3.0.\n\nIncludes AndroidX, Bouncy Castle, JmDNS and SLF4J. Source and license notices accompany this release.\n\nCarPlay and the CarPlay icon belong to Apple Inc. DiPlay is an independent project. + Reconnecting to your iPhone… + Remote + Remote server address contains U+0000 + Remote server address is required + Remote server address must start with http:// or https:// + Remote token contains U+0000 + Report Android location to the iPhone + Report location to iPhone + Report saved. Open it from your file manager to share it. + Reports save to Downloads/DiPlay.\u0020 + Requesting iPhone USB permission + Requesting MFi USB permission + Reset + Reset and connect + Reset car marker to centre + Reset CarPlay Wi-Fi + Reset CarPlay Wi-Fi? + Resolution + Return from car settings to DiPlay, then connect. Accept the CarPlay prompt on your iPhone. A car internet plan is not required; mobile data availability depends on your phone’s network settings. + Review app permissions + Right-hand drive + Running + 0.3x + 1000 ms · most stable + 1.0x + 1 · Choose your connection + 1. Connect a computer with ADB installed to the car using your existing USB or wireless debugging connection. Approve debugging on the car if asked.\n\n2. Run this command in the computer’s terminal: + 1. Open car hotspot settings, turn the hotspot on and select 5 GHz if available.\n2. Copy its name and password below exactly.\n3. Leave the car hotspot on when connecting. DiPlay sends the details over Bluetooth; your iPhone joins automatically. + 2.4 GHz + 2 · Pair your iPhone + 300 ms · default + 30 fps · lighter load + 3 · Connect + 3. Tap Check and enable below. This enables the cluster map and automatic theme following. An active CarPlay session reconnects once. You can then disconnect the computer. + 500 ms + 5 GHz + 60 fps · smoother motion + 60% · lightest load + 80% · lighter load + Safe area + Safe area: waiting for activity size + Save + Save 1:1 + Save and reconnect + Save details + Save diagnostic report + Save hotspot details and use this mode + Save the name and password from the car’s hotspot settings first + Save your hotspot details in Connection setup first + Saved for your next connection + Saving report… + Security + Selecting CarPlay configuration + Sends precise Android location as CarPlay GPS data when the iPhone requests it. + Server address + Set + Set up once. Your details stay saved for the next drive. Changes apply to your next connection. + Settings + Setup needs attention + Share + Show on-screen debug logs + Show password + Show phone navigation arrows, distance and street names on supported BYD displays. Vehicle compatibility varies. + Show the side map only when its card is open, and switch to the full map in Map theme. Theme and card changes keep CarPlay connected. + Shows the iPhone\'s cluster map on the instrument cluster. Choose Small or Full screen navi in the cluster\'s steering-wheel menu. + Start CarPlay automatically after device boot + Starting + Starting AirPlay service + Starting wireless hotspot + Startup + Still waiting for Usage Access. Check that the command ran successfully for this car, then try again. + Tap a number to test. 14 = driver speaker on BYD. + The head unit couldn’t start CarPlay Wi-Fi. Check Wi-Fi and close other projection apps. Retrying… + This ends the existing Wi-Fi Direct connection, including one left behind after reinstalling. Close other projection apps first. Your car’s internet Wi-Fi stays on. + This head unit cannot use the smaller size at this resolution. Using Default. + This head unit could not open a save location. Please try saving to Downloads again. + This head unit does not support Wi-Fi Direct. + This head unit has no available file picker to save the report. + This head unit may not support wireless CarPlay. Try a USB connection. + Token (optional) + Transport error; reconnecting + Turn on Bluetooth + Turn on Wi-Fi in the head unit’s settings to connect. + Turn the car’s Wi-Fi switch on. Allow Location / Nearby devices if requested and enable Location when Android asks. If playback stutters, try the built-in car hotspot at 5 GHz. + Usage Access: enabled + Usage Access lets DiPlay follow the instrument theme and map card. Only BYD cluster activity events are processed locally. This car does not provide a working permission settings page. + Usage Access: setup needed for automatic mode + USB/CH341 + Use a USB data cable and the car’s USB data port. Unlock your iPhone and allow CarPlay. No hotspot setup is needed. + Use a USB data cable and unlock your iPhone.\nAllow Trust and CarPlay if your iPhone asks. + Use HEVC. Leave off for the widest head-unit compatibility. + Use software HEVC decoder + Use usage / content-type routing instead of stream type + Use your last connection type and selected iPhone. + VPN consent was denied + Waiting for iPhone over USB + Waiting for MFi coprocessor + Waiting for paired iPhone + Wi-Fi Direct is still busy. Close the other projection app and try again. + Wi-Fi P2P (5 GHz) + Wi-Fi session + Widest width + Window + WIRELESS CARPLAY + Wireless CarPlay + Wireless CarPlay active + Wireless CarPlay control running + Wireless CarPlay transport + Wireless connection help + Wireless hotspot: off + Wireless permissions + WPA2 + WPA2/WPA3 password must be 8-63 characters + WPA3 + WPA3 transition + Your drive, your way. + Your iPhone isn’t available. Unlock it and check Bluetooth. + Your maps, music and conversations.\nCarPlay, right here on your car display. + YOUR PHONE. YOUR DRIVE. + Your report was saved to the selected location.