Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b24a02ac2 | ||
|
|
fb67f5e02f | ||
|
|
4a3da0963b |
@@ -65,10 +65,8 @@
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- MAIN/LAUNCHER lives on the two aliases below, so the launcher
|
||||
name can be switched at runtime (#104). -->
|
||||
<!-- Shared text becomes a new task's title. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
@@ -89,6 +87,32 @@
|
||||
<data android:mimeType="text/calendar" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity-alias
|
||||
android:name=".DefaultNameAlias"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias
|
||||
android:name=".TasksNameAlias"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name_tasks_alias"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- Quick Settings "New task" tile. Only the system QS host can bind it. -->
|
||||
<service
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.jeanlucmakiola.agendula.data.appname
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import de.jeanlucmakiola.agendula.ui.navigation.AppShortcuts
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** The label the launcher shows for the app (#104). */
|
||||
enum class LauncherName { AGENDULA, TASKS }
|
||||
|
||||
/** The two launcher aliases declared in the manifest. */
|
||||
enum class LauncherAlias { DEFAULT, TASKS }
|
||||
|
||||
/** One component-enable change in an [aliasWritePlan]. */
|
||||
data class AliasStateChange(val alias: LauncherAlias, val enabled: Boolean)
|
||||
|
||||
/**
|
||||
* Reads the `TasksNameAlias` enabled state as a [LauncherName]. Only an explicit
|
||||
* enable means "Tasks"; never toggled and disabled both mean the manifest default.
|
||||
*/
|
||||
fun launcherNameFor(tasksAliasState: Int): LauncherName =
|
||||
if (tasksAliasState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
|
||||
LauncherName.TASKS
|
||||
} else {
|
||||
LauncherName.AGENDULA
|
||||
}
|
||||
|
||||
/**
|
||||
* The steps to switch to [target]: enable the target alias first, then disable
|
||||
* the other, so the launcher can briefly see two entries but never none.
|
||||
*/
|
||||
fun aliasWritePlan(target: LauncherName): List<AliasStateChange> = when (target) {
|
||||
LauncherName.TASKS -> listOf(
|
||||
AliasStateChange(LauncherAlias.TASKS, enabled = true),
|
||||
AliasStateChange(LauncherAlias.DEFAULT, enabled = false),
|
||||
)
|
||||
LauncherName.AGENDULA -> listOf(
|
||||
AliasStateChange(LauncherAlias.DEFAULT, enabled = true),
|
||||
AliasStateChange(LauncherAlias.TASKS, enabled = false),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the launcher label by enabling one of the two `<activity-alias>`
|
||||
* components. The component state is the only source of truth; nothing is
|
||||
* stored in prefs.
|
||||
*/
|
||||
@Singleton
|
||||
class LauncherNameManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val packageManager: PackageManager get() = context.packageManager
|
||||
|
||||
// The package is the (suffixed) applicationId, the class is namespace-qualified.
|
||||
private fun component(alias: LauncherAlias): ComponentName {
|
||||
val simpleName = when (alias) {
|
||||
LauncherAlias.DEFAULT -> "DefaultNameAlias"
|
||||
LauncherAlias.TASKS -> "TasksNameAlias"
|
||||
}
|
||||
return ComponentName(context.packageName, "$NAMESPACE.$simpleName")
|
||||
}
|
||||
|
||||
fun current(): LauncherName =
|
||||
launcherNameFor(packageManager.getComponentEnabledSetting(component(LauncherAlias.TASKS)))
|
||||
|
||||
fun set(name: LauncherName) {
|
||||
for (change in aliasWritePlan(name)) {
|
||||
val state = if (change.enabled) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
} else {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
}
|
||||
packageManager.setComponentEnabledSetting(component(change.alias), state, PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
// Dynamic shortcuts hang off the launcher activity, which just changed.
|
||||
AppShortcuts.publish(context)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** `namespace` in app/build.gradle.kts, not the suffixed applicationId. */
|
||||
const val NAMESPACE = "de.jeanlucmakiola.agendula"
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,16 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.jeanlucmakiola.agendula.R
|
||||
import de.jeanlucmakiola.agendula.data.appname.LauncherName
|
||||
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
|
||||
import de.jeanlucmakiola.agendula.data.prefs.TimeFormatPref
|
||||
import de.jeanlucmakiola.agendula.data.prefs.is24Hour
|
||||
import de.jeanlucmakiola.agendula.ui.common.formatMinuteOfDay
|
||||
import de.jeanlucmakiola.agendula.ui.common.localeFirstDayOfWeek
|
||||
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
||||
import de.jeanlucmakiola.floret.components.GroupedHint
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.OptionPicker
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
@@ -38,6 +41,8 @@ internal fun AppearanceScreen(
|
||||
) {
|
||||
val dynamicColorAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
var showTheme by remember { mutableStateOf(false) }
|
||||
var showAppName by remember { mutableStateOf(false) }
|
||||
val launcherName by viewModel.launcherName.collectAsStateWithLifecycle()
|
||||
var showTimeFormat by remember { mutableStateOf(false) }
|
||||
var showWeekStart by remember { mutableStateOf(false) }
|
||||
val systemIs24Hour = DateFormat.is24HourFormat(LocalContext.current)
|
||||
@@ -90,6 +95,26 @@ internal fun AppearanceScreen(
|
||||
position = Position.Bottom,
|
||||
onClick = { showWeekStart = true },
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_app_name),
|
||||
summary = launcherNameLabel(launcherName),
|
||||
position = Position.Alone,
|
||||
onClick = { showAppName = true },
|
||||
)
|
||||
}
|
||||
|
||||
if (showAppName) {
|
||||
OptionPicker(
|
||||
title = stringResource(R.string.settings_app_name),
|
||||
options = LauncherName.entries,
|
||||
selected = launcherName,
|
||||
label = { launcherNameLabel(it) },
|
||||
header = { GroupedHint(stringResource(R.string.settings_app_name_hint)) },
|
||||
onSelect = viewModel::setLauncherName,
|
||||
onDismiss = { showAppName = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showTimeFormat) {
|
||||
@@ -164,6 +189,14 @@ private fun weekStartLabel(day: DayOfWeek?): String =
|
||||
/** 14:00, the sample time "Automatic" is shown with. */
|
||||
private const val SAMPLE_MINUTE_OF_DAY = 14 * 60
|
||||
|
||||
@Composable
|
||||
private fun launcherNameLabel(name: LauncherName): String = stringResource(
|
||||
when (name) {
|
||||
LauncherName.AGENDULA -> R.string.app_name
|
||||
LauncherName.TASKS -> R.string.app_name_tasks_alias
|
||||
},
|
||||
)
|
||||
|
||||
private fun themeLabel(mode: ThemeMode): Int = when (mode) {
|
||||
ThemeMode.SYSTEM -> R.string.settings_theme_system
|
||||
ThemeMode.LIGHT -> R.string.settings_theme_light
|
||||
|
||||
@@ -3,6 +3,8 @@ package de.jeanlucmakiola.agendula.ui.settings
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.jeanlucmakiola.agendula.data.appname.LauncherName
|
||||
import de.jeanlucmakiola.agendula.data.appname.LauncherNameManager
|
||||
import de.jeanlucmakiola.agendula.data.di.IoDispatcher
|
||||
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
||||
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
||||
@@ -33,6 +35,7 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.DayOfWeek
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -102,7 +105,8 @@ class SettingsViewModel @Inject constructor(
|
||||
private val oneShotImport: OneShotImport,
|
||||
private val reminderScheduler: ReminderScheduler,
|
||||
private val taskNotifier: TaskNotifier,
|
||||
@IoDispatcher io: CoroutineDispatcher,
|
||||
private val launcherNameManager: LauncherNameManager,
|
||||
@IoDispatcher private val io: CoroutineDispatcher,
|
||||
repository: TasksRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -217,6 +221,23 @@ class SettingsViewModel @Inject constructor(
|
||||
|
||||
val legacyImportRetrying: StateFlow<Boolean> = retryingLegacyImport
|
||||
|
||||
private val _launcherName = MutableStateFlow(LauncherName.AGENDULA)
|
||||
val launcherName: StateFlow<LauncherName> = _launcherName
|
||||
|
||||
init {
|
||||
viewModelScope.launch { _launcherName.value = withContext(io) { launcherNameManager.current() } }
|
||||
}
|
||||
|
||||
fun setLauncherName(name: LauncherName) {
|
||||
_launcherName.value = name
|
||||
viewModelScope.launch {
|
||||
_launcherName.value = withContext(io) {
|
||||
launcherNameManager.set(name)
|
||||
launcherNameManager.current()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setThemeMode(mode: ThemeMode) = viewModelScope.launch { prefs.setThemeMode(mode) }
|
||||
fun setDynamicColor(enabled: Boolean) = viewModelScope.launch { prefs.setDynamicColor(enabled) }
|
||||
fun setDefaultList(id: Long?) = viewModelScope.launch { prefs.setDefaultListId(id) }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<resources>
|
||||
<string name="app_name">Agendula</string>
|
||||
<string name="app_name_tasks_alias">Tasks</string>
|
||||
|
||||
<!-- Generic -->
|
||||
<string name="task_untitled">Untitled task</string>
|
||||
@@ -450,6 +451,8 @@
|
||||
<string name="settings_theme_dark">Dark</string>
|
||||
<string name="settings_theme_black">Black</string>
|
||||
<string name="settings_theme_black_summary">Dark with pure black backgrounds, for AMOLED screens</string>
|
||||
<string name="settings_app_name">App name</string>
|
||||
<string name="settings_app_name_hint">Show Agendula as “Tasks” in your launcher. Only the launcher name changes; the icon may move to a new spot. Agendula closes when you switch, so reopen it from the launcher.</string>
|
||||
<string name="settings_dynamic_color">Dynamic colour</string>
|
||||
<string name="settings_dynamic_color_hint">Use colours from your wallpaper</string>
|
||||
<string name="settings_section_reminders">Notifications</string>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.jeanlucmakiola.agendula.data.appname
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class LauncherNameManagerTest {
|
||||
|
||||
@Test
|
||||
fun `an enabled Tasks alias reads as Tasks`() {
|
||||
assertThat(launcherNameFor(PackageManager.COMPONENT_ENABLED_STATE_ENABLED)).isEqualTo(LauncherName.TASKS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a default or disabled Tasks alias reads as Agendula`() {
|
||||
assertThat(launcherNameFor(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)).isEqualTo(LauncherName.AGENDULA)
|
||||
assertThat(launcherNameFor(PackageManager.COMPONENT_ENABLED_STATE_DISABLED)).isEqualTo(LauncherName.AGENDULA)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `switching always enables the target before disabling the other`() {
|
||||
assertThat(aliasWritePlan(LauncherName.TASKS)).containsExactly(
|
||||
AliasStateChange(LauncherAlias.TASKS, enabled = true),
|
||||
AliasStateChange(LauncherAlias.DEFAULT, enabled = false),
|
||||
).inOrder()
|
||||
assertThat(aliasWritePlan(LauncherName.AGENDULA)).containsExactly(
|
||||
AliasStateChange(LauncherAlias.DEFAULT, enabled = true),
|
||||
AliasStateChange(LauncherAlias.TASKS, enabled = false),
|
||||
).inOrder()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user