All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.smartdevicelink.managers.screen.menu.BaseMenuManager Maven / Gradle / Ivy

/*
 * Copyright (c) 2019 Livio, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * Neither the name of the Livio Inc. nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.smartdevicelink.managers.screen.menu;

import static com.smartdevicelink.managers.screen.menu.MenuReplaceUtilities.cloneMenuCellsList;

import androidx.annotation.NonNull;

import com.livio.taskmaster.Queue;
import com.livio.taskmaster.Task;
import com.smartdevicelink.managers.BaseSubManager;
import com.smartdevicelink.managers.CompletionListener;
import com.smartdevicelink.managers.ISdl;
import com.smartdevicelink.managers.file.FileManager;
import com.smartdevicelink.managers.lifecycle.OnSystemCapabilityListener;
import com.smartdevicelink.managers.lifecycle.SystemCapabilityManager;
import com.smartdevicelink.protocol.enums.FunctionID;
import com.smartdevicelink.proxy.RPCNotification;
import com.smartdevicelink.proxy.rpc.DisplayCapability;
import com.smartdevicelink.proxy.rpc.OnCommand;
import com.smartdevicelink.proxy.rpc.OnHMIStatus;
import com.smartdevicelink.proxy.rpc.WindowCapability;
import com.smartdevicelink.proxy.rpc.enums.DisplayType;
import com.smartdevicelink.proxy.rpc.enums.HMILevel;
import com.smartdevicelink.proxy.rpc.enums.PredefinedWindows;
import com.smartdevicelink.proxy.rpc.enums.SystemCapabilityType;
import com.smartdevicelink.proxy.rpc.enums.SystemContext;
import com.smartdevicelink.proxy.rpc.listeners.OnRPCNotificationListener;
import com.smartdevicelink.util.DebugTool;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

abstract class BaseMenuManager extends BaseSubManager {
    private static final String TAG = "BaseMenuManager";
    static final int parentIdNotFound = 2000000000;

    final WeakReference fileManager;
    List currentMenuCells;
    List menuCells;
    DynamicMenuUpdatesMode dynamicMenuUpdatesMode;
    MenuConfiguration currentMenuConfiguration;
    MenuConfiguration menuConfiguration;
    private String displayType;
    HMILevel currentHMILevel;
    SystemContext currentSystemContext;
    OnRPCNotificationListener hmiListener;
    OnRPCNotificationListener commandListener;
    OnSystemCapabilityListener onDisplaysCapabilityListener;
    WindowCapability windowCapability;
    Queue transactionQueue;

    BaseMenuManager(@NonNull ISdl internalInterface, @NonNull FileManager fileManager) {
        super(internalInterface);

        this.menuConfiguration = new MenuConfiguration(null, null);
        this.menuCells = new ArrayList<>();
        this.currentMenuCells = new ArrayList<>();
        this.dynamicMenuUpdatesMode = DynamicMenuUpdatesMode.ON_WITH_COMPAT_MODE;
        this.transactionQueue = newTransactionQueue();
        this.fileManager = new WeakReference<>(fileManager);
        this.currentSystemContext = SystemContext.SYSCTXT_MAIN;
        this.currentHMILevel = HMILevel.HMI_NONE;
        addListeners();
    }

    @Override
    public void start(CompletionListener listener) {
        transitionToState(READY);
        super.start(listener);
    }

    @Override
    public void dispose() {
        menuCells = new ArrayList<>();
        currentMenuCells = new ArrayList<>();
        currentHMILevel = HMILevel.HMI_NONE;
        currentSystemContext = SystemContext.SYSCTXT_MAIN;
        dynamicMenuUpdatesMode = DynamicMenuUpdatesMode.ON_WITH_COMPAT_MODE;
        windowCapability = null;
        menuConfiguration = null;
        currentMenuConfiguration = null;

        // Cancel the operations
        if (transactionQueue != null) {
            transactionQueue.close();
            transactionQueue = null;
        }

        // Remove listeners
        internalInterface.removeOnRPCNotificationListener(FunctionID.ON_HMI_STATUS, hmiListener);
        internalInterface.removeOnRPCNotificationListener(FunctionID.ON_COMMAND, commandListener);
        if (internalInterface.getSystemCapabilityManager() != null) {
            internalInterface.getSystemCapabilityManager().removeOnSystemCapabilityListener(SystemCapabilityType.DISPLAYS, onDisplaysCapabilityListener);
        }

        super.dispose();
    }

    private Queue newTransactionQueue() {
        Queue queue = internalInterface.getTaskmaster().createQueue("MenuManager", 7, false);
        queue.pause();
        return queue;
    }

    // Suspend the queue if the HMI level is NONE since we want to delay sending RPCs until we're in non-NONE
    private void updateTransactionQueueSuspended() {
        if (currentHMILevel == HMILevel.HMI_NONE || currentSystemContext == SystemContext.SYSCTXT_MENU) {
            DebugTool.logInfo(TAG, String.format("Suspending the transaction queue. Current HMI level is: %s, current system context is: %s", currentHMILevel, currentSystemContext));
            transactionQueue.pause();
        } else {
            DebugTool.logInfo(TAG, "Starting the transaction queue");
            transactionQueue.resume();
        }
    }

    public void setDynamicUpdatesMode(@NonNull DynamicMenuUpdatesMode value) {
        this.dynamicMenuUpdatesMode = value;
    }

    /**
     * @return The currently set DynamicMenuUpdatesMode. It defaults to ON_WITH_COMPAT_MODE
     */
    public DynamicMenuUpdatesMode getDynamicMenuUpdatesMode() {
        return this.dynamicMenuUpdatesMode;
    }

    /**
     * Creates and sends all associated Menu RPCs
     *
     * @param cells - the menu cells that are to be sent to the head unit, including their sub-cells.
     */
    public void setMenuCells(@NonNull List cells) {
        if (cells == null) {
            DebugTool.logError(TAG, "Cells list is null. Skipping...");
            return;
        }

        if (!menuCellsAreUnique(cells, new ArrayList())) {
            DebugTool.logError(TAG, "Not all set menu cells are unique, but that is required");
            return;
        }

        // Create a deep copy of the list so future changes by developers don't affect the algorithm logic
        this.menuCells = cloneMenuCellsList(cells);

        boolean isDynamicMenuUpdateActive = isDynamicMenuUpdateActive(dynamicMenuUpdatesMode, displayType);
        Task operation = new MenuReplaceOperation(internalInterface, fileManager.get(), windowCapability, currentMenuConfiguration, currentMenuCells, menuCells, isDynamicMenuUpdateActive, new MenuManagerCompletionListener() {
            @Override
            public void onComplete(boolean success, List currentMenuCells) {
                BaseMenuManager.this.currentMenuCells = currentMenuCells;
                updateMenuReplaceOperationsWithNewCurrentMenu();
                DebugTool.logInfo(TAG, "Finished updating menu");
            }
        });

        // Cancel previous MenuReplaceOperations
        for (Task task : transactionQueue.getTasksAsList()) {
            if (task instanceof MenuReplaceOperation) {
                task.cancelTask();
            }
        }

        transactionQueue.add(operation, false);
    }

    /**
     * Returns current list of menu cells
     *
     * @return a List of Currently set menu cells
     */
    public List getMenuCells() {
        return menuCells;
    }

    private boolean openMenuPrivate(MenuCell cell) {
        MenuCell foundClonedCell = null;

        if (cell != null) {
            // We must see if we have a copy of this cell, since we clone the objects
            for (MenuCell clonedCell : currentMenuCells) {
                if (clonedCell.equals(cell) && clonedCell.getCellId() != parentIdNotFound) {
                    // We've found the correct sub menu cell
                    foundClonedCell = clonedCell;
                    break;
                }
            }
        }

        if (cell != null && (!cell.isSubMenuCell())) {
            DebugTool.logError(TAG, String.format("The cell %s does not contain any sub cells, so no submenu can be opened", cell.getTitle()));
            return false;
        } else if (cell != null && foundClonedCell == null) {
            DebugTool.logError(TAG, "This cell has not been sent to the head unit, so no submenu can be opened. Make sure that the cell exists in the SDLManager.menu list");
            return false;
        } else if (internalInterface.getSdlMsgVersion().getMajorVersion() < 6) {
            DebugTool.logWarning(TAG, "The openSubmenu method is not supported on this head unit.");
            return false;
        }

        // Create the operation
        MenuShowOperation operation = new MenuShowOperation(internalInterface, foundClonedCell);

        // Cancel previous open menu operations
        for (Task task : transactionQueue.getTasksAsList()) {
            if (task instanceof MenuShowOperation) {
                task.cancelTask();
            }
        }

        transactionQueue.add(operation, false);

        return true;
    }

    /**
     * Opens the Main Menu
     */
    public boolean openMenu() {
        return openMenuPrivate(null);
    }

    /**
     * Opens a subMenu.
     *
     * @param cell - A SubMenu cell whose sub menu you wish to open
     */
    public boolean openSubMenu(@NonNull MenuCell cell) {
        return openMenuPrivate(cell);
    }


    /**
     * This method is called via the screen manager to set the menuConfiguration.
     * The menuConfiguration.SubMenuLayout value will be used when a menuCell with sub-cells has a null value for SubMenuLayout
     *
     * @param menuConfiguration - The default menuConfiguration
     */
    public void setMenuConfiguration(@NonNull final MenuConfiguration menuConfiguration) {
        if (menuConfiguration.equals(this.menuConfiguration)) {
            DebugTool.logInfo(TAG, "New menu configuration is equal to existing one, will not set new configuration");
            return;
        }

        this.menuConfiguration = menuConfiguration;

        MenuConfigurationUpdateOperation operation = new MenuConfigurationUpdateOperation(internalInterface, windowCapability, menuConfiguration, new CompletionListener() {
            @Override
            public void onComplete(boolean success) {
                if (!success) {
                    DebugTool.logError(TAG, "Error updating menu configuration.");
                    return;
                }
                BaseMenuManager.this.currentMenuConfiguration = menuConfiguration;
                updateMenuReplaceOperationsWithNewMenuConfiguration();
            }
        });

        // Cancel previous menu configuration operations
        for (Task task : transactionQueue.getTasksAsList()) {
            if (task instanceof MenuConfigurationUpdateOperation) {
                task.cancelTask();
            }
        }

        transactionQueue.add(operation, false);
    }

    public MenuConfiguration getMenuConfiguration() {
        return this.menuConfiguration;
    }

    private void addListeners() {
        onDisplaysCapabilityListener = new OnSystemCapabilityListener() {
            @Override
            public void onCapabilityRetrieved(Object capability) {
                // instead of using the parameter it's more safe to use the convenience method
                List capabilities = SystemCapabilityManager.convertToList(capability, DisplayCapability.class);
                if (capabilities == null || capabilities.size() == 0) {
                    DebugTool.logError(TAG, "Capabilities sent here are null or empty");
                } else {
                    DisplayCapability display = capabilities.get(0);
                    displayType = display.getDisplayName();
                    for (WindowCapability windowCapability : display.getWindowCapabilities()) {
                        int currentWindowID = windowCapability.getWindowID() != null ? windowCapability.getWindowID() : PredefinedWindows.DEFAULT_WINDOW.getValue();
                        if (currentWindowID == PredefinedWindows.DEFAULT_WINDOW.getValue()) {
                            BaseMenuManager.this.windowCapability = windowCapability;
                            updateMenuReplaceOperationsWithNewWindowCapability();
                        }
                    }
                }
            }

            @Override
            public void onError(String info) {
                DebugTool.logError(TAG, "Display Capability cannot be retrieved");
                windowCapability = null;
            }
        };
        if (internalInterface.getSystemCapabilityManager() != null) {
            this.internalInterface.getSystemCapabilityManager().addOnSystemCapabilityListener(SystemCapabilityType.DISPLAYS, onDisplaysCapabilityListener);
        }

        hmiListener = new OnRPCNotificationListener() {
            @Override
            public void onNotified(RPCNotification notification) {
                OnHMIStatus onHMIStatus = (OnHMIStatus) notification;
                if (onHMIStatus.getWindowID() != null && onHMIStatus.getWindowID() != PredefinedWindows.DEFAULT_WINDOW.getValue()) {
                    return;
                }
                currentHMILevel = onHMIStatus.getHmiLevel();
                currentSystemContext = onHMIStatus.getSystemContext();
                updateTransactionQueueSuspended();
            }
        };
        internalInterface.addOnRPCNotificationListener(FunctionID.ON_HMI_STATUS, hmiListener);

        commandListener = new OnRPCNotificationListener() {
            @Override
            public void onNotified(RPCNotification notification) {
                OnCommand onCommand = (OnCommand) notification;
                callListenerForCells(currentMenuCells, onCommand);
            }
        };
        internalInterface.addOnRPCNotificationListener(FunctionID.ON_COMMAND, commandListener);
    }

    private boolean callListenerForCells(List cells, OnCommand command) {
        if (cells == null || cells.isEmpty() || command == null) {
            return false;
        }

        for (MenuCell cell : cells) {
            if (cell.getCellId() == command.getCmdID() && cell.getMenuSelectionListener() != null) {
                cell.getMenuSelectionListener().onTriggered(command.getTriggerSource());
                return true;
            }
            if (cell.isSubMenuCell() && !cell.getSubCells().isEmpty()) {
                // for each cell, if it has sub cells, recursively loop through those as well
                boolean success = callListenerForCells(cell.getSubCells(), command);
                if (success) {
                    return true;
                }
            }
        }

        return false;
    }

    private void updateMenuReplaceOperationsWithNewCurrentMenu() {
        for (Task task : transactionQueue.getTasksAsList()) {
            if (task instanceof MenuReplaceOperation) {
                ((MenuReplaceOperation) task).setCurrentMenu(this.currentMenuCells);
            }
        }
    }

    private void updateMenuReplaceOperationsWithNewWindowCapability() {
        for (Task task : transactionQueue.getTasksAsList()) {
            if (task instanceof MenuReplaceOperation) {
                ((MenuReplaceOperation) task).setWindowCapability(this.windowCapability);
            }
        }
    }

    private void updateMenuReplaceOperationsWithNewMenuConfiguration() {
        for (Task task : transactionQueue.getTasksAsList()) {
            if (task instanceof MenuReplaceOperation) {
                ((MenuReplaceOperation) task).setMenuConfiguration(currentMenuConfiguration);
            }
        }
    }

    private boolean isDynamicMenuUpdateActive(DynamicMenuUpdatesMode updateMode, String displayType) {
        if (updateMode.equals(DynamicMenuUpdatesMode.ON_WITH_COMPAT_MODE)) {
            if (displayType == null) {
                return true;
            }
            return (!displayType.equals(DisplayType.GEN3_8_INCH.toString()));
        } else if (updateMode.equals(DynamicMenuUpdatesMode.FORCE_OFF)) {
            return false;
        } else if (updateMode.equals(DynamicMenuUpdatesMode.FORCE_ON)) {
            return true;
        }

        return true;
    }

    /**
     * Check for cell lists with completely duplicate information, or any duplicate voiceCommands
     *
     * @param cells            List of MenuCell's you will be adding
     * @param allVoiceCommands List of String's for VoiceCommands (Used for recursive calls to check voiceCommands of the cells)
     * @return Boolean that indicates whether menuCells are unique or not
     */
    private boolean menuCellsAreUnique(List cells, ArrayList allVoiceCommands) {
        // Check all voice commands for identical items and check each list of cells for identical cells
        HashSet identicalCellsCheckSet = new HashSet<>();

        for (MenuCell cell : cells) {
            identicalCellsCheckSet.add(cell);

            // Recursively check the sub-cell lists to see if they are all unique as well. If anything is not, this will chain back up the list to return false.
            if (cell.isSubMenuCell() && cell.getSubCells().size() > 0) {
                boolean subCellsAreUnique = menuCellsAreUnique(cell.getSubCells(), allVoiceCommands);

                if (!subCellsAreUnique) {
                    DebugTool.logError(TAG, "Not all subCells are unique. The menu will not be set.");
                    return false;
                }
            }

            // Voice commands have to be identical across all lists
            if (cell.getVoiceCommands() != null) {
                allVoiceCommands.addAll(cell.getVoiceCommands());
            }
        }

        // Check for duplicate cells
        if (identicalCellsCheckSet.size() != cells.size()) {
            DebugTool.logError(TAG, "Not all cells are unique. Cells in each list (such as main menu or sub cell list) must have some differentiating property other than the sub cells within a cell. The menu will not be set.");
            return false;
        }

        // All the VR commands must be unique
        HashSet voiceCommandsSet = new HashSet<>(allVoiceCommands);
        if (allVoiceCommands.size() != voiceCommandsSet.size()) {
            DebugTool.logError(TAG, "Attempted to create a menu with duplicate voice commands. Voice commands must be unique. The menu will not be set");
            return false;
        }

        return true;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy