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