• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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