MenuHostHelper.java

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.view;

import android.annotation.SuppressLint;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Helper class for implementing {@link MenuHost}.
 *
 * This class should be used to implement the {@link MenuHost} functions. i.e.:
 *
 * <pre class="prettyprint">
 * class ExampleComponent : MenuHost {
 *
 *     private val menuHostHelper = MenuHostHelper{ invalidateMenu() }
 *
 *     override fun invalidateMenu() { … }
 *
 *     override fun addMenuProvider(provider: MenuProvider, owner: LifecycleOwner) {
 *         menuHostHelper.addMenuProvider(provider, owner)
 *     }
 *
 *     // Override remaining MenuHost methods in similar fashion
 * }
 * </pre>
 */
public class MenuHostHelper {

    private final Runnable mOnInvalidateMenuCallback;
    private final CopyOnWriteArrayList<MenuProvider> mMenuProviders = new CopyOnWriteArrayList<>();
    private final Map<MenuProvider, LifecycleContainer> mProviderToLifecycleContainers =
            new HashMap<>();

    /**
     * Construct a new MenuHostHelper.
     *
     * @param onInvalidateMenuCallback callback to invalidate the menu
     *                                 whenever there may be a change to it
     */
    public MenuHostHelper(@NonNull Runnable onInvalidateMenuCallback) {
        mOnInvalidateMenuCallback = onInvalidateMenuCallback;
    }

    /**
     * Called right before the given {@link Menu}, which was provided by one of the
     * current {@link MenuProvider}s, is to be shown. This happens when the menu has
     * been dynamically modified.
     *
     * @param menu the menu that is to be prepared
     * @see #onCreateMenu(Menu, MenuInflater)
     */
    public void onPrepareMenu(@NonNull Menu menu) {
        for (MenuProvider menuProvider : mMenuProviders) {
            menuProvider.onPrepareMenu(menu);
        }
    }

    /**
     * Inflates the entire {@link Menu}, which will include all
     * {@link MenuItem}s provided by all current {@link MenuProvider}s.
     *
     * @param menu         the menu to inflate all the menu items into
     * @param menuInflater the inflater to be used to inflate the menu
     */
    public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
        for (MenuProvider menuProvider : mMenuProviders) {
            menuProvider.onCreateMenu(menu, menuInflater);
        }
    }

    /**
     * Called whenever one of the menu items from any of the current
     * {@link MenuProvider}s is selected.
     *
     * @param item the menu item that was selected
     * @return {@code true} to indicate the menu processing was consumed
     * by one of the {@link MenuProvider}s, {@code false} otherwise.
     */
    public boolean onMenuItemSelected(@NonNull MenuItem item) {
        for (MenuProvider menuProvider : mMenuProviders) {
            if (menuProvider.onMenuItemSelected(item)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Called when the given {@link Menu}, which was provided by one of the
     * current {@link MenuProvider}s, is closed.
     *
     * @param menu the menu that has been closed
     */
    public void onMenuClosed(@NonNull Menu menu) {
        for (MenuProvider menuProvider : mMenuProviders) {
            menuProvider.onMenuClosed(menu);
        }
    }

    /**
     * Adds the given {@link MenuProvider} to the helper.
     *
     * @param provider the MenuProvider to be added
     */
    public void addMenuProvider(@NonNull MenuProvider provider) {
        mMenuProviders.add(provider);
        mOnInvalidateMenuCallback.run();
    }

    /**
     * Adds the given {@link MenuProvider} to the helper.
     *
     * This {@link MenuProvider} will be removed once the given {@link LifecycleOwner}
     * receives an {@link Lifecycle.Event.ON_DESTROY} event.
     *
     * @param provider the MenuProvider to be added
     * @param owner    the Lifecycle owner whose state will determine the removal of the provider
     */
    public void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner) {
        addMenuProvider(provider);
        Lifecycle lifecycle = owner.getLifecycle();
        LifecycleContainer lifecycleContainer = mProviderToLifecycleContainers.remove(provider);
        if (lifecycleContainer != null) {
            lifecycleContainer.clearObservers();
        }
        LifecycleEventObserver observer = (source, event) -> {
            if (event == Lifecycle.Event.ON_DESTROY) {
                removeMenuProvider(provider);
            }
        };
        mProviderToLifecycleContainers.put(provider, new LifecycleContainer(lifecycle, observer));
    }

    /**
     * Adds the given {@link MenuProvider} to the helper once the given
     * {@link LifecycleOwner} reaches the given {@link Lifecycle.State}.
     *
     * This {@link MenuProvider} will be removed once the given {@link LifecycleOwner}
     * goes down from the given {@link Lifecycle.State} or receives an
     * {@link Lifecycle.Event.ON_DESTROY} event.
     *
     * @param provider the MenuProvider to be added
     * @param state    the Lifecycle.State to check for automated addition/removal
     * @param owner    the Lifecycle owner whose state will determine the removal of the provider
     */
    @SuppressLint("LambdaLast")
    public void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner,
            @NonNull Lifecycle.State state) {
        Lifecycle lifecycle = owner.getLifecycle();
        LifecycleContainer lifecycleContainer = mProviderToLifecycleContainers.remove(provider);
        if (lifecycleContainer != null) {
            lifecycleContainer.clearObservers();
        }
        LifecycleEventObserver observer = (source, event) -> {
            if (event == Lifecycle.Event.upTo(state)) {
                addMenuProvider(provider);
            } else if (event == Lifecycle.Event.ON_DESTROY) {
                removeMenuProvider(provider);
            } else if (event == Lifecycle.Event.downFrom(state)) {
                mMenuProviders.remove(provider);
                mOnInvalidateMenuCallback.run();
            }
        };
        mProviderToLifecycleContainers.put(provider, new LifecycleContainer(lifecycle, observer));
    }

    /**
     * Removes the given {@link MenuProvider} from the helper.
     *
     * @param provider the MenuProvider to be removed
     */
    public void removeMenuProvider(@NonNull MenuProvider provider) {
        mMenuProviders.remove(provider);
        LifecycleContainer lifecycleContainer = mProviderToLifecycleContainers.remove(provider);
        if (lifecycleContainer != null) {
            lifecycleContainer.clearObservers();
        }
        mOnInvalidateMenuCallback.run();
    }

    private static class LifecycleContainer {
        final Lifecycle mLifecycle;
        private LifecycleEventObserver mObserver;

        LifecycleContainer(@NonNull Lifecycle lifecycle, @NonNull LifecycleEventObserver observer) {
            mLifecycle = lifecycle;
            mObserver = observer;
            mLifecycle.addObserver(observer);
        }

        void clearObservers() {
            mLifecycle.removeObserver(mObserver);
            mObserver = null;
        }
    }
}