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:
@@ -7,3 +7,4 @@ captures/
|
|||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
.cxx/
|
.cxx/
|
||||||
app/release/
|
app/release/
|
||||||
|
.artifacts
|
||||||
@@ -5,14 +5,23 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<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" />
|
||||||
|
<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.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>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<package android:name="com.amazon.dee.app" />
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -45,5 +54,15 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="microphone"
|
android:foregroundServiceType="microphone"
|
||||||
android:stopWithTask="false" />
|
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>
|
</application>
|
||||||
</manifest>
|
</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.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.Geocoder
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -17,26 +18,22 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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) {
|
class LauncherViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val store = SettingsStore(application)
|
private val store = SettingsStore(application)
|
||||||
private val weatherRepository = WeatherRepository()
|
private val weatherRepository = WeatherRepository()
|
||||||
private val wakeWordController = WakeWordController(application)
|
private val wakeWordController = WakeWordController(application)
|
||||||
|
// private val bluetoothController = BluetoothController(application)
|
||||||
private val _uiState = MutableStateFlow(LauncherUiState())
|
private val _uiState = MutableStateFlow(LauncherUiState())
|
||||||
val uiState: StateFlow<LauncherUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<LauncherUiState> = _uiState.asStateFlow()
|
||||||
private var settingsLoaded = false
|
private var settingsLoaded = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadApps()
|
loadApps()
|
||||||
|
/* viewModelScope.launch {
|
||||||
|
bluetoothController.status.collectLatest { status ->
|
||||||
|
_uiState.value = _uiState.value.copy(bluetooth = status)
|
||||||
|
}
|
||||||
|
} */
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
wakeWordController.status.collectLatest { status ->
|
wakeWordController.status.collectLatest { status ->
|
||||||
_uiState.value = _uiState.value.copy(wakeWord = 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)
|
_uiState.value = _uiState.value.copy(settings = settings)
|
||||||
wakeWordController.configure(
|
wakeWordController.configure(
|
||||||
settings.wakeWord.enabled,
|
settings.wakeWord.enabled,
|
||||||
settings.wakeWord.sensitivity
|
settings.wakeWord.sensitivity,
|
||||||
|
settings.wakeWord.autoCloseSeconds,
|
||||||
|
settings.wakeWord.toggleClose,
|
||||||
|
settings.language
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
_uiState.value.wakeWord.modelState == ModelInstallState.READY &&
|
_uiState.value.wakeWord.modelState == ModelInstallState.READY &&
|
||||||
@@ -111,17 +111,36 @@ class LauncherViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
fun refreshWeather() {
|
fun refreshWeather() {
|
||||||
val settings = _uiState.value.settings
|
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 {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(weatherLoading = true, weatherError = null)
|
_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) }
|
runCatching { weatherRepository.load(lat, lon, settings.useFahrenheit) }
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
_uiState.value = _uiState.value.copy(
|
_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 pauseWakeWord() = wakeWordController.pause()
|
||||||
fun resumeWakeWord() = wakeWordController.resume()
|
fun resumeWakeWord() = wakeWordController.resume()
|
||||||
fun testWakeWord() = wakeWordController.testDetection()
|
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 {
|
fun launch(app: LauncherApp): Boolean {
|
||||||
val intent = Intent(Intent.ACTION_MAIN)
|
val intent = Intent(Intent.ACTION_MAIN)
|
||||||
.addCategory(Intent.CATEGORY_LAUNCHER)
|
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
@@ -168,6 +208,11 @@ class LauncherViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}.isSuccess
|
}.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
// bluetoothController.release()
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadApps() {
|
private fun loadApps() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = withContext(Dispatchers.IO) {
|
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.content.ComponentName
|
||||||
import android.graphics.drawable.Drawable
|
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(
|
data class LauncherApp(
|
||||||
val label: String,
|
val label: String,
|
||||||
val component: ComponentName,
|
val component: ComponentName,
|
||||||
@@ -34,15 +54,18 @@ data class DailyWeather(
|
|||||||
val weatherCode: Int
|
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 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 ModelInstallState { NOT_INSTALLED, DOWNLOADING, INSTALLING, READY, FAILED }
|
||||||
enum class WakeWordState { DISABLED, DOWNLOADING, INSTALLING, READY, LISTENING, PAUSED, MICROPHONE_BUSY, ERROR }
|
enum class WakeWordState { DISABLED, DOWNLOADING, INSTALLING, READY, LISTENING, PAUSED, MICROPHONE_BUSY, ERROR }
|
||||||
|
|
||||||
data class WakeWordSettings(
|
data class WakeWordSettings(
|
||||||
val enabled: Boolean = false,
|
val enabled: Boolean = false,
|
||||||
val sensitivity: Int = 50,
|
val sensitivity: Int = 50,
|
||||||
val installedModelVersion: String = ""
|
val installedModelVersion: String = "",
|
||||||
|
val autoCloseSeconds: Int = 60,
|
||||||
|
val toggleClose: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
data class WakeWordSnapshot(
|
data class WakeWordSnapshot(
|
||||||
@@ -70,6 +93,7 @@ data class LauncherSettings(
|
|||||||
val nightStartHour: Int = 23,
|
val nightStartHour: Int = 23,
|
||||||
val nightEndHour: Int = 7,
|
val nightEndHour: Int = 7,
|
||||||
val nightDimPercent: Int = 88,
|
val nightDimPercent: Int = 88,
|
||||||
|
val language: AppLanguage = AppLanguage.ENGLISH,
|
||||||
val wakeWord: WakeWordSettings = WakeWordSettings()
|
val wakeWord: WakeWordSettings = WakeWordSettings()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,18 +102,18 @@ sealed interface Slide {
|
|||||||
data object Weather : Slide
|
data object Weather : Slide
|
||||||
}
|
}
|
||||||
|
|
||||||
fun weatherDescription(code: Int): String = when (code) {
|
fun weatherDescription(code: Int): Int = when (code) {
|
||||||
0 -> "Clear"
|
0 -> R.string.weather_clear
|
||||||
1, 2 -> "Partly cloudy"
|
1, 2 -> R.string.weather_partly_cloudy
|
||||||
3 -> "Overcast"
|
3 -> R.string.weather_overcast
|
||||||
45, 48 -> "Fog"
|
45, 48 -> R.string.weather_fog
|
||||||
51, 53, 55, 56, 57 -> "Drizzle"
|
51, 53, 55, 56, 57 -> R.string.weather_drizzle
|
||||||
61, 63, 65, 66, 67 -> "Rain"
|
61, 63, 65, 66, 67 -> R.string.weather_rain
|
||||||
71, 73, 75, 77 -> "Snow"
|
71, 73, 75, 77 -> R.string.weather_snow
|
||||||
80, 81, 82 -> "Rain showers"
|
80, 81, 82 -> R.string.weather_rain_showers
|
||||||
85, 86 -> "Snow showers"
|
85, 86 -> R.string.weather_snow_showers
|
||||||
95, 96, 99 -> "Thunderstorm"
|
95, 96, 99 -> R.string.weather_thunderstorm
|
||||||
else -> "Unknown"
|
else -> R.string.weather_unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
fun weatherGlyph(code: Int): String = when (code) {
|
fun weatherGlyph(code: Int): String = when (code) {
|
||||||
|
|||||||
@@ -29,9 +29,12 @@ class SettingsStore(private val context: Context) {
|
|||||||
val nightStart = intPreferencesKey("night_start")
|
val nightStart = intPreferencesKey("night_start")
|
||||||
val nightEnd = intPreferencesKey("night_end")
|
val nightEnd = intPreferencesKey("night_end")
|
||||||
val nightDim = intPreferencesKey("night_dim")
|
val nightDim = intPreferencesKey("night_dim")
|
||||||
|
val language = stringPreferencesKey("language")
|
||||||
val wakeEnabled = booleanPreferencesKey("wake_enabled")
|
val wakeEnabled = booleanPreferencesKey("wake_enabled")
|
||||||
val wakeSensitivity = intPreferencesKey("wake_sensitivity")
|
val wakeSensitivity = intPreferencesKey("wake_sensitivity")
|
||||||
val wakeModelVersion = stringPreferencesKey("wake_model_version")
|
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 ->
|
val settings: Flow<LauncherSettings> = context.dataStore.data.map { p ->
|
||||||
@@ -56,10 +59,13 @@ class SettingsStore(private val context: Context) {
|
|||||||
nightStartHour = p[Keys.nightStart] ?: 23,
|
nightStartHour = p[Keys.nightStart] ?: 23,
|
||||||
nightEndHour = p[Keys.nightEnd] ?: 7,
|
nightEndHour = p[Keys.nightEnd] ?: 7,
|
||||||
nightDimPercent = p[Keys.nightDim] ?: 88,
|
nightDimPercent = p[Keys.nightDim] ?: 88,
|
||||||
|
language = enumOrDefault(p[Keys.language], AppLanguage.ENGLISH),
|
||||||
wakeWord = WakeWordSettings(
|
wakeWord = WakeWordSettings(
|
||||||
enabled = p[Keys.wakeEnabled] ?: false,
|
enabled = p[Keys.wakeEnabled] ?: false,
|
||||||
sensitivity = p[Keys.wakeSensitivity] ?: 50,
|
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.nightStart] = value.nightStartHour
|
||||||
p[Keys.nightEnd] = value.nightEndHour
|
p[Keys.nightEnd] = value.nightEndHour
|
||||||
p[Keys.nightDim] = value.nightDimPercent
|
p[Keys.nightDim] = value.nightDimPercent
|
||||||
|
p[Keys.language] = value.language.name
|
||||||
p[Keys.wakeEnabled] = value.wakeWord.enabled
|
p[Keys.wakeEnabled] = value.wakeWord.enabled
|
||||||
p[Keys.wakeSensitivity] = value.wakeWord.sensitivity
|
p[Keys.wakeSensitivity] = value.wakeWord.sensitivity
|
||||||
p[Keys.wakeModelVersion] = value.wakeWord.installedModelVersion
|
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.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.provider.Settings
|
||||||
|
|
||||||
enum class AssistLaunchResult { LAUNCHED, NOT_CONFIGURED, BACKGROUND_BLOCKED }
|
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
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun assistantIntent() = Intent(Intent.ACTION_ASSIST)
|
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)
|
.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.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import com.ambient.launcher.AppLanguage
|
||||||
import com.ambient.launcher.ModelInstallState
|
import com.ambient.launcher.ModelInstallState
|
||||||
import com.ambient.launcher.WakeWordSnapshot
|
import com.ambient.launcher.WakeWordSnapshot
|
||||||
import com.ambient.launcher.WakeWordState
|
import com.ambient.launcher.WakeWordState
|
||||||
@@ -46,6 +47,9 @@ class WakeWordController(private val context: Context) {
|
|||||||
private var enabled = false
|
private var enabled = false
|
||||||
private var visible = false
|
private var visible = false
|
||||||
private var sensitivity = 50
|
private var sensitivity = 50
|
||||||
|
private var autoCloseSeconds = 0
|
||||||
|
private var toggleClose = false
|
||||||
|
private var language = AppLanguage.ENGLISH
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val lastDetection = context.getSharedPreferences("wake_runtime", Context.MODE_PRIVATE)
|
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
|
enabled = isEnabled
|
||||||
sensitivity = newSensitivity.coerceIn(0, 100)
|
sensitivity = newSensitivity.coerceIn(0, 100)
|
||||||
|
autoCloseSeconds = newAutoClose
|
||||||
|
toggleClose = newToggleClose
|
||||||
|
language = newLanguage
|
||||||
if (!enabled) disable()
|
if (!enabled) disable()
|
||||||
else if (visible) enable()
|
else if (visible) enable()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setVisible(isVisible: Boolean) {
|
fun setVisible(isVisible: Boolean) {
|
||||||
visible = isVisible
|
visible = isVisible
|
||||||
if (visible && enabled) enable()
|
if (visible) {
|
||||||
|
if (enabled) enable()
|
||||||
|
sendAction(WakeWordService.ACTION_REPORT_VISIBLE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enable() {
|
fun enable() {
|
||||||
@@ -139,6 +155,9 @@ class WakeWordController(private val context: Context) {
|
|||||||
val intent = Intent(context, WakeWordService::class.java)
|
val intent = Intent(context, WakeWordService::class.java)
|
||||||
.setAction(WakeWordService.ACTION_START)
|
.setAction(WakeWordService.ACTION_START)
|
||||||
.putExtra(WakeWordService.EXTRA_SENSITIVITY, sensitivity)
|
.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) }
|
runCatching { ContextCompat.startForegroundService(context, intent) }
|
||||||
.onFailure {
|
.onFailure {
|
||||||
WakeWordRuntime.update(
|
WakeWordRuntime.update(
|
||||||
|
|||||||
@@ -44,6 +44,38 @@ object WakeWordModel {
|
|||||||
fun path(context: Context, name: String) = File(directory(context), name).absolutePath
|
fun path(context: Context, name: String) = File(directory(context), name).absolutePath
|
||||||
|
|
||||||
private fun File.readTextOrNull(): String? = runCatching { readText() }.getOrNull()
|
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(
|
class WakeWordModelWorker(
|
||||||
@@ -72,7 +104,10 @@ class WakeWordModelWorker(
|
|||||||
staging.deleteRecursively()
|
staging.deleteRecursively()
|
||||||
staging.mkdirs()
|
staging.mkdirs()
|
||||||
extractSelected(archive, staging)
|
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 }) {
|
if (!WakeWordModel.requiredFiles.all { File(staging, it).isFile }) {
|
||||||
error("Model archive is incomplete")
|
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 {
|
private fun sha256(file: File): String {
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
file.inputStream().buffered().use { input ->
|
file.inputStream().buffered().use { input ->
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.os.Looper
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.ambient.launcher.AppLanguage
|
||||||
import com.ambient.launcher.MainActivity
|
import com.ambient.launcher.MainActivity
|
||||||
import com.ambient.launcher.R
|
import com.ambient.launcher.R
|
||||||
import com.ambient.launcher.SettingsStore
|
import com.ambient.launcher.SettingsStore
|
||||||
@@ -46,7 +47,12 @@ class WakeWordService : Service() {
|
|||||||
@Volatile private var listening = false
|
@Volatile private var listening = false
|
||||||
@Volatile private var paused = false
|
@Volatile private var paused = false
|
||||||
private var sensitivity = 50
|
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 var lastDetection = 0L
|
||||||
|
private val autoCloseRunnable = Runnable { returnToLauncher() }
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -61,6 +67,10 @@ class WakeWordService : Service() {
|
|||||||
intent.getIntExtra(EXTRA_SENSITIVITY, 50).coerceIn(0, 100)
|
intent.getIntExtra(EXTRA_SENSITIVITY, 50).coerceIn(0, 100)
|
||||||
if (requestedSensitivity != sensitivity && listening) stopListening()
|
if (requestedSensitivity != sensitivity && listening) stopListening()
|
||||||
sensitivity = requestedSensitivity
|
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
|
paused = false
|
||||||
startListening()
|
startListening()
|
||||||
}
|
}
|
||||||
@@ -77,6 +87,10 @@ class WakeWordService : Service() {
|
|||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ACTION_REPORT_VISIBLE -> {
|
||||||
|
isAssistantActive = false
|
||||||
|
mainHandler.removeCallbacks(autoCloseRunnable)
|
||||||
|
}
|
||||||
ACTION_TEST -> handleDetection(force = true)
|
ACTION_TEST -> handleDetection(force = true)
|
||||||
else -> {
|
else -> {
|
||||||
WakeWordRuntime.update(
|
WakeWordRuntime.update(
|
||||||
@@ -214,6 +228,16 @@ class WakeWordService : Service() {
|
|||||||
private fun handleDetection(force: Boolean) {
|
private fun handleDetection(force: Boolean) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (!force && now - lastDetection < DETECTION_DEBOUNCE_MS) return
|
if (!force && now - lastDetection < DETECTION_DEBOUNCE_MS) return
|
||||||
|
|
||||||
|
if (!force && toggleClose && isAssistantActive) {
|
||||||
|
lastDetection = now
|
||||||
|
returnToLauncher()
|
||||||
|
if (!paused) {
|
||||||
|
mainHandler.postDelayed({ startListening() }, toggleCooldown())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
lastDetection = now
|
lastDetection = now
|
||||||
getSharedPreferences("wake_runtime", MODE_PRIVATE)
|
getSharedPreferences("wake_runtime", MODE_PRIVATE)
|
||||||
.edit()
|
.edit()
|
||||||
@@ -227,9 +251,28 @@ class WakeWordService : Service() {
|
|||||||
)
|
)
|
||||||
val launcher = DefaultAssistantLauncher(this)
|
val launcher = DefaultAssistantLauncher(this)
|
||||||
val result = launcher.launch()
|
val result = launcher.launch()
|
||||||
if (result != AssistLaunchResult.LAUNCHED) showAssistantFallback(launcher)
|
if (result == AssistLaunchResult.LAUNCHED) {
|
||||||
if (!paused) mainHandler.postDelayed({ startListening() }, ASSISTANT_COOLDOWN_MS)
|
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() {
|
private fun pauseListening() {
|
||||||
paused = true
|
paused = true
|
||||||
@@ -340,8 +383,12 @@ class WakeWordService : Service() {
|
|||||||
const val ACTION_PAUSE = "com.ambient.launcher.voice.PAUSE"
|
const val ACTION_PAUSE = "com.ambient.launcher.voice.PAUSE"
|
||||||
const val ACTION_RESUME = "com.ambient.launcher.voice.RESUME"
|
const val ACTION_RESUME = "com.ambient.launcher.voice.RESUME"
|
||||||
const val ACTION_STOP = "com.ambient.launcher.voice.STOP"
|
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 ACTION_TEST = "com.ambient.launcher.voice.TEST"
|
||||||
const val EXTRA_SENSITIVITY = "sensitivity"
|
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 SAMPLE_RATE = 16_000
|
||||||
private const val DETECTION_DEBOUNCE_MS = 5_000L
|
private const val DETECTION_DEBOUNCE_MS = 5_000L
|
||||||
private const val ASSISTANT_COOLDOWN_MS = 12_000L
|
private const val ASSISTANT_COOLDOWN_MS = 12_000L
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:shape="rectangle">
|
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
|
<gradient
|
||||||
android:angle="315"
|
android:startX="0"
|
||||||
android:endColor="#16243A"
|
android:startY="0"
|
||||||
android:startColor="#594755"
|
android:endX="24"
|
||||||
android:type="linear" />
|
android:endY="24"
|
||||||
</shape>
|
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>
|
<resources>
|
||||||
<string name="app_name">Ambient Launcher</string>
|
<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>
|
</resources>
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
Vendored
+93
@@ -0,0 +1,93 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user