1 /*
2  * Copyright (C) 2023 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.car.customization.tool.domain.menu
18 
19 import android.content.Context
20 import com.android.car.customization.tool.di.UIContext
21 import com.android.car.customization.tool.domain.Action
22 import java.util.LinkedList
23 import java.util.Queue
24 import javax.inject.Inject
25 import javax.inject.Provider
26 import javax.inject.Singleton
27 
28 /**
29  * The Controller for the Menu.
30  *
31  * The main responsibility of this class is to provide a new Menu at the start of the service and to
32  * update the Menu based on the actions that are sent by the [CustomizationToolStateMachine]
33  *
34  * @param rootProvider a Dagger [Provider] that contains the root node for the Menu.
35  * @param context Android [Context], mainly used for making exceptions messages more useful.
36  * @param menuActionReducers used to extend the actions supported by the [MenuController].
37  */
38 @Singleton
39 internal class MenuController @Inject constructor(
40     private val rootProvider: Provider<MenuItem.SubMenuNavigation>,
41     @UIContext private val context: Context,
42     private val menuActionReducers: Map<Class<out Action>, @JvmSuppressWildcards MenuActionReducer>,
43 ) {
44 
buildMenunull45     fun buildMenu(): Menu {
46         val root = rootProvider.get()
47         assertUniqueIds(root)
48         return Menu(rootNode = root, currentParentNode = root)
49     }
50 
handleActionnull51     fun handleAction(menu: Menu, action: MenuAction): Menu = when (action) {
52         ReloadMenuAction -> reloadMenu(menu)
53         NavigateUpAction -> navigateUp(menu)
54         is OpenSubMenuAction -> navigateToSubMenu(menu, action)
55         else -> menuActionReducers[action::class.java]?.reduce(
56             menu,
57             action
58         )
59             ?: throw IllegalArgumentException("Action not implemented for this MenuController $action")
60     }
61 
assertUniqueIdsnull62     private fun assertUniqueIds(
63         root: MenuItem.SubMenuNavigation,
64     ) {
65         val existingIds = mutableSetOf<Int>()
66         val nodesToVisit: Queue<MenuItem> = LinkedList()
67         nodesToVisit.add(root)
68 
69         while (nodesToVisit.isNotEmpty()) {
70             val node = requireNotNull(nodesToVisit.poll())
71             if (existingIds.contains(node.displayTextRes)) {
72                 throw IllegalStateException(
73                     "The menu tree contains duplicate IDs: ${
74                         context.resources.getResourceName(node.displayTextRes)
75                     }"
76                 )
77             }
78             if (node is MenuItem.SubMenuNavigation) {
79                 node.subMenu.forEach {
80                     if (it !is MenuItem.UpNavigation) nodesToVisit.add(it)
81                 }
82             }
83             existingIds.add(node.displayTextRes)
84         }
85     }
86 
87     /**
88      * The function executed when the [MenuAction.NavigateUpAction] action is triggered.
89      *
90      * @param oldMenu the current [Menu].
91      * @return A new [Menu] with an updated [Menu.currentParentNode] value.
92      * @throws IllegalStateException in case the new parent node is not found.
93      */
navigateUpnull94     private fun navigateUp(oldMenu: Menu): Menu {
95         val newParent = oldMenu.rootNode.findParentOf(oldMenu.currentParentNode)
96             ?: throw IllegalStateException(
97                 "No parent has been found from the node: ${oldMenu.currentParentNode}"
98             )
99         return oldMenu.copy(currentParentNode = newParent)
100     }
101 
102     /**
103      * The function executed when the [MenuAction.OpenSubMenuAction] action is triggered. It changes the
104      * [Menu] activating a new sub menu.
105      *
106      * @param oldMenu the current [Menu]
107      * @param action A [MenuAction.OpenSubMenuAction] action that contains the information of the new sub
108      * menu to show
109      */
navigateToSubMenunull110     private fun navigateToSubMenu(
111         oldMenu: Menu,
112         action: OpenSubMenuAction,
113     ): Menu {
114         val newParent =
115             oldMenu.currentParentNode.subMenu.find { it.displayTextRes == action.newParentId }
116                 ?: throw IllegalArgumentException(
117                     "The new menu is not a direct child of the current menu: " +
118                         "current parent: ${oldMenu.currentParentNode}, " +
119                         "new parent id: ${context.getString(action.newParentId)}"
120                 )
121 
122         return oldMenu.copy(currentParentNode = newParent as MenuItem.SubMenuNavigation)
123     }
124 
125     /**
126      * The function executed when the [MenuAction.ReloadMenuAction] action is triggered. It creates a
127      * whole new [Menu], updating all of the [MenuItem] and in doing so makes sure that the
128      * [Menu] always reflects the actual state of the system
129      *
130      * @param oldMenu the current [Menu]
131      * @return a new [Menu] with updated nodes
132      */
reloadMenunull133     private fun reloadMenu(oldMenu: Menu): Menu {
134         val newRoot = rootProvider.get()
135         val newParentNode = newRoot.findSubMenuNavigationNode(oldMenu.currentParentNode.displayTextRes)
136             ?: throw IllegalStateException(
137                 "The new menu tree doesn't contain ID: ${
138                     context.getString(oldMenu.currentParentNode.displayTextRes)
139                 }"
140             )
141         return Menu(rootNode = newRoot, currentParentNode = newParentNode)
142     }
143 }
144