diff --git a/app/src/main/java/com/mccal/folio/CustomizationSheet.kt b/app/src/main/java/com/mccal/folio/CustomizationSheet.kt index 97d4a0a..8a779af 100644 --- a/app/src/main/java/com/mccal/folio/CustomizationSheet.kt +++ b/app/src/main/java/com/mccal/folio/CustomizationSheet.kt @@ -407,6 +407,8 @@ internal fun CustomizationSheet(state: LauncherState, initiallyWide: Boolean, mo state.iconPack, { IconPacks.clear(); model.setIconPack(it) }, tag = "icon-pack") CardNote(stringResource(R.string.icon_packs_and_themes_are_made_by_indepe)) if (packs.isEmpty()) CardNote(stringResource(R.string.install_any_icon_pack_made_for_nova_styl)) + CardAction(stringResource(R.string.refresh_icons), onClick = model::reloadIcons) + CardNote(stringResource(R.string.refresh_icons_note)) // Live Clock and Calendar: the app's own icon, or live icons that match the others, or always light/dark. IosMenuRow(stringResource(R.string.clock_calendar), listOf("OFF" to stringResource(R.string.app_icons_2), "AUTO" to stringResource(R.string.live_automatic), "LIGHT" to stringResource(R.string.live_light), "DARK" to stringResource(R.string.live_dark)), if (state.liveIcons) state.liveIconLook else "OFF", @@ -791,8 +793,10 @@ internal fun CustomizationSheet(state: LauncherState, initiallyWide: Boolean, mo divider() } Column(Modifier.weight(1f).fillMaxHeight().padding(horizontal = 20.dp)) { - // With the list still on screen there's nothing for Back to reveal, so the bar keeps only Done. - SettingsNavBar(if (threeColumns) null else nestedBackLabel, onBack, onClose, + // With the list still on screen there's nothing for Back to reveal, so the bar keeps only Done. Without + // it, every page but the first gets a way back, not only the sidebar button, which doesn't read as one. + SettingsNavBar(if (threeColumns) null else nestedBackLabel + ?: if (!tiled && page != CustomizationPage.OVERVIEW) stringResource(R.string.folio) else null, onBack, onClose, leading = if (tiled) null else ({ SidebarButton { sidebarOpen = !sidebarOpen } })) if (page != CustomizationPage.OVERVIEW) SettingsLargeTitle(title) Column(Modifier.weight(1f).edgeFade(bodyScroll).verticalScroll(bodyScroll).padding(bottom = 20.dp), horizontalAlignment = Alignment.CenterHorizontally) { @@ -844,14 +848,16 @@ internal fun CustomizationSheet(state: LauncherState, initiallyWide: Boolean, mo } @Composable private fun SettingsNavBar(backLabel: String?, onBack: () -> Unit, onClose: () -> Unit, leading: (@Composable () -> Unit)? = null) { - // iOS navigation bar: "‹ Back" on sub-pages (or the sidebar button), Done on the right. + // iOS navigation bar: the sidebar button and "‹ Back" on the left, Done on the right. Box(Modifier.fillMaxWidth().heightIn(min = 44.dp)) { - if (backLabel == null && leading != null) Box(Modifier.align(Alignment.CenterStart)) { leading() } - if (backLabel != null) Row(Modifier.align(Alignment.CenterStart).clip(RoundedCornerShape(10.dp)) - .clickable(onClick = onBack).padding(vertical = 8.dp, horizontal = 2.dp).testTag("customization-back"), - verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Rounded.ChevronLeft, null, tint = IosBlue, modifier = Modifier.size(28.dp)) - Text(backLabel, color = IosBlue, fontSize = 17.sp) + Row(Modifier.align(Alignment.CenterStart), verticalAlignment = Alignment.CenterVertically) { + leading?.invoke() + if (backLabel != null) Row(Modifier.clip(RoundedCornerShape(10.dp)) + .clickable(onClick = onBack).padding(vertical = 8.dp, horizontal = 2.dp).testTag("customization-back"), + verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Rounded.ChevronLeft, null, tint = IosBlue, modifier = Modifier.size(28.dp)) + Text(backLabel, color = IosBlue, fontSize = 17.sp) + } } Text(stringResource(R.string.done), color = IosBlue, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.align(Alignment.CenterEnd).clip(RoundedCornerShape(10.dp)).clickable(onClick = onClose) diff --git a/app/src/main/java/com/mccal/folio/DuneWallpaper.kt b/app/src/main/java/com/mccal/folio/DuneWallpaper.kt index 7972247..40e4804 100644 --- a/app/src/main/java/com/mccal/folio/DuneWallpaper.kt +++ b/app/src/main/java/com/mccal/folio/DuneWallpaper.kt @@ -1,5 +1,6 @@ package com.mccal.folio +import androidx.core.content.edit import kotlinx.coroutines.flow.conflate import android.service.wallpaper.WallpaperService @@ -31,6 +32,7 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.drawscope.CanvasDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.unit.Density @@ -45,7 +47,12 @@ internal fun DuneWallpaper(modifier: Modifier = Modifier) { val photo = produceState(initialValue = initial, key1 = context, key2 = revision) { value = withContext(Dispatchers.IO) { loadLauncherBackground(context) } }.value - Canvas(Modifier.fillMaxSize().then(modifier)) { drawLauncherBackground(photo?.asImageBitmap(), palette.dark) } + // Drawn once into its own layer and reused: the background never moves, but during a swipe everything above it + // does, so it was redrawn every frame (a gradient, three dunes and 29 strokes, full screen) and the GPU missed + // frames. A cached layer is one texture copy a frame instead. + Canvas(Modifier.fillMaxSize().then(modifier).graphicsLayer { + compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen + }) { drawLauncherBackground(photo?.asImageBitmap(), palette.dark) } } internal fun DrawScope.drawLauncherBackground(photo: ImageBitmap?, dark: Boolean = false) { @@ -235,7 +242,7 @@ internal fun SystemWallpaperParallax(pager: androidx.compose.foundation.pager.Pa * * Turning it on needs the window itself, not just a flag on it: `android:windowShowWallpaper` belongs to the theme * the window was built from, and a window built opaque shows the wallpaper only through the relayout before covering - * it again, which looked like the wallpaper flashing up and vanishing. The setting is saved before this is called and + * it again, which looked like the wallpaper flashing up and vanishing. The setting is saved here, at once, and * [MainActivity] picks the wallpaper theme from it, so starting the screen again is what actually turns it on. * * Turning it off needs no restart: an opaque background over the same window hides the wallpaper, as it always did. @@ -251,8 +258,11 @@ internal tailrec fun android.content.Context.asActivity(): android.app.Activity? } internal fun android.app.Activity.applyWallpaperWindow(system: Boolean) { - val showing = window.attributes.flags and android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER != 0 - if (system && !showing) { recreate(); return } + // Saved here and on its own before the screen starts again: the full state save can be held back (a first run + // still loading its apps), and the new window is built from what's saved. apply() is enough, since the new screen + // runs in this process and reads the same preferences in memory. + getSharedPreferences(SettingKeys.PREFS, 0).edit { putBoolean(SettingKeys.SYSTEM_WALLPAPER, system) } + if (system && !showsWallpaper) { startAgain(); return } val background = if (system) android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT) else obtainStyledAttributes(R.style.Theme_Duo, intArrayOf(android.R.attr.windowBackground)).let { it.getDrawable(0).also { _ -> it.recycle() } } window.setBackgroundDrawable(background) @@ -260,8 +270,27 @@ internal fun android.app.Activity.applyWallpaperWindow(system: Boolean) { else window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) } +/** + * A new instance of this screen, with a new window. Not `recreate()`: Android keeps the old window across it, and a + * window first made opaque stays opaque to the screen even after it asks for the wallpaper, so the wallpaper never + * showed and every swipe piled onto the last frame (#12, #35). The same intent, so Settings opens again on top. + */ +private fun android.app.Activity.startAgain() { + startActivity(android.content.Intent(intent).addFlags( + android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK)) + finish() +} + +/** Whether this window actually shows Android's wallpaper behind it, whatever the setting says. */ +internal val android.app.Activity.showsWallpaper: Boolean + get() = window.attributes.flags and android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER != 0 + /** Whether Home shows Android's wallpaper (read straight from saved state: needed before the activity's window exists). */ internal fun usesSystemWallpaper(context: android.content.Context): Boolean = runCatching { - org.json.JSONObject(context.getSharedPreferences(SettingKeys.PREFS, 0).getString(SettingKeys.STATE, "{}") ?: "{}") - .optBoolean("systemWallpaper", false) + val prefs = context.getSharedPreferences(SettingKeys.PREFS, 0) + if (prefs.contains(SettingKeys.SYSTEM_WALLPAPER)) return@runCatching prefs.getBoolean(SettingKeys.SYSTEM_WALLPAPER, false) + org.json.JSONObject(prefs.getString(SettingKeys.STATE, "{}") ?: "{}").optBoolean("systemWallpaper", false) }.getOrDefault(false) + +/** Once per process: the window is rebuilt at most once to match the setting, so a mismatch can never loop. */ +internal object WallpaperWindowRepair { var tried = false } diff --git a/app/src/main/java/com/mccal/folio/HomeWorkspace.kt b/app/src/main/java/com/mccal/folio/HomeWorkspace.kt index d58e138..ce32894 100644 --- a/app/src/main/java/com/mccal/folio/HomeWorkspace.kt +++ b/app/src/main/java/com/mccal/folio/HomeWorkspace.kt @@ -336,7 +336,8 @@ internal fun HomePagePane( SharedHomeGrid(page, state.homeSlots, state.leadingSlots, previewSlots, previewLeadingSlots, previewWidgetPlacements, appsById, geometry.copy(iconSize = pageIcon), pageLabels, widgets, drag, target, folders = state.folders, onLaunch = onLaunch, onActions = onActions, onWidget = onWidget, - onFolder = onFolder, onEmptyWidget = onEmptyWidget, onMove = onMove) + onFolder = onFolder, onEmptyWidget = onEmptyWidget, onMove = onMove, + onEmptyDoubleTap = if (doubleTapAction == FolioAction.NONE) null else ({ FolioActions.run(context, doubleTapAction) })) if (state.loading) LinearProgressIndicator(Modifier.fillMaxWidth().padding(16.dp)) if (state.error != null) Text(state.error, color = Color.White, modifier = Modifier.clickable(onClick = onRefresh).padding(12.dp)) @@ -375,6 +376,8 @@ internal fun SharedHomeGrid( onFolder: (String) -> Unit, onEmptyWidget: (Int) -> Unit, onMove: (String, Int) -> Unit = { _, _ -> }, + /** The Double Tap action, for empty cells: each cell takes its own taps, so the page behind never sees them. */ + onEmptyDoubleTap: (() -> Unit)? = null, ) { val rowHeight = geometry.rowHeight val iconSize = geometry.iconSize @@ -442,7 +445,10 @@ internal fun SharedHomeGrid( // Keyboard and switch focus goes to the app or folder itself, not the empty cell behind it. .focusProperties { canFocus = false } .combinedClickable(onClick = { if (savedFolder != null) onFolder(savedFolder.id) else if (edit.active) edit.stop() }, - onLongClick = { if (savedId == null && !drag.active) onEmptyWidget(globalIndex) }) + onLongClick = { if (savedId == null && !drag.active) onEmptyWidget(globalIndex) }, + // Only on an empty cell outside jiggle mode, so a tap on an app or folder isn't held back waiting + // for a second one. + onDoubleClick = onEmptyDoubleTap?.takeIf { savedId == null && !edit.active && !drag.active }) .background(if (highlighted) Glass.copy(alpha = .25f) else Color.Transparent, RoundedCornerShape(16.dp)) .border(if (highlighted) 2.dp else 0.dp, if (highlighted) Color.White.copy(alpha = .8f) else Color.Transparent, RoundedCornerShape(16.dp)), diff --git a/app/src/main/java/com/mccal/folio/LauncherModel.kt b/app/src/main/java/com/mccal/folio/LauncherModel.kt index f27467a..8c3cb4f 100644 --- a/app/src/main/java/com/mccal/folio/LauncherModel.kt +++ b/app/src/main/java/com/mccal/folio/LauncherModel.kt @@ -338,6 +338,8 @@ class LauncherModel(application: Application) : AndroidViewModel(application) { // Accessed only in the serialized IO refresh. Returning Home reuses existing bitmaps. private val iconCache = mutableMapOf() private var iconConfiguration = "" + /** Set by [reloadIcons]; the refresh that follows drops every cached icon. */ + @Volatile private var iconsStale = false internal var completedRefreshes = 0 private set private val callback = object : LauncherApps.Callback() { @@ -375,6 +377,9 @@ class LauncherModel(application: Application) : AndroidViewModel(application) { init { launcherApps.registerCallback(callback); refresh(); FolioSettingsBridge.liveModel = java.lang.ref.WeakReference(this) } + /** Settings › Refresh Icons: for a theme app that changes icons without telling launchers (#19). */ + fun reloadIcons() { iconsStale = true; IconPacks.clear(); refresh() } + fun refresh(invalidatedPackage: String? = null, user: UserHandle = Process.myUserHandle()) { invalidatedPackage?.let { invalidatedPackages += userManager.getSerialNumberForUser(user) to it } if (refreshing) { refreshPending = true; return } @@ -385,11 +390,11 @@ class LauncherModel(application: Application) : AndroidViewModel(application) { invalidatedPackages.clear() removedPackages.clear() val resources = getApplication().resources - val configuration = resources.configuration.let { "${it.densityDpi}|${it.locales.toLanguageTags()}|${it.uiMode}" } + val configuration = resources.configuration.let { "${it.densityDpi}|${it.locales.toLanguageTags()}|${it.uiMode}|${assetsSequence(it.toString())}" } viewModelScope.launch { try { val apps = withContext(Dispatchers.IO) { - if (configuration != iconConfiguration) { iconCache.clear(); iconConfiguration = configuration } + if (configuration != iconConfiguration || iconsStale) { iconCache.clear(); iconConfiguration = configuration; iconsStale = false } iconCache.keys.removeAll { key -> parseProfileAppId(key)?.let { identity -> val serial = identity.userSerial ?: userManager.getSerialNumberForUser(Process.myUserHandle()) serial to (ComponentName.unflattenFromString(identity.component)?.packageName ?: "") in invalidated @@ -1146,7 +1151,9 @@ class LauncherModel(application: Application) : AndroidViewModel(application) { editor.putString("state_v7_backup", legacyRaw) if (legacyRaw != null && sourceSchema < 9 && !prefs.contains("state_v8_backup")) editor.putString("state_v8_backup", legacyRaw) - editor.putString("state", data.toString()).putBoolean("initialized", true).apply() + editor.putString("state", data.toString()).putBoolean("initialized", true) + // Kept beside the state so a restore or theme import that changes it chooses the right window next time. + .putBoolean(SettingKeys.SYSTEM_WALLPAPER, s.systemWallpaper).apply() } private fun load(): LauncherState = runCatching { @@ -1428,3 +1435,11 @@ val LauncherState.glassTintAmount: Float get() = if (tintedGlass) .56f * glassTi /** Reduce Transparency: nearly solid widgets, Side Bar and dock, with a clearer edge (the saved values stay as they are). */ fun LauncherState.withSolidGlass(): LauncherState = copy(widgetGlass = maxOf(widgetGlass, .9f), glassOutline = maxOf(glassOutline, .45f), statusStyle = statusStyle.copy(railGlass = maxOf(statusStyle.railGlass, .9f))) + +/** + * Android counts changes to installed themes and overlays (Samsung's Theme Park among them) in the configuration's + * assets sequence. It isn't public API, but the configuration prints it as "as.N", so a theme that swaps icons without + * a package change still clears the icon cache, while folding and rotating don't. 0 when it isn't there. + */ +internal fun assetsSequence(configuration: String): Int = + Regex("""\bas\.(\d+)""").find(configuration)?.groupValues?.get(1)?.toIntOrNull() ?: 0 diff --git a/app/src/main/java/com/mccal/folio/LauncherScreen.kt b/app/src/main/java/com/mccal/folio/LauncherScreen.kt index df90373..e651dbf 100644 --- a/app/src/main/java/com/mccal/folio/LauncherScreen.kt +++ b/app/src/main/java/com/mccal/folio/LauncherScreen.kt @@ -152,6 +152,13 @@ fun LauncherScreen( var libraryQuery by rememberSaveable { mutableStateOf("") } var pinQuery by rememberSaveable { mutableStateOf("") } val launcherActivity = androidx.activity.compose.LocalActivity.current as MainActivity + // The setting says Android's wallpaper but the window was built without it: rebuild it once to match. + LaunchedEffect(state.systemWallpaper) { + if (state.systemWallpaper && !launcherActivity.showsWallpaper && !WallpaperWindowRepair.tried) { + WallpaperWindowRepair.tried = true + launcherActivity.applyWallpaperWindow(true) + } + } val launcherRootView = LocalView.current.rootView val marketSession = remember(model) { MarketSession(launcherActivity, ModelLauncher(model)) } // Package Safe Mode: runs as Home starts, so a package that crashed Folio while it was being applied is turned off @@ -536,7 +543,9 @@ fun LauncherScreen( val panelsOn = FeatureScopes.on(state.featureScopes, "appPanels", state.appPanels, screenFor(panelWide)) if (panelsOn && !homeEdit.active) { app: AppEntry -> haptic.performHapticFeedback(HapticFeedbackType.ContextClick); overlays.panel = app.id } else null }) { - if (!state.systemWallpaper) DuneWallpaper() + // Folio's background unless Android's wallpaper is really behind the window: a see-through window with + // nothing behind it shows every earlier frame (#12, #35), so the worst case is the dunes, never a smear. + if (!state.systemWallpaper || !launcherActivity.showsWallpaper) DuneWallpaper() else if (state.wallpaperMotion) SystemWallpaperParallax(nativePager) // iOS "dark appearance dims wallpaper". val dim by androidx.compose.animation.core.animateFloatAsState(if (state.dimWallpaperDark && appearance.dark) .3f else 0f, label = "wallpaper dim") @@ -635,7 +644,13 @@ fun LauncherScreen( val pagerWidth = if (geometry.horizontalDock && !geometry.dockBesideRail) maxWidth else maxWidth - preset.dockWidth.dp - 28.dp val leftColumnOrigin = (maxWidth / 2f - geometry.gridWidth.dp) / 2f - 16.dp val homeStride = panelWidth - leftColumnOrigin - val bottomSpace = (if (isDefaultHome) 44.dp else 88.dp) + if (geometry.horizontalDock) (geometry.dockBarHeight + 16f).dp else 0.dp + // The page controls under Home (and the Preview bar before Folio is the Home app), measured: the old guess + // of 44dp, 88dp with the Preview bar, left the App Library's panel under the Preview bar sideways. + var bottomControlsHeight by remember { mutableStateOf(0.dp) } + // As the Home app it stays 44dp unless the dots are taller, so no one's automatic rows shrink; the Preview + // bar gets a clear gap above it. + val controlsSpace = if (isDefaultHome) maxOf(44.dp, bottomControlsHeight) else maxOf(88.dp, bottomControlsHeight + 12.dp) + val bottomSpace = controlsSpace + if (geometry.horizontalDock) (geometry.dockBarHeight + 16f).dp else 0.dp val workspaceMotion = if (geometry.expanded) remember(firstHome, visibleHomePages, pagerWidth, homeStride, density) { WorkspacePageMotion(firstHome, visibleHomePages, with(density) { pagerWidth.toPx() }, with(density) { homeStride.toPx() }) } else null @@ -796,7 +811,13 @@ fun LauncherScreen( // Portrait unfolded (iPhone Duo): a horizontal dock bar centered along the bottom, above the page controls. val dockPitch = geometry.dockPitch val dockBarWidth = (dockPitch * state.dock.size + 16f).dp - Box((if (geometry.horizontalDock) (if (hinge?.active == true && hinge.vertical) + // Like iPhone, the dock bar steps aside for Today View: it follows the swipe out, then leaves altogether so + // it can't sit over Today's widgets and Edit button (#25). Only the bar; the Side Bar dock is beside Today. + val dockStepsAsideForToday = todayMode && firstHome > 0 && geometry.horizontalDock + val dockAwayForToday by remember(dockStepsAsideForToday, nativePager) { + derivedStateOf { dockStepsAsideForToday && nativePager.currentPage + nativePager.currentPageOffsetFraction <= .02f } + } + if (!dockAwayForToday) Box((if (geometry.horizontalDock) (if (hinge?.active == true && hinge.vertical) // Half folded like a book: the bar sits centered on the trailing half, off the hinge. Modifier.align(Alignment.BottomEnd).padding(end = ((contentWidth / 2 - dockBarWidth) / 2).coerceAtLeast(0.dp)) else if (geometry.dockBesideRail) @@ -805,10 +826,16 @@ fun LauncherScreen( .padding(start = if (state.leftHanded) 0.dp else ((pagerWidth + 16.dp - dockBarWidth) / 2).coerceAtLeast(0.dp), end = if (state.leftHanded) ((pagerWidth + 16.dp - dockBarWidth) / 2).coerceAtLeast(0.dp) else 0.dp) else Modifier.align(Alignment.BottomCenter)) - .padding(bottom = (if (isDefaultHome) 44 else 88).dp + 8.dp) + .padding(bottom = controlsSpace + 8.dp) .width(dockBarWidth).height(geometry.dockBarHeight.dp) else Modifier.align(railTop(state.leftHanded)).railEdge(state.leftHanded, 12.dp).offset(y = dockTopShown.dp) .width(preset.dockWidth.dp).height(dockHeightShown.dp)).graphicsLayer { + if (dockStepsAsideForToday) { + // Read here, not in composition, so following the swipe doesn't recompose the screen. + val towardToday = (1f - nativePager.currentPage - nativePager.currentPageOffsetFraction).coerceIn(0f, 1f) + alpha = 1f - towardToday + translationY = towardToday * size.height * .6f + } // Composite the stationary dock independently of the shared pager layer (not while magnifying: it would clip). compositingStrategy = if (state.dockMagnify) androidx.compose.ui.graphics.CompositingStrategy.Auto else androidx.compose.ui.graphics.CompositingStrategy.Offscreen @@ -827,6 +854,7 @@ fun LauncherScreen( // page's width is held out of the row, leaving it centred on Home on both screens. val besideHome = if (geometry.expanded) panelWidth.coerceAtLeast(0.dp) else 0.dp Column(Modifier.align(if (state.leftHanded) Alignment.BottomEnd else Alignment.BottomStart).width(pagerWidth) + .onSizeChanged { bottomControlsHeight = with(density) { it.height.toDp() } } .padding(start = if (state.leftHanded) 0.dp else besideHome + 16.dp, end = if (state.leftHanded) besideHome + 16.dp else 0.dp, bottom = 6.dp), horizontalAlignment = Alignment.CenterHorizontally) { if (!isDefaultHome && !homeEdit.active && !drag.active) PreviewBar(onUseAsHome = { sheet = ""; onMakeDefault() }, diff --git a/app/src/main/java/com/mccal/folio/MainActivity.kt b/app/src/main/java/com/mccal/folio/MainActivity.kt index 065f174..30a09d8 100644 --- a/app/src/main/java/com/mccal/folio/MainActivity.kt +++ b/app/src/main/java/com/mccal/folio/MainActivity.kt @@ -81,7 +81,7 @@ class MainActivity : ComponentActivity() { private var recreatingShadeSetup = false override fun onCreate(savedInstanceState: Bundle?) { - // The wallpaper theme must be chosen before the window exists (switching it later recreates the activity). + // The wallpaper theme must be chosen before the window exists (switching to it starts the screen again). if (usesSystemWallpaper(this)) setTheme(R.style.Theme_Duo_Wallpaper) super.onCreate(savedInstanceState) setupExperience = SetupExperience(this) diff --git a/app/src/main/java/com/mccal/folio/SettingKeys.kt b/app/src/main/java/com/mccal/folio/SettingKeys.kt index b835653..23bb56a 100644 --- a/app/src/main/java/com/mccal/folio/SettingKeys.kt +++ b/app/src/main/java/com/mccal/folio/SettingKeys.kt @@ -4,6 +4,8 @@ package com.mccal.folio internal object SettingKeys { const val PREFS = "launcher" const val STATE = "state" + /** Whether Home's window shows Android's wallpaper, saved on its own and at once (see applyWallpaperWindow). */ + const val SYSTEM_WALLPAPER = "systemWallpaperWindow" const val DOCK = "dock" const val LEFT_HANDED = "leftHanded" const val DOCK_EVERYWHERE = "dockEverywhere" diff --git a/app/src/main/java/com/mccal/folio/SetupChecklist.kt b/app/src/main/java/com/mccal/folio/SetupChecklist.kt index 0bab232..27c49ba 100644 --- a/app/src/main/java/com/mccal/folio/SetupChecklist.kt +++ b/app/src/main/java/com/mccal/folio/SetupChecklist.kt @@ -74,7 +74,7 @@ internal fun rememberSetupSteps(isDefaultHome: Boolean, onMakeDefault: () -> Uni notifications.isNotificationPolicyAccessGranted, false, context.getString(R.string.allow)) { open(Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS)) }, SetupStep(Icons.Rounded.Wallpaper, context.getString(R.string.keep_your_wallpaper), context.getString(R.string.coming_from_samsung_s_or_another_launche), - systemWallpaper, false, context.getString(R.string.use)) { onSystemWallpaper(true); context.asActivity()?.recreate() }, + systemWallpaper, false, context.getString(R.string.use)) { onSystemWallpaper(true); context.asActivity()?.applyWallpaperWindow(true) }, SetupStep(Icons.Rounded.Assistant, context.getString(R.string.folio_as_your_digital_assistant), context.getString(R.string.holding_the_side_key_opens_folio_s_picke), AssistPickerActivity.isDefaultAssistant(context), false, context.getString(R.string.choose)) { open(AssistPickerActivity.settingsIntent()) }, diff --git a/app/src/main/java/com/mccal/folio/TodayView.kt b/app/src/main/java/com/mccal/folio/TodayView.kt index b5e8a98..201b05e 100644 --- a/app/src/main/java/com/mccal/folio/TodayView.kt +++ b/app/src/main/java/com/mccal/folio/TodayView.kt @@ -67,7 +67,7 @@ internal fun TodayView(state: LauncherState, widgets: WidgetController, modifier // Search capsule val ink = LocalHomeInk.current Row(Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp)).background(Color.White.copy(alpha = if (ink.dark) .5f else .16f)) - .clickable(onClickLabel = "Search", onClick = onSearch).padding(horizontal = 14.dp, vertical = 11.dp), + .clickable(onClickLabel = stringResource(R.string.search), onClick = onSearch).padding(horizontal = 14.dp, vertical = 11.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Rounded.Search, null, tint = ink.secondary, modifier = Modifier.size(20.dp)) Spacer(Modifier.width(8.dp)) diff --git a/app/src/main/res/values-b+zh+Hans/strings.xml b/app/src/main/res/values-b+zh+Hans/strings.xml index aecd382..ed14568 100644 --- a/app/src/main/res/values-b+zh+Hans/strings.xml +++ b/app/src/main/res/values-b+zh+Hans/strings.xml @@ -1359,4 +1359,6 @@ 功能 在折痕处分开 · 建议和自动更正 · 六种语言 · 剪贴板历史 · 快捷短语 · 不申请任何权限 Keyd 是一个单独的 App,因为 Android 要求键盘必须是独立的输入法。你的代码已把它的源添加到 Market 中,所以可以直接获取。它不申请任何权限,连网络权限也没有。 + 刷新图标 + 如果主题应用更改了图标,而 Folio 仍显示旧图标,可在这里重新载入。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e95a3a3..96fd90d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1402,4 +1402,6 @@ What it does Splits around the crease · suggestions and autocorrect · six languages · clipboard history · shortcuts · no permissions at all Keyd is a separate app, because Android needs a keyboard to be its own input method. Your code added its source to the Market, so it\'s there to get, and it asks for no permissions at all, not even the internet. + Refresh Icons + If a theme app changed your icons and Folio still shows the old ones, this loads them again. diff --git a/app/src/test/java/com/mccal/folio/HardcodedTextTest.kt b/app/src/test/java/com/mccal/folio/HardcodedTextTest.kt index d83b564..d54ae9d 100644 --- a/app/src/test/java/com/mccal/folio/HardcodedTextTest.kt +++ b/app/src/test/java/com/mccal/folio/HardcodedTextTest.kt @@ -56,5 +56,5 @@ class HardcodedTextTest { assertTrue("English in the manifest: $literal. Use @string/ so Android shows it translated.", literal.isEmpty()) } - private companion object { const val LIMIT = 222 } + private companion object { const val LIMIT = 221 } } diff --git a/app/src/test/java/com/mccal/folio/IconRefreshTest.kt b/app/src/test/java/com/mccal/folio/IconRefreshTest.kt new file mode 100644 index 0000000..a2e41d0 --- /dev/null +++ b/app/src/test/java/com/mccal/folio/IconRefreshTest.kt @@ -0,0 +1,20 @@ +package com.mccal.folio + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** #19: a theme that swaps icons (Theme Park) changes the configuration's assets sequence, and nothing else Folio watched. */ +class IconRefreshTest { + @Test fun `reads the assets sequence from the configuration's text`() { + // As Configuration.toString prints it on Android 17, trimmed. + assertEquals(42, assetsSequence("{1.0 310mcc260mnc [en_US] ldltr sw561dp w561dp h839dp 420dpi nrml long port finger -keyb/v/h -nav/h winConfig={ } s.8 fontWeightAdjustment=0 as.42}")) + } + + @Test fun `is 0 when the configuration doesn't say`() { + assertEquals(0, assetsSequence("{1.0 ?mcc?mnc [en_US] ldltr sw561dp}")) + } + + @Test fun `isn't fooled by other fields that end in as`() { + assertEquals(0, assetsSequence("{1.0 has.3 alias.7}")) + } +}