1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.graphics 18 19 import android.content.Context 20 import android.content.res.Resources 21 import com.android.launcher3.EncryptionType 22 import com.android.launcher3.Item 23 import com.android.launcher3.LauncherPrefChangeListener 24 import com.android.launcher3.LauncherPrefs 25 import com.android.launcher3.LauncherPrefs.Companion.backedUpItem 26 import com.android.launcher3.dagger.ApplicationContext 27 import com.android.launcher3.dagger.LauncherAppComponent 28 import com.android.launcher3.dagger.LauncherAppSingleton 29 import com.android.launcher3.graphics.ShapeDelegate.Companion.pickBestShape 30 import com.android.launcher3.icons.IconThemeController 31 import com.android.launcher3.icons.mono.MonoIconThemeController 32 import com.android.launcher3.shapes.ShapesProvider 33 import com.android.launcher3.util.DaggerSingletonObject 34 import com.android.launcher3.util.DaggerSingletonTracker 35 import com.android.launcher3.util.Executors.MAIN_EXECUTOR 36 import com.android.launcher3.util.SimpleBroadcastReceiver 37 import java.util.concurrent.CopyOnWriteArrayList 38 import javax.inject.Inject 39 40 /** Centralized class for managing Launcher icon theming */ 41 @LauncherAppSingleton 42 class ThemeManager 43 @Inject 44 constructor( 45 @ApplicationContext private val context: Context, 46 private val prefs: LauncherPrefs, 47 private val iconControllerFactory: IconControllerFactory, 48 lifecycle: DaggerSingletonTracker, 49 ) { 50 51 /** Representation of the current icon state */ 52 var iconState = parseIconState(null) 53 private set 54 55 var isMonoThemeEnabled 56 set(value) = prefs.put(THEMED_ICONS, value) 57 get() = prefs.get(THEMED_ICONS) 58 59 val themeController 60 get() = iconState.themeController 61 62 val isIconThemeEnabled 63 get() = themeController != null 64 65 val iconShape 66 get() = iconState.iconShape 67 68 val folderShape 69 get() = iconState.folderShape 70 71 private val listeners = CopyOnWriteArrayList<ThemeChangeListener>() 72 73 init { 74 val receiver = SimpleBroadcastReceiver(context, MAIN_EXECUTOR) { verifyIconState() } 75 receiver.registerPkgActions("android", ACTION_OVERLAY_CHANGED) 76 77 val keys = (iconControllerFactory.prefKeys + PREF_ICON_SHAPE) 78 79 val keysArray = keys.toTypedArray() 80 val prefKeySet = keys.map { it.sharedPrefKey } 81 val prefListener = LauncherPrefChangeListener { key -> 82 if (prefKeySet.contains(key)) verifyIconState() 83 } 84 prefs.addListener(prefListener, *keysArray) 85 lifecycle.addCloseable { 86 receiver.unregisterReceiverSafely() 87 prefs.removeListener(prefListener, *keysArray) 88 } 89 } 90 91 private fun verifyIconState() { 92 val newState = parseIconState(iconState) 93 if (newState == iconState) return 94 iconState = newState 95 96 listeners.forEach { it.onThemeChanged() } 97 } 98 99 fun addChangeListener(listener: ThemeChangeListener) = listeners.add(listener) 100 101 fun removeChangeListener(listener: ThemeChangeListener) = listeners.remove(listener) 102 103 private fun parseIconState(oldState: IconState?): IconState { 104 val shapeModel = 105 prefs.get(PREF_ICON_SHAPE).let { shapeOverride -> 106 ShapesProvider.iconShapes.firstOrNull { it.key == shapeOverride } 107 } 108 val iconMask = 109 when { 110 shapeModel != null -> shapeModel.pathString 111 CONFIG_ICON_MASK_RES_ID == Resources.ID_NULL -> "" 112 else -> context.resources.getString(CONFIG_ICON_MASK_RES_ID) 113 } 114 115 val iconShape = 116 if (oldState != null && oldState.iconMask == iconMask) oldState.iconShape 117 else pickBestShape(iconMask) 118 119 val folderShapeMask = shapeModel?.folderPathString ?: iconMask 120 val folderShape = 121 when { 122 oldState != null && oldState.folderShapeMask == folderShapeMask -> 123 oldState.folderShape 124 folderShapeMask == iconMask || folderShapeMask.isEmpty() -> iconShape 125 else -> pickBestShape(folderShapeMask) 126 } 127 128 return IconState( 129 iconMask = iconMask, 130 folderShapeMask = folderShapeMask, 131 themeController = iconControllerFactory.createThemeController(), 132 iconScale = shapeModel?.iconScale ?: 1f, 133 iconShape = iconShape, 134 folderShape = folderShape, 135 ) 136 } 137 138 data class IconState( 139 val iconMask: String, 140 val folderShapeMask: String, 141 val themeController: IconThemeController?, 142 val themeCode: String = themeController?.themeID ?: "no-theme", 143 val iconScale: Float = 1f, 144 val iconShape: ShapeDelegate, 145 val folderShape: ShapeDelegate, 146 ) { 147 fun toUniqueId() = "${iconMask.hashCode()},$themeCode" 148 } 149 150 /** Interface for receiving theme change events */ 151 fun interface ThemeChangeListener { 152 fun onThemeChanged() 153 } 154 155 open class IconControllerFactory @Inject constructor(protected val prefs: LauncherPrefs) { 156 157 open val prefKeys: List<Item> = listOf(THEMED_ICONS) 158 159 open fun createThemeController(): IconThemeController? { 160 return if (prefs.get(THEMED_ICONS)) MONO_THEME_CONTROLLER else null 161 } 162 } 163 164 companion object { 165 166 @JvmField val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getThemeManager) 167 const val KEY_ICON_SHAPE = "icon_shape_model" 168 169 const val KEY_THEMED_ICONS = "themed_icons" 170 @JvmField val THEMED_ICONS = backedUpItem(KEY_THEMED_ICONS, false, EncryptionType.ENCRYPTED) 171 @JvmField val PREF_ICON_SHAPE = backedUpItem(KEY_ICON_SHAPE, "", EncryptionType.ENCRYPTED) 172 173 private const val ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED" 174 private val CONFIG_ICON_MASK_RES_ID: Int = 175 Resources.getSystem().getIdentifier("config_icon_mask", "string", "android") 176 177 // Use a constant to allow equality check in verifyIconState 178 private val MONO_THEME_CONTROLLER = MonoIconThemeController() 179 } 180 } 181