mirror of
https://github.com/jahruz67/Ambient-Launcher.git
synced 2026-08-08 18:14:05 +00:00
Add new XML layout files for view settings, view settings v2, and voice interface
- Created view_settings.xml to define the layout for the settings view. - Added view_settings_v2.xml for an updated version of the settings layout. - Introduced view_voice.xml to establish the layout for the voice interface.
This commit is contained in:
@@ -5,14 +5,23 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent>
|
||||
<package android:name="com.amazon.dee.app" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
@@ -45,5 +54,15 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="microphone"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<service
|
||||
android:name=".KeepAwakeService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:stopWithTask="false">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Maintains the screen awake state based on user settings across all applications." />
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.ambient.launcher
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.media.session.MediaSession
|
||||
import android.media.session.PlaybackState
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class BluetoothController(private val context: Context) {
|
||||
private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
|
||||
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
private val _status = MutableStateFlow(BluetoothSnapshot())
|
||||
val status: StateFlow<BluetoothSnapshot> = _status.asStateFlow()
|
||||
|
||||
private var a2dpSinkProxy: BluetoothProfile? = null
|
||||
private var avrcpControllerProxy: BluetoothProfile? = null
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
private val A2DP_SINK = 11
|
||||
private val AVRCP_CONTROLLER = 12
|
||||
|
||||
private fun stateName(state: Int): String = when (state) {
|
||||
BluetoothProfile.STATE_DISCONNECTED -> "DISCONNECTED"
|
||||
BluetoothProfile.STATE_CONNECTING -> "CONNECTING"
|
||||
BluetoothProfile.STATE_CONNECTED -> "CONNECTED"
|
||||
BluetoothProfile.STATE_DISCONNECTING -> "DISCONNECTING"
|
||||
else -> "UNKNOWN($state)"
|
||||
}
|
||||
|
||||
// Track the device that is currently connected so we don't interfere with it
|
||||
private var lastConnectedDevice: android.bluetooth.BluetoothDevice? = null
|
||||
|
||||
private val profileListener = object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
when (profile) {
|
||||
A2DP_SINK -> {
|
||||
a2dpSinkProxy = proxy
|
||||
Log.d("BluetoothController", "A2DP Sink proxy connected")
|
||||
updateConnectionState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {
|
||||
when (profile) {
|
||||
A2DP_SINK -> {
|
||||
a2dpSinkProxy = null
|
||||
Log.d("BluetoothController", "A2DP Sink proxy disconnected")
|
||||
}
|
||||
}
|
||||
updateConnectionState()
|
||||
}
|
||||
}
|
||||
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Log.d("BluetoothController", "Received broadcast: ${intent.action}")
|
||||
when (intent.action) {
|
||||
"android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED" -> {
|
||||
val state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1)
|
||||
val prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1)
|
||||
val device = intent.getParcelableExtra<android.bluetooth.BluetoothDevice>(
|
||||
BluetoothDeviceExtra
|
||||
)
|
||||
Log.i("BluetoothController", "A2DP Sink ${stateName(prevState)} -> ${stateName(state)} device=$device")
|
||||
|
||||
when (state) {
|
||||
BluetoothProfile.STATE_CONNECTED -> {
|
||||
lastConnectedDevice = device
|
||||
updateConnectionState()
|
||||
}
|
||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||
if (device == lastConnectedDevice) {
|
||||
lastConnectedDevice = null
|
||||
}
|
||||
updateConnectionState()
|
||||
}
|
||||
else -> updateConnectionState()
|
||||
}
|
||||
}
|
||||
BluetoothAdapter.ACTION_STATE_CHANGED -> {
|
||||
updateBluetoothState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
setupMediaSession()
|
||||
if (bluetoothAdapter == null) {
|
||||
_status.value = _status.value.copy(isSupported = false)
|
||||
} else {
|
||||
updateBluetoothState()
|
||||
bluetoothAdapter.getProfileProxy(context, profileListener, A2DP_SINK)
|
||||
|
||||
val filter = IntentFilter().apply {
|
||||
addAction("android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED")
|
||||
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||
}
|
||||
context.registerReceiver(receiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMediaSession() {
|
||||
mediaSession = MediaSession(context, "AmbientLauncher").apply {
|
||||
setCallback(object : MediaSession.Callback() {
|
||||
// Diagnostic: Media controls disabled
|
||||
})
|
||||
val state = PlaybackState.Builder()
|
||||
.setActions(0)
|
||||
.setState(PlaybackState.STATE_STOPPED, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f)
|
||||
.build()
|
||||
setPlaybackState(state)
|
||||
isActive = false // Diagnostic: Do not activate by default
|
||||
}
|
||||
Log.d("BluetoothController", "MediaSession initialized (inactive)")
|
||||
}
|
||||
|
||||
private fun updateBluetoothState() {
|
||||
val enabled = bluetoothAdapter?.isEnabled == true
|
||||
_status.value = _status.value.copy(isEnabled = enabled)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun updateConnectionState() {
|
||||
val sinkProxy = a2dpSinkProxy
|
||||
val devices = sinkProxy?.connectedDevices ?: emptyList()
|
||||
val device = devices.firstOrNull()
|
||||
|
||||
// Diagnostic build: No audio focus request or media session activation
|
||||
|
||||
_status.value = _status.value.copy(
|
||||
connectedDeviceName = device?.name ?: device?.address
|
||||
)
|
||||
}
|
||||
|
||||
fun play() { /* Diagnostic stub */ }
|
||||
fun pause() { /* Diagnostic stub */ }
|
||||
fun next() { /* Diagnostic stub */ }
|
||||
fun previous() { /* Diagnostic stub */ }
|
||||
|
||||
fun release() {
|
||||
runCatching { context.unregisterReceiver(receiver) }
|
||||
bluetoothAdapter?.closeProfileProxy(A2DP_SINK, a2dpSinkProxy)
|
||||
mediaSession?.release()
|
||||
mediaSession = null
|
||||
Log.d("BluetoothController", "Released resources and media session")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra name for the BluetoothDevice parcelable in A2DP Sink broadcasts.
|
||||
* On older Android versions the extra may use BluetoothDevice.EXTRA_DEVICE;
|
||||
* this handles both.
|
||||
*/
|
||||
private val BluetoothDeviceExtra: String by lazy {
|
||||
try {
|
||||
// Use the standard BluetoothDevice.EXTRA_DEVICE constant
|
||||
android.bluetooth.BluetoothDevice::class.java
|
||||
.getField("EXTRA_DEVICE")
|
||||
.get(null) as String
|
||||
} catch (_: Exception) {
|
||||
"android.bluetooth.device.extra.DEVICE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.ambient.launcher
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.BatteryManager
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
|
||||
class KeepAwakeService : Service() {
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var mode: KeepAwakeMode = KeepAwakeMode.SYSTEM_DEFAULT
|
||||
private var charging: Boolean = false
|
||||
|
||||
private val batteryReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||
charging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||
status == BatteryManager.BATTERY_STATUS_FULL
|
||||
updateWakeLock()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
|
||||
wakeLock = powerManager.newWakeLock(
|
||||
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
||||
"AmbientLauncher:KeepAwakeGlobal"
|
||||
)
|
||||
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||
registerReceiver(batteryReceiver, filter)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val modeName = intent?.getStringExtra(EXTRA_MODE)
|
||||
mode = KeepAwakeMode.entries.find { it.name == modeName } ?: KeepAwakeMode.SYSTEM_DEFAULT
|
||||
|
||||
if (mode == KeepAwakeMode.SYSTEM_DEFAULT || mode == KeepAwakeMode.WHILE_VISIBLE) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
promoteToForeground()
|
||||
updateWakeLock()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun updateWakeLock() {
|
||||
val shouldKeep = when (mode) {
|
||||
KeepAwakeMode.ALWAYS -> true
|
||||
KeepAwakeMode.WHILE_CHARGING -> charging
|
||||
else -> false
|
||||
}
|
||||
|
||||
if (shouldKeep) {
|
||||
if (wakeLock?.isHeld == false) {
|
||||
wakeLock?.acquire()
|
||||
}
|
||||
} else {
|
||||
if (wakeLock?.isHeld == true) {
|
||||
wakeLock?.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun promoteToForeground() {
|
||||
val type = if (Build.VERSION.SDK_INT >= 34) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
} else 0
|
||||
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NOTIFICATION_ID,
|
||||
createNotification(),
|
||||
type
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.notification_keep_awake_title))
|
||||
.setContentText(getString(R.string.notification_keep_awake_text))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Keep Awake Service",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterReceiver(batteryReceiver)
|
||||
if (wakeLock?.isHeld == true) {
|
||||
wakeLock?.release()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
const val EXTRA_MODE = "mode"
|
||||
private const val CHANNEL_ID = "keep_awake_channel"
|
||||
private const val NOTIFICATION_ID = 888
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Geocoder
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -17,26 +18,22 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class LauncherUiState(
|
||||
val settings: LauncherSettings = LauncherSettings(),
|
||||
val apps: List<LauncherApp> = emptyList(),
|
||||
val localImages: List<Any> = emptyList(),
|
||||
val weather: WeatherNow? = null,
|
||||
val weatherLoading: Boolean = false,
|
||||
val weatherError: String? = null,
|
||||
val wakeWord: WakeWordSnapshot = WakeWordSnapshot()
|
||||
)
|
||||
|
||||
class LauncherViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val store = SettingsStore(application)
|
||||
private val weatherRepository = WeatherRepository()
|
||||
private val wakeWordController = WakeWordController(application)
|
||||
// private val bluetoothController = BluetoothController(application)
|
||||
private val _uiState = MutableStateFlow(LauncherUiState())
|
||||
val uiState: StateFlow<LauncherUiState> = _uiState.asStateFlow()
|
||||
private var settingsLoaded = false
|
||||
|
||||
init {
|
||||
loadApps()
|
||||
/* viewModelScope.launch {
|
||||
bluetoothController.status.collectLatest { status ->
|
||||
_uiState.value = _uiState.value.copy(bluetooth = status)
|
||||
}
|
||||
} */
|
||||
viewModelScope.launch {
|
||||
wakeWordController.status.collectLatest { status ->
|
||||
_uiState.value = _uiState.value.copy(wakeWord = status)
|
||||
@@ -63,7 +60,10 @@ class LauncherViewModel(application: Application) : AndroidViewModel(application
|
||||
_uiState.value = _uiState.value.copy(settings = settings)
|
||||
wakeWordController.configure(
|
||||
settings.wakeWord.enabled,
|
||||
settings.wakeWord.sensitivity
|
||||
settings.wakeWord.sensitivity,
|
||||
settings.wakeWord.autoCloseSeconds,
|
||||
settings.wakeWord.toggleClose,
|
||||
settings.language
|
||||
)
|
||||
if (
|
||||
_uiState.value.wakeWord.modelState == ModelInstallState.READY &&
|
||||
@@ -111,17 +111,36 @@ class LauncherViewModel(application: Application) : AndroidViewModel(application
|
||||
|
||||
fun refreshWeather() {
|
||||
val settings = _uiState.value.settings
|
||||
val lat = settings.latitude.toDoubleOrNull()
|
||||
val lon = settings.longitude.toDoubleOrNull()
|
||||
if (lat == null || lon == null || lat !in -90.0..90.0 || lon !in -180.0..180.0) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
weatherLoading = false,
|
||||
weatherError = "Enter valid latitude and longitude."
|
||||
)
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(weatherLoading = true, weatherError = null)
|
||||
|
||||
val (lat, lon) = withContext(Dispatchers.IO) {
|
||||
val latD = settings.latitude.toDoubleOrNull()
|
||||
val lonD = settings.longitude.toDoubleOrNull()
|
||||
|
||||
if (latD != null && lonD != null && latD in -90.0..90.0 && lonD in -180.0..180.0) {
|
||||
latD to lonD
|
||||
} else if (settings.locationName.isNotBlank()) {
|
||||
runCatching {
|
||||
@Suppress("DEPRECATION")
|
||||
Geocoder(getApplication()).getFromLocationName(settings.locationName, 1)
|
||||
?.firstOrNull()
|
||||
?.let { it.latitude to it.longitude }
|
||||
}.getOrNull()
|
||||
} else null
|
||||
} ?: run {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
weatherLoading = false,
|
||||
weatherError = "Enter a city name or valid coordinates."
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Update settings with found coordinates if they were missing/different
|
||||
if (settings.latitude != lat.toString() || settings.longitude != lon.toString()) {
|
||||
updateSettings { it.copy(latitude = lat.toString(), longitude = lon.toString()) }
|
||||
}
|
||||
|
||||
runCatching { weatherRepository.load(lat, lon, settings.useFahrenheit) }
|
||||
.onSuccess {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
@@ -154,10 +173,31 @@ class LauncherViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
}
|
||||
|
||||
fun setWakeWordAutoClose(seconds: Int) {
|
||||
updateSettings { settings ->
|
||||
settings.copy(wakeWord = settings.wakeWord.copy(autoCloseSeconds = seconds))
|
||||
}
|
||||
}
|
||||
|
||||
fun setWakeWordToggleClose(enabled: Boolean) {
|
||||
updateSettings { settings ->
|
||||
settings.copy(wakeWord = settings.wakeWord.copy(toggleClose = enabled))
|
||||
}
|
||||
}
|
||||
|
||||
fun setLanguage(language: AppLanguage) {
|
||||
updateSettings { it.copy(language = language) }
|
||||
}
|
||||
|
||||
fun pauseWakeWord() = wakeWordController.pause()
|
||||
fun resumeWakeWord() = wakeWordController.resume()
|
||||
fun testWakeWord() = wakeWordController.testDetection()
|
||||
|
||||
fun bluetoothPlay() { /* bluetoothController.play() */ }
|
||||
fun bluetoothPause() { /* bluetoothController.pause() */ }
|
||||
fun bluetoothNext() { /* bluetoothController.next() */ }
|
||||
fun bluetoothPrevious() { /* bluetoothController.previous() */ }
|
||||
|
||||
fun launch(app: LauncherApp): Boolean {
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
@@ -168,6 +208,11 @@ class LauncherViewModel(application: Application) : AndroidViewModel(application
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
// bluetoothController.release()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun loadApps() {
|
||||
viewModelScope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,26 @@ package com.ambient.launcher
|
||||
import android.content.ComponentName
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
data class LauncherUiState(
|
||||
val settings: LauncherSettings = LauncherSettings(),
|
||||
val apps: List<LauncherApp> = emptyList(),
|
||||
val localImages: List<Any> = emptyList(),
|
||||
val weather: WeatherNow? = null,
|
||||
val weatherLoading: Boolean = false,
|
||||
val weatherError: String? = null,
|
||||
val wakeWord: WakeWordSnapshot = WakeWordSnapshot(),
|
||||
val bluetooth: BluetoothSnapshot = BluetoothSnapshot()
|
||||
)
|
||||
|
||||
data class BluetoothSnapshot(
|
||||
val connectedDeviceName: String? = null,
|
||||
val isPlaying: Boolean = false,
|
||||
val artist: String? = null,
|
||||
val title: String? = null,
|
||||
val isSupported: Boolean = true,
|
||||
val isEnabled: Boolean = false
|
||||
)
|
||||
|
||||
data class LauncherApp(
|
||||
val label: String,
|
||||
val component: ComponentName,
|
||||
@@ -34,15 +54,18 @@ data class DailyWeather(
|
||||
val weatherCode: Int
|
||||
)
|
||||
|
||||
enum class KeepAwakeMode { WHILE_VISIBLE, WHILE_CHARGING, SYSTEM_DEFAULT }
|
||||
enum class KeepAwakeMode { WHILE_VISIBLE, WHILE_CHARGING, ALWAYS, SYSTEM_DEFAULT }
|
||||
enum class ClockCorner { BOTTOM_LEFT, BOTTOM_RIGHT, TOP_LEFT, TOP_RIGHT }
|
||||
enum class AppLanguage { ENGLISH, SPANISH, ENGLISH_SPANISH, SPANISH_ENGLISH }
|
||||
enum class ModelInstallState { NOT_INSTALLED, DOWNLOADING, INSTALLING, READY, FAILED }
|
||||
enum class WakeWordState { DISABLED, DOWNLOADING, INSTALLING, READY, LISTENING, PAUSED, MICROPHONE_BUSY, ERROR }
|
||||
|
||||
data class WakeWordSettings(
|
||||
val enabled: Boolean = false,
|
||||
val sensitivity: Int = 50,
|
||||
val installedModelVersion: String = ""
|
||||
val installedModelVersion: String = "",
|
||||
val autoCloseSeconds: Int = 60,
|
||||
val toggleClose: Boolean = false
|
||||
)
|
||||
|
||||
data class WakeWordSnapshot(
|
||||
@@ -70,6 +93,7 @@ data class LauncherSettings(
|
||||
val nightStartHour: Int = 23,
|
||||
val nightEndHour: Int = 7,
|
||||
val nightDimPercent: Int = 88,
|
||||
val language: AppLanguage = AppLanguage.ENGLISH,
|
||||
val wakeWord: WakeWordSettings = WakeWordSettings()
|
||||
)
|
||||
|
||||
@@ -78,18 +102,18 @@ sealed interface Slide {
|
||||
data object Weather : Slide
|
||||
}
|
||||
|
||||
fun weatherDescription(code: Int): String = when (code) {
|
||||
0 -> "Clear"
|
||||
1, 2 -> "Partly cloudy"
|
||||
3 -> "Overcast"
|
||||
45, 48 -> "Fog"
|
||||
51, 53, 55, 56, 57 -> "Drizzle"
|
||||
61, 63, 65, 66, 67 -> "Rain"
|
||||
71, 73, 75, 77 -> "Snow"
|
||||
80, 81, 82 -> "Rain showers"
|
||||
85, 86 -> "Snow showers"
|
||||
95, 96, 99 -> "Thunderstorm"
|
||||
else -> "Unknown"
|
||||
fun weatherDescription(code: Int): Int = when (code) {
|
||||
0 -> R.string.weather_clear
|
||||
1, 2 -> R.string.weather_partly_cloudy
|
||||
3 -> R.string.weather_overcast
|
||||
45, 48 -> R.string.weather_fog
|
||||
51, 53, 55, 56, 57 -> R.string.weather_drizzle
|
||||
61, 63, 65, 66, 67 -> R.string.weather_rain
|
||||
71, 73, 75, 77 -> R.string.weather_snow
|
||||
80, 81, 82 -> R.string.weather_rain_showers
|
||||
85, 86 -> R.string.weather_snow_showers
|
||||
95, 96, 99 -> R.string.weather_thunderstorm
|
||||
else -> R.string.weather_unknown
|
||||
}
|
||||
|
||||
fun weatherGlyph(code: Int): String = when (code) {
|
||||
|
||||
@@ -29,9 +29,12 @@ class SettingsStore(private val context: Context) {
|
||||
val nightStart = intPreferencesKey("night_start")
|
||||
val nightEnd = intPreferencesKey("night_end")
|
||||
val nightDim = intPreferencesKey("night_dim")
|
||||
val language = stringPreferencesKey("language")
|
||||
val wakeEnabled = booleanPreferencesKey("wake_enabled")
|
||||
val wakeSensitivity = intPreferencesKey("wake_sensitivity")
|
||||
val wakeModelVersion = stringPreferencesKey("wake_model_version")
|
||||
val wakeAutoClose = intPreferencesKey("wake_auto_close")
|
||||
val wakeToggleClose = booleanPreferencesKey("wake_toggle_close")
|
||||
}
|
||||
|
||||
val settings: Flow<LauncherSettings> = context.dataStore.data.map { p ->
|
||||
@@ -56,10 +59,13 @@ class SettingsStore(private val context: Context) {
|
||||
nightStartHour = p[Keys.nightStart] ?: 23,
|
||||
nightEndHour = p[Keys.nightEnd] ?: 7,
|
||||
nightDimPercent = p[Keys.nightDim] ?: 88,
|
||||
language = enumOrDefault(p[Keys.language], AppLanguage.ENGLISH),
|
||||
wakeWord = WakeWordSettings(
|
||||
enabled = p[Keys.wakeEnabled] ?: false,
|
||||
sensitivity = p[Keys.wakeSensitivity] ?: 50,
|
||||
installedModelVersion = p[Keys.wakeModelVersion] ?: ""
|
||||
installedModelVersion = p[Keys.wakeModelVersion] ?: "",
|
||||
autoCloseSeconds = p[Keys.wakeAutoClose] ?: 60,
|
||||
toggleClose = p[Keys.wakeToggleClose] ?: false
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -82,9 +88,12 @@ class SettingsStore(private val context: Context) {
|
||||
p[Keys.nightStart] = value.nightStartHour
|
||||
p[Keys.nightEnd] = value.nightEndHour
|
||||
p[Keys.nightDim] = value.nightDimPercent
|
||||
p[Keys.language] = value.language.name
|
||||
p[Keys.wakeEnabled] = value.wakeWord.enabled
|
||||
p[Keys.wakeSensitivity] = value.wakeWord.sensitivity
|
||||
p[Keys.wakeModelVersion] = value.wakeWord.installedModelVersion
|
||||
p[Keys.wakeAutoClose] = value.wakeWord.autoCloseSeconds
|
||||
p[Keys.wakeToggleClose] = value.wakeWord.toggleClose
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
|
||||
enum class AssistLaunchResult { LAUNCHED, NOT_CONFIGURED, BACKGROUND_BLOCKED }
|
||||
|
||||
@@ -32,6 +33,22 @@ class DefaultAssistantLauncher(private val context: Context) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
private fun assistantIntent() = Intent(Intent.ACTION_ASSIST)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
private fun assistantIntent(): Intent {
|
||||
val alexaPackage = "com.amazon.dee.app"
|
||||
val currentAssistant = Settings.Secure.getString(context.contentResolver, "assistant")
|
||||
|
||||
// If Alexa is the default assistant, try to pre-activate it using VOICE_COMMAND
|
||||
if (currentAssistant?.contains(alexaPackage) == true) {
|
||||
val voiceCommandIntent = Intent(Intent.ACTION_VOICE_COMMAND)
|
||||
.setPackage(alexaPackage)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
|
||||
if (voiceCommandIntent.resolveActivity(context.packageManager) != null) {
|
||||
return voiceCommandIntent
|
||||
}
|
||||
}
|
||||
|
||||
return Intent(Intent.ACTION_ASSIST)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import com.ambient.launcher.AppLanguage
|
||||
import com.ambient.launcher.ModelInstallState
|
||||
import com.ambient.launcher.WakeWordSnapshot
|
||||
import com.ambient.launcher.WakeWordState
|
||||
@@ -46,6 +47,9 @@ class WakeWordController(private val context: Context) {
|
||||
private var enabled = false
|
||||
private var visible = false
|
||||
private var sensitivity = 50
|
||||
private var autoCloseSeconds = 0
|
||||
private var toggleClose = false
|
||||
private var language = AppLanguage.ENGLISH
|
||||
|
||||
init {
|
||||
val lastDetection = context.getSharedPreferences("wake_runtime", Context.MODE_PRIVATE)
|
||||
@@ -96,16 +100,28 @@ class WakeWordController(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun configure(isEnabled: Boolean, newSensitivity: Int) {
|
||||
fun configure(
|
||||
isEnabled: Boolean,
|
||||
newSensitivity: Int,
|
||||
newAutoClose: Int = 0,
|
||||
newToggleClose: Boolean = false,
|
||||
newLanguage: AppLanguage = AppLanguage.ENGLISH
|
||||
) {
|
||||
enabled = isEnabled
|
||||
sensitivity = newSensitivity.coerceIn(0, 100)
|
||||
autoCloseSeconds = newAutoClose
|
||||
toggleClose = newToggleClose
|
||||
language = newLanguage
|
||||
if (!enabled) disable()
|
||||
else if (visible) enable()
|
||||
}
|
||||
|
||||
fun setVisible(isVisible: Boolean) {
|
||||
visible = isVisible
|
||||
if (visible && enabled) enable()
|
||||
if (visible) {
|
||||
if (enabled) enable()
|
||||
sendAction(WakeWordService.ACTION_REPORT_VISIBLE)
|
||||
}
|
||||
}
|
||||
|
||||
fun enable() {
|
||||
@@ -139,6 +155,9 @@ class WakeWordController(private val context: Context) {
|
||||
val intent = Intent(context, WakeWordService::class.java)
|
||||
.setAction(WakeWordService.ACTION_START)
|
||||
.putExtra(WakeWordService.EXTRA_SENSITIVITY, sensitivity)
|
||||
.putExtra(WakeWordService.EXTRA_AUTO_CLOSE, autoCloseSeconds)
|
||||
.putExtra(WakeWordService.EXTRA_TOGGLE_CLOSE, toggleClose)
|
||||
.putExtra(WakeWordService.EXTRA_LANGUAGE, language.name)
|
||||
runCatching { ContextCompat.startForegroundService(context, intent) }
|
||||
.onFailure {
|
||||
WakeWordRuntime.update(
|
||||
|
||||
@@ -44,6 +44,38 @@ object WakeWordModel {
|
||||
fun path(context: Context, name: String) = File(directory(context), name).absolutePath
|
||||
|
||||
private fun File.readTextOrNull(): String? = runCatching { readText() }.getOrNull()
|
||||
|
||||
fun createComputerKeyword(tokensFile: File, outputFile: File) {
|
||||
val tokens = tokensFile.readLines().mapIndexedNotNull { index, line ->
|
||||
val columns = line.trim().split(Regex("\\s+"))
|
||||
val piece = columns.firstOrNull()?.takeIf { it.isNotBlank() } ?: return@mapIndexedNotNull null
|
||||
piece to (columns.getOrNull(1)?.toIntOrNull() ?: index)
|
||||
}
|
||||
val target = "▁COMPUTER"
|
||||
data class Candidate(val pieces: List<Pair<String, Int>>, val score: Int)
|
||||
val best = arrayOfNulls<Candidate>(target.length + 1)
|
||||
best[0] = Candidate(emptyList(), 0)
|
||||
for (start in target.indices) {
|
||||
val current = best[start] ?: continue
|
||||
tokens.forEach { token ->
|
||||
val piece = token.first
|
||||
if (target.startsWith(piece, start)) {
|
||||
val end = start + piece.length
|
||||
val candidate = Candidate(current.pieces + token, current.score + token.second)
|
||||
val previous = best[end]
|
||||
if (
|
||||
previous == null ||
|
||||
candidate.pieces.size < previous.pieces.size ||
|
||||
candidate.pieces.size == previous.pieces.size && candidate.score < previous.score
|
||||
) {
|
||||
best[end] = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val pieces = best[target.length]?.pieces ?: error("Model cannot tokenize COMPUTER")
|
||||
outputFile.writeText(pieces.joinToString(" ") { it.first } + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
class WakeWordModelWorker(
|
||||
@@ -72,7 +104,10 @@ class WakeWordModelWorker(
|
||||
staging.deleteRecursively()
|
||||
staging.mkdirs()
|
||||
extractSelected(archive, staging)
|
||||
createComputerKeyword(File(staging, "tokens.txt"), File(staging, "keywords.txt"))
|
||||
WakeWordModel.createComputerKeyword(
|
||||
File(staging, "tokens.txt"),
|
||||
File(staging, "keywords.txt")
|
||||
)
|
||||
if (!WakeWordModel.requiredFiles.all { File(staging, it).isFile }) {
|
||||
error("Model archive is incomplete")
|
||||
}
|
||||
@@ -151,38 +186,6 @@ class WakeWordModelWorker(
|
||||
}
|
||||
}
|
||||
|
||||
private fun createComputerKeyword(tokensFile: File, output: File) {
|
||||
val tokens = tokensFile.readLines().mapIndexedNotNull { index, line ->
|
||||
val columns = line.trim().split(Regex("\\s+"))
|
||||
val piece = columns.firstOrNull()?.takeIf { it.isNotBlank() } ?: return@mapIndexedNotNull null
|
||||
piece to (columns.getOrNull(1)?.toIntOrNull() ?: index)
|
||||
}
|
||||
val target = "▁COMPUTER"
|
||||
data class Candidate(val pieces: List<Pair<String, Int>>, val score: Int)
|
||||
val best = arrayOfNulls<Candidate>(target.length + 1)
|
||||
best[0] = Candidate(emptyList(), 0)
|
||||
for (start in target.indices) {
|
||||
val current = best[start] ?: continue
|
||||
tokens.forEach { token ->
|
||||
val piece = token.first
|
||||
if (target.startsWith(piece, start)) {
|
||||
val end = start + piece.length
|
||||
val candidate = Candidate(current.pieces + token, current.score + token.second)
|
||||
val previous = best[end]
|
||||
if (
|
||||
previous == null ||
|
||||
candidate.pieces.size < previous.pieces.size ||
|
||||
candidate.pieces.size == previous.pieces.size && candidate.score < previous.score
|
||||
) {
|
||||
best[end] = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val pieces = best[target.length]?.pieces ?: error("Model cannot tokenize COMPUTER")
|
||||
output.writeText(pieces.joinToString(" ") { it.first } + "\n")
|
||||
}
|
||||
|
||||
private fun sha256(file: File): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
file.inputStream().buffered().use { input ->
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.os.Looper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.ambient.launcher.AppLanguage
|
||||
import com.ambient.launcher.MainActivity
|
||||
import com.ambient.launcher.R
|
||||
import com.ambient.launcher.SettingsStore
|
||||
@@ -46,7 +47,12 @@ class WakeWordService : Service() {
|
||||
@Volatile private var listening = false
|
||||
@Volatile private var paused = false
|
||||
private var sensitivity = 50
|
||||
private var autoCloseSeconds = 0
|
||||
private var toggleClose = false
|
||||
private var language = AppLanguage.ENGLISH
|
||||
private var isAssistantActive = false
|
||||
private var lastDetection = 0L
|
||||
private val autoCloseRunnable = Runnable { returnToLauncher() }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -61,6 +67,10 @@ class WakeWordService : Service() {
|
||||
intent.getIntExtra(EXTRA_SENSITIVITY, 50).coerceIn(0, 100)
|
||||
if (requestedSensitivity != sensitivity && listening) stopListening()
|
||||
sensitivity = requestedSensitivity
|
||||
autoCloseSeconds = intent.getIntExtra(EXTRA_AUTO_CLOSE, 0)
|
||||
toggleClose = intent.getBooleanExtra(EXTRA_TOGGLE_CLOSE, false)
|
||||
val langName = intent.getStringExtra(EXTRA_LANGUAGE)
|
||||
language = AppLanguage.entries.find { it.name == langName } ?: AppLanguage.ENGLISH
|
||||
paused = false
|
||||
startListening()
|
||||
}
|
||||
@@ -77,6 +87,10 @@ class WakeWordService : Service() {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
ACTION_REPORT_VISIBLE -> {
|
||||
isAssistantActive = false
|
||||
mainHandler.removeCallbacks(autoCloseRunnable)
|
||||
}
|
||||
ACTION_TEST -> handleDetection(force = true)
|
||||
else -> {
|
||||
WakeWordRuntime.update(
|
||||
@@ -214,6 +228,16 @@ class WakeWordService : Service() {
|
||||
private fun handleDetection(force: Boolean) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (!force && now - lastDetection < DETECTION_DEBOUNCE_MS) return
|
||||
|
||||
if (!force && toggleClose && isAssistantActive) {
|
||||
lastDetection = now
|
||||
returnToLauncher()
|
||||
if (!paused) {
|
||||
mainHandler.postDelayed({ startListening() }, toggleCooldown())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lastDetection = now
|
||||
getSharedPreferences("wake_runtime", MODE_PRIVATE)
|
||||
.edit()
|
||||
@@ -227,10 +251,29 @@ class WakeWordService : Service() {
|
||||
)
|
||||
val launcher = DefaultAssistantLauncher(this)
|
||||
val result = launcher.launch()
|
||||
if (result != AssistLaunchResult.LAUNCHED) showAssistantFallback(launcher)
|
||||
if (!paused) mainHandler.postDelayed({ startListening() }, ASSISTANT_COOLDOWN_MS)
|
||||
if (result == AssistLaunchResult.LAUNCHED) {
|
||||
isAssistantActive = true
|
||||
if (autoCloseSeconds > 0) {
|
||||
mainHandler.removeCallbacks(autoCloseRunnable)
|
||||
mainHandler.postDelayed(autoCloseRunnable, autoCloseSeconds * 1000L)
|
||||
}
|
||||
} else {
|
||||
showAssistantFallback(launcher)
|
||||
}
|
||||
if (!paused) mainHandler.postDelayed({ startListening() }, toggleCooldown())
|
||||
}
|
||||
|
||||
private fun returnToLauncher() {
|
||||
isAssistantActive = false
|
||||
mainHandler.removeCallbacks(autoCloseRunnable)
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
.addCategory(Intent.CATEGORY_HOME)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
runCatching { startActivity(intent) }
|
||||
}
|
||||
|
||||
private fun toggleCooldown(): Long = if (toggleClose) 2_500L else ASSISTANT_COOLDOWN_MS
|
||||
|
||||
private fun pauseListening() {
|
||||
paused = true
|
||||
stopListening()
|
||||
@@ -340,8 +383,12 @@ class WakeWordService : Service() {
|
||||
const val ACTION_PAUSE = "com.ambient.launcher.voice.PAUSE"
|
||||
const val ACTION_RESUME = "com.ambient.launcher.voice.RESUME"
|
||||
const val ACTION_STOP = "com.ambient.launcher.voice.STOP"
|
||||
const val ACTION_REPORT_VISIBLE = "com.ambient.launcher.voice.REPORT_VISIBLE"
|
||||
const val ACTION_TEST = "com.ambient.launcher.voice.TEST"
|
||||
const val EXTRA_SENSITIVITY = "sensitivity"
|
||||
const val EXTRA_AUTO_CLOSE = "auto_close"
|
||||
const val EXTRA_TOGGLE_CLOSE = "toggle_close"
|
||||
const val EXTRA_LANGUAGE = "language"
|
||||
private const val SAMPLE_RATE = 16_000
|
||||
private const val DETECTION_DEBOUNCE_MS = 5_000L
|
||||
private const val ASSISTANT_COOLDOWN_MS = 12_000L
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:angle="315"
|
||||
android:endColor="#16243A"
|
||||
android:startColor="#594755"
|
||||
android:type="linear" />
|
||||
</shape>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M0,0h24v24h-24z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:endX="24"
|
||||
android:endY="24"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#594755" />
|
||||
<item android:offset="1" android:color="#16243A" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<resources>
|
||||
<string name="app_name">Ambient Launcher</string>
|
||||
|
||||
<!-- General -->
|
||||
<string name="back">Atrás</string>
|
||||
<string name="close">Cerrar</string>
|
||||
<string name="done">Hecho</string>
|
||||
<string name="retry">reintentar</string>
|
||||
|
||||
<!-- Home Screen -->
|
||||
<string name="open_apps">Abrir aplicaciones</string>
|
||||
<string name="open_settings">Abrir ajustes</string>
|
||||
<string name="weather_unavailable">Clima no disponible</string>
|
||||
<string name="next_hours">Próximas horas</string>
|
||||
<string name="rain_chance">%1$d%% lluvia</string>
|
||||
<string name="ambient_photograph">Fotografía de ambiente</string>
|
||||
|
||||
<!-- App Tray -->
|
||||
<string name="all_applications">Todas las aplicaciones</string>
|
||||
<string name="quick_launch">Inicio rápido</string>
|
||||
<string name="search_apps">Buscar aplicaciones</string>
|
||||
<string name="add_app">Añadir aplicación</string>
|
||||
<string name="no_apps_pinned">No hay aplicaciones fijadas.</string>
|
||||
<string name="no_matching_apps">No hay aplicaciones coincidentes.</string>
|
||||
|
||||
<!-- Settings Sections -->
|
||||
<string name="display">Pantalla</string>
|
||||
<string name="weather">Clima</string>
|
||||
<string name="routines">Rutina nocturna</string>
|
||||
<string name="voice">Asistente de voz</string>
|
||||
<string name="apps">Aplicaciones</string>
|
||||
<string name="bluetooth">Bluetooth</string>
|
||||
<string name="access">Acceso al sistema</string>
|
||||
<string name="language">Idioma</string>
|
||||
|
||||
<!-- Display Settings -->
|
||||
<string name="keep_awake">Mantener pantalla encendida</string>
|
||||
<string name="keep_awake_detail">Mantiene la pantalla encendida globalmente o según la visibilidad.</string>
|
||||
<string name="keep_awake_visible">Mientras el lanzador sea visible</string>
|
||||
<string name="keep_awake_charging">Solo durante la carga (Global)</string>
|
||||
<string name="keep_awake_always">Siempre (Global)</string>
|
||||
<string name="keep_awake_default">Seguir tiempo de espera de Android</string>
|
||||
<string name="photo_interval">Intervalo de fotos</string>
|
||||
<string name="seconds">%1$d segundos</string>
|
||||
<string name="image_darkening">Oscurecimiento de imagen</string>
|
||||
<string name="clock_corner">Esquina de reloj y clima</string>
|
||||
<string name="clock_corner_detail">El panel se desplaza sutilmente para reducir el desgaste.</string>
|
||||
<string name="photo_source">Fuente de fotos</string>
|
||||
<string name="photo_source_online">Paisajes, ciudades, naturaleza y arte seleccionados en línea.</string>
|
||||
<string name="photo_source_local">Usando la carpeta local seleccionada.</string>
|
||||
<string name="choose_local_folder">Elegir carpeta local</string>
|
||||
<string name="use_online_collection">Usar colección en línea</string>
|
||||
|
||||
<!-- Weather Settings -->
|
||||
<string name="show_weather">Mostrar clima</string>
|
||||
<string name="show_weather_detail">Condiciones compactas y diapositivas de pronóstico completo periódicas.</string>
|
||||
<string name="location">Ubicación</string>
|
||||
<string name="location_detail">Introduzca el nombre de una ciudad o coordenadas. Las coordenadas se rellenan automáticamente al buscar por ciudad.</string>
|
||||
<string name="location_label">Ciudad, Estado/País</string>
|
||||
<string name="location_placeholder">ej. Madrid, ES</string>
|
||||
<string name="latitude">Latitud</string>
|
||||
<string name="longitude">Longitud</string>
|
||||
<string name="searching">Buscando…</string>
|
||||
<string name="search_refresh">Buscar y actualizar</string>
|
||||
<string name="temp_unit">Unidad de temperatura</string>
|
||||
<string name="forecast_frequency">Frecuencia de pronóstico completo</string>
|
||||
<string name="after_every_photos">Después de cada %1$d fotos</string>
|
||||
|
||||
<!-- Routine Settings -->
|
||||
<string name="ultra_dim">Atenuación programada</string>
|
||||
<string name="ultra_dim_detail">Se aplica siempre que el lanzador es visible. Android no se despierta por este horario.</string>
|
||||
<string name="active_hours">Horas activas</string>
|
||||
<string name="start">Inicio</string>
|
||||
<string name="end">Fin</string>
|
||||
<string name="ultra_dim_strength">Fuerza de atenuación</string>
|
||||
<string name="night_mode_note">El modo nocturno también oculta el clima, desactiva el movimiento de imagen y usa un reloj rojo oscuro.</string>
|
||||
|
||||
<!-- Voice Settings -->
|
||||
<string name="voice_listen">Escuchar “Computer”</string>
|
||||
<string name="voice_listen_detail">La detección permanece en el dispositivo. Seguir escuchando en otras aplicaciones requiere una notificación persistente y el indicador de privacidad del micrófono de Android.</string>
|
||||
<string name="status_disabled">Desactivado</string>
|
||||
<string name="status_downloading">Descargando modelo</string>
|
||||
<string name="status_installing">Instalando modelo</string>
|
||||
<string name="status_ready">Listo</string>
|
||||
<string name="status_listening">Escuchando</string>
|
||||
<string name="status_paused">Pausado</string>
|
||||
<string name="status_busy">Micrófono ocupado; reintentando</string>
|
||||
<string name="status_error">Error</string>
|
||||
<string name="sensitivity">Sensibilidad</string>
|
||||
<string name="sensitivity_detail">Valores más altos detectan más fácilmente pero pueden causar activaciones falsas.</string>
|
||||
<string name="auto_close">Cierre automático del asistente</string>
|
||||
<string name="auto_close_detail">Regresar automáticamente al lanzador después de %1$d segundos.</string>
|
||||
<string name="auto_close_manual">El asistente permanecerá abierto hasta que se cierre manualmente.</string>
|
||||
<string name="toggle_wake">Alternar con palabra de activación</string>
|
||||
<string name="toggle_wake_detail">Si el asistente ya está abierto, decir “Computer” de nuevo lo cerrará y regresará aquí.</string>
|
||||
<string name="toggle_cooldown_note">El tiempo de espera de escucha se reduce a 2.5s para alternar más rápido.</string>
|
||||
<string name="listening_controls">Controles de escucha</string>
|
||||
<string name="listening_controls_detail">La prueba libera el micrófono de este lanzador antes de abrir el asistente de Android seleccionado.</string>
|
||||
<string name="resume">Reanudar</string>
|
||||
<string name="pause">Pausar</string>
|
||||
<string name="test_assistant">Probar asistente</string>
|
||||
<string name="open_voice_settings">Abrir ajustes de voz de Android</string>
|
||||
<string name="privacy">Privacidad</string>
|
||||
<string name="privacy_detail">Los marcos de audio se procesan localmente y nunca se guardan ni transmiten.</string>
|
||||
<string name="privacy_note">Solo se conservan el último tiempo de detección y el estado del servicio para diagnóstico.</string>
|
||||
|
||||
<!-- Applications Settings -->
|
||||
<string name="apps_pinned">%1$d aplicación(es) fijada(s)</string>
|
||||
<string name="apps_pinned_detail">Abra la bandeja de aplicaciones desde Inicio, luego elija Añadir aplicación para fijar o quitar aplicaciones.</string>
|
||||
<string name="apps_privacy_detail">El seguimiento de aplicaciones recientes y el acceso de uso están desactivados.</string>
|
||||
<string name="apps_privacy_note">Este lanzador solo lee las actividades que anuncian un icono ejecutable.</string>
|
||||
|
||||
<!-- Access Settings -->
|
||||
<string name="default_home">Aplicación de inicio predeterminada</string>
|
||||
<string name="default_home_detail">Requerido para que el gesto de Inicio del hardware regrese aquí.</string>
|
||||
<string name="make_default_home">Establecer como aplicación de inicio</string>
|
||||
<string name="permission_internet">Internet</string>
|
||||
<string name="permission_internet_detail">Se usa para el clima y la colección de fotos en línea integrada.</string>
|
||||
<string name="permission_local_photo">Carpeta de fotos local</string>
|
||||
<string name="permission_local_photo_detail">Concedido solo a la carpeta que seleccione explíitamente.</string>
|
||||
<string name="permission_location">Ubicación</string>
|
||||
<string name="permission_location_detail">No se solicita. El clima usa las coordenadas introducidas en los ajustes.</string>
|
||||
<string name="permission_microphone">Micrófono</string>
|
||||
<string name="permission_microphone_detail">Se solicita solo cuando la escucha de palabra de activación en el dispositivo está habilitada.</string>
|
||||
<string name="permission_overlay">Mostrar sobre otras aplicaciones</string>
|
||||
<string name="permission_overlay_detail">No se solicita. La atenuación ultra solo cubre este lanzador.</string>
|
||||
<string name="permission_usage">Acceso de uso</string>
|
||||
<string name="permission_usage_detail">No se solicita. No se recoge el historial de aplicaciones recientes.</string>
|
||||
<string name="permission_notifications">Notificaciones</string>
|
||||
<string name="permission_notifications_detail">Se usa para el servicio de escucha visible y la acción de respaldo del asistente.</string>
|
||||
<string name="audio_output">Salida de audio</string>
|
||||
<string name="audio_output_detail">Un lanzador normal no puede forzar a otras aplicaciones a usar un dispositivo específico.</string>
|
||||
<string name="open_sound_settings">Abrir ajustes de sonido de Android</string>
|
||||
<string name="available">Disponible</string>
|
||||
<string name="not_requested">No solicitado</string>
|
||||
|
||||
<!-- Language Settings -->
|
||||
<string name="language_settings_detail">Controles de idioma de la aplicación e idioma del sistema.</string>
|
||||
<string name="app_language">Idioma de la aplicación</string>
|
||||
<string name="lang_english">Inglés</string>
|
||||
<string name="lang_spanish">Español</string>
|
||||
<string name="lang_english_spanish">Inglés-Español</string>
|
||||
<string name="lang_spanish_english">Español-Inglés</string>
|
||||
<string name="system_language">Idioma del sistema</string>
|
||||
<string name="system_language_detail">Cambiar los ajustes de idioma del sistema Android.</string>
|
||||
<string name="open_system_language">Abrir ajustes de idioma del sistema</string>
|
||||
<string name="system_language_warning">El idioma del sistema no está actualizado</string>
|
||||
|
||||
<!-- Bluetooth Settings -->
|
||||
<string name="bluetooth_sink">Receptor de Audio</string>
|
||||
<string name="bluetooth_sink_detail">Permite que otros dispositivos se conecten y reproduzcan música a través de esta tableta.</string>
|
||||
<string name="bluetooth_status">Estado de Bluetooth</string>
|
||||
<string name="bluetooth_not_supported">Bluetooth no es compatible con este dispositivo.</string>
|
||||
<string name="bluetooth_disabled">Bluetooth está desactivado.</string>
|
||||
<string name="bluetooth_ready">Listo para conectar.</string>
|
||||
<string name="bluetooth_connected_to">Conectado a %s</string>
|
||||
<string name="bluetooth_discoverable">Visible como “%s”</string>
|
||||
<string name="open_bluetooth_settings">Abrir ajustes de Bluetooth</string>
|
||||
<string name="now_playing">Reproduciendo ahora</string>
|
||||
<string name="unknown_artist">Artista desconocido</string>
|
||||
<string name="unknown_track">Pista desconocida</string>
|
||||
|
||||
<!-- Weather Descriptions -->
|
||||
<string name="weather_clear">Despejado</string>
|
||||
<string name="weather_partly_cloudy">Parcialmente nublado</string>
|
||||
<string name="weather_overcast">Nublado</string>
|
||||
<string name="weather_fog">Niebla</string>
|
||||
<string name="weather_drizzle">Llovizna</string>
|
||||
<string name="weather_rain">Lluvia</string>
|
||||
<string name="weather_snow">Nieve</string>
|
||||
<string name="weather_rain_showers">Chubascos</string>
|
||||
<string name="weather_snow_showers">Nevadas</string>
|
||||
<string name="weather_thunderstorm">Tormenta</string>
|
||||
<string name="weather_unknown">Desconocido</string>
|
||||
<string name="weather_feels">Sensación %1$d°</string>
|
||||
<string name="weather_humidity">Humedad %1$d%%</string>
|
||||
<string name="weather_wind">Viento %1$d</string>
|
||||
<string name="temp_high">Máx %1$d°</string>
|
||||
<string name="temp_low">Mín %1$d°</string>
|
||||
<string name="fahrenheit">Fahrenheit</string>
|
||||
<string name="celsius">Celsius</string>
|
||||
|
||||
<!-- Service and Notifications -->
|
||||
<string name="notification_listening_title">Escuchando “Computer”</string>
|
||||
<string name="notification_listening_text">Toca para volver a Ambient Launcher</string>
|
||||
<string name="notification_paused_title">Escucha pausada</string>
|
||||
<string name="notification_detected_title">Palabra de activación detectada</string>
|
||||
<string name="notification_fallback_text">Toca para abrir tu asistente predeterminado</string>
|
||||
<string name="notification_keep_awake_title">Mantener pantalla encendida activo</string>
|
||||
<string name="notification_keep_awake_text">Este servicio evita que la pantalla se apague.</string>
|
||||
</resources>
|
||||
@@ -1,3 +1,191 @@
|
||||
<resources>
|
||||
<string name="app_name">Ambient Launcher</string>
|
||||
|
||||
<!-- General -->
|
||||
<string name="back">Back</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="retry">retry</string>
|
||||
|
||||
<!-- Home Screen -->
|
||||
<string name="open_apps">Open applications</string>
|
||||
<string name="open_settings">Open settings</string>
|
||||
<string name="weather_unavailable">Weather unavailable</string>
|
||||
<string name="next_hours">Next hours</string>
|
||||
<string name="rain_chance">%1$d%% rain</string>
|
||||
<string name="ambient_photograph">Ambient photograph</string>
|
||||
|
||||
<!-- App Tray -->
|
||||
<string name="all_applications">All applications</string>
|
||||
<string name="quick_launch">Quick launch</string>
|
||||
<string name="search_apps">Search apps</string>
|
||||
<string name="add_app">Add app</string>
|
||||
<string name="no_apps_pinned">No apps pinned yet.</string>
|
||||
<string name="no_matching_apps">No matching apps.</string>
|
||||
|
||||
<!-- Settings Sections -->
|
||||
<string name="display">Display</string>
|
||||
<string name="weather">Weather</string>
|
||||
<string name="routines">Night routine</string>
|
||||
<string name="voice">Voice assistant</string>
|
||||
<string name="apps">Applications</string>
|
||||
<string name="bluetooth">Bluetooth</string>
|
||||
<string name="access">System access</string>
|
||||
<string name="language">Language</string>
|
||||
|
||||
<!-- Display Settings -->
|
||||
<string name="keep_awake">Keep screen awake</string>
|
||||
<string name="keep_awake_detail">Maintains the screen awake state globally or based on visibility.</string>
|
||||
<string name="keep_awake_visible">While launcher is visible</string>
|
||||
<string name="keep_awake_charging">Only while charging (Global)</string>
|
||||
<string name="keep_awake_always">Always (Global)</string>
|
||||
<string name="keep_awake_default">Follow Android timeout</string>
|
||||
<string name="photo_interval">Photo interval</string>
|
||||
<string name="seconds">%1$d seconds</string>
|
||||
<string name="image_darkening">Image darkening</string>
|
||||
<string name="clock_corner">Clock and weather corner</string>
|
||||
<string name="clock_corner_detail">The panel shifts subtly to reduce burn-in.</string>
|
||||
<string name="photo_source">Photo source</string>
|
||||
<string name="photo_source_online">Curated online landscapes, cities, nature, and art.</string>
|
||||
<string name="photo_source_local">Using the selected local folder.</string>
|
||||
<string name="choose_local_folder">Choose local folder</string>
|
||||
<string name="use_online_collection">Use online collection</string>
|
||||
|
||||
<!-- Weather Settings -->
|
||||
<string name="show_weather">Show weather</string>
|
||||
<string name="show_weather_detail">Compact conditions and periodic full forecast slides.</string>
|
||||
<string name="location">Location</string>
|
||||
<string name="location_detail">Enter a city name or coordinates. Coordinates are auto-filled when searching by city.</string>
|
||||
<string name="location_label">City, State/Country</string>
|
||||
<string name="location_placeholder">e.g. London, UK</string>
|
||||
<string name="latitude">Latitude</string>
|
||||
<string name="longitude">Longitude</string>
|
||||
<string name="searching">Searching…</string>
|
||||
<string name="search_refresh">Search & Refresh</string>
|
||||
<string name="temp_unit">Temperature unit</string>
|
||||
<string name="forecast_frequency">Full forecast frequency</string>
|
||||
<string name="after_every_photos">After every %1$d photos</string>
|
||||
|
||||
<!-- Routine Settings -->
|
||||
<string name="ultra_dim">Scheduled ultra-dim</string>
|
||||
<string name="ultra_dim_detail">Applies whenever the launcher is visible. Android is not awakened by this schedule.</string>
|
||||
<string name="active_hours">Active hours</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="end">End</string>
|
||||
<string name="ultra_dim_strength">Ultra-dim strength</string>
|
||||
<string name="night_mode_note">Night mode also hides weather, disables image motion, and uses a dark-red clock.</string>
|
||||
|
||||
<!-- Voice Settings -->
|
||||
<string name="voice_listen">Listen for “Computer”</string>
|
||||
<string name="voice_listen_detail">Detection stays on-device. Continued listening in other apps requires a persistent notification and Android’s microphone privacy indicator.</string>
|
||||
<string name="status_disabled">Disabled</string>
|
||||
<string name="status_downloading">Downloading model</string>
|
||||
<string name="status_installing">Installing model</string>
|
||||
<string name="status_ready">Ready</string>
|
||||
<string name="status_listening">Listening</string>
|
||||
<string name="status_paused">Paused</string>
|
||||
<string name="status_busy">Microphone busy; retrying</string>
|
||||
<string name="status_error">Error</string>
|
||||
<string name="sensitivity">Sensitivity</string>
|
||||
<string name="sensitivity_detail">Higher values detect more easily but may false-trigger.</string>
|
||||
<string name="auto_close">Auto-close assistant</string>
|
||||
<string name="auto_close_detail">Automatically return to launcher after %1$d seconds.</string>
|
||||
<string name="auto_close_manual">Assistant will stay open until manually dismissed.</string>
|
||||
<string name="toggle_wake">Toggle with wake word</string>
|
||||
<string name="toggle_wake_detail">If the assistant is already open, saying “Computer” again will close it and return here.</string>
|
||||
<string name="toggle_cooldown_note">Listening cooldown is reduced to 2.5s for faster toggling.</string>
|
||||
<string name="listening_controls">Listening controls</string>
|
||||
<string name="listening_controls_detail">Testing releases this launcher’s microphone before opening the selected Android assistant.</string>
|
||||
<string name="resume">Resume</string>
|
||||
<string name="pause">Pause</string>
|
||||
<string name="test_assistant">Test assistant</string>
|
||||
<string name="open_voice_settings">Open Android voice settings</string>
|
||||
<string name="privacy">Privacy</string>
|
||||
<string name="privacy_detail">Audio frames are processed locally and are never saved or transmitted.</string>
|
||||
<string name="privacy_note">Only the last detection time and service status are retained for diagnostics.</string>
|
||||
|
||||
<!-- Applications Settings -->
|
||||
<string name="apps_pinned">%1$d app(s) pinned</string>
|
||||
<string name="apps_pinned_detail">Open the app tray from Home, then choose Add app to pin or remove applications.</string>
|
||||
<string name="apps_privacy_detail">Recent-app tracking and usage access are disabled.</string>
|
||||
<string name="apps_privacy_note">This launcher only reads activities that advertise a launchable icon.</string>
|
||||
|
||||
<!-- Access Settings -->
|
||||
<string name="default_home">Default Home app</string>
|
||||
<string name="default_home_detail">Required for the hardware Home gesture to return here.</string>
|
||||
<string name="make_default_home">Make default Home app</string>
|
||||
<string name="permission_internet">Internet</string>
|
||||
<string name="permission_internet_detail">Used for weather and the built-in online photo collection.</string>
|
||||
<string name="permission_local_photo">Local photo folder</string>
|
||||
<string name="permission_local_photo_detail">Granted only to the folder you explicitly select.</string>
|
||||
<string name="permission_location">Location</string>
|
||||
<string name="permission_location_detail">Not requested. Weather uses coordinates entered in settings.</string>
|
||||
<string name="permission_microphone">Microphone</string>
|
||||
<string name="permission_microphone_detail">Requested only when on-device wake-word listening is enabled.</string>
|
||||
<string name="permission_overlay">Display over other apps</string>
|
||||
<string name="permission_overlay_detail">Not requested. Ultra-dim only covers this launcher.</string>
|
||||
<string name="permission_usage">Usage access</string>
|
||||
<string name="permission_usage_detail">Not requested. Recent-app history is not collected.</string>
|
||||
<string name="permission_notifications">Notifications</string>
|
||||
<string name="permission_notifications_detail">Used for the visible listening service and assistant fallback action.</string>
|
||||
<string name="audio_output">Audio output</string>
|
||||
<string name="audio_output_detail">A normal launcher cannot force other apps to use a specific device.</string>
|
||||
<string name="open_sound_settings">Open Android sound settings</string>
|
||||
<string name="available">Available</string>
|
||||
<string name="not_requested">Not requested</string>
|
||||
|
||||
<!-- Language Settings -->
|
||||
<string name="language_settings_detail">App language and system language controls.</string>
|
||||
<string name="app_language">App Language</string>
|
||||
<string name="lang_english">English</string>
|
||||
<string name="lang_spanish">Spanish</string>
|
||||
<string name="lang_english_spanish">English-Spanish</string>
|
||||
<string name="lang_spanish_english">Spanish-English</string>
|
||||
<string name="system_language">System Language</string>
|
||||
<string name="system_language_detail">Change Android system language settings.</string>
|
||||
<string name="open_system_language">Open system language settings</string>
|
||||
<string name="system_language_warning">System language is not up to date</string>
|
||||
|
||||
<!-- Bluetooth Settings -->
|
||||
<string name="bluetooth_sink">Audio Receiver (Sink)</string>
|
||||
<string name="bluetooth_sink_detail">Allow other devices to connect and play music through this tablet.</string>
|
||||
<string name="bluetooth_status">Bluetooth Status</string>
|
||||
<string name="bluetooth_not_supported">Bluetooth is not supported on this device.</string>
|
||||
<string name="bluetooth_disabled">Bluetooth is turned off.</string>
|
||||
<string name="bluetooth_ready">Ready to connect.</string>
|
||||
<string name="bluetooth_connected_to">Connected to %s</string>
|
||||
<string name="bluetooth_discoverable">Discoverable as “%s”</string>
|
||||
<string name="open_bluetooth_settings">Open Bluetooth settings</string>
|
||||
<string name="now_playing">Now Playing</string>
|
||||
<string name="unknown_artist">Unknown Artist</string>
|
||||
<string name="unknown_track">Unknown Track</string>
|
||||
|
||||
<!-- Weather Descriptions -->
|
||||
<string name="weather_clear">Clear</string>
|
||||
<string name="weather_partly_cloudy">Partly cloudy</string>
|
||||
<string name="weather_overcast">Overcast</string>
|
||||
<string name="weather_fog">Fog</string>
|
||||
<string name="weather_drizzle">Drizzle</string>
|
||||
<string name="weather_rain">Rain</string>
|
||||
<string name="weather_snow">Snow</string>
|
||||
<string name="weather_rain_showers">Rain showers</string>
|
||||
<string name="weather_snow_showers">Snow showers</string>
|
||||
<string name="weather_thunderstorm">Thunderstorm</string>
|
||||
<string name="weather_unknown">Unknown</string>
|
||||
<string name="weather_feels">Feels %1$d°</string>
|
||||
<string name="weather_humidity">Humidity %1$d%%</string>
|
||||
<string name="weather_wind">Wind %1$d</string>
|
||||
<string name="temp_high">H %1$d°</string>
|
||||
<string name="temp_low">L %1$d°</string>
|
||||
<string name="fahrenheit">Fahrenheit</string>
|
||||
<string name="celsius">Celsius</string>
|
||||
|
||||
<!-- Service and Notifications -->
|
||||
<string name="notification_listening_title">Listening for “Computer”</string>
|
||||
<string name="notification_listening_text">Tap to return to Ambient Launcher</string>
|
||||
<string name="notification_paused_title">Wake word paused</string>
|
||||
<string name="notification_detected_title">Wake word detected</string>
|
||||
<string name="notification_fallback_text">Tap to open your default assistant</string>
|
||||
<string name="notification_keep_awake_title">Keep screen awake active</string>
|
||||
<string name="notification_keep_awake_text">This service keeps your screen from timing out.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user