/*
 * Copyright (C) 2013 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 com.android.documentsui.sidebar;

import static com.android.documentsui.base.Shared.compareToIgnoreCaseNullable;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.base.SharedMinimal.VERBOSE;

import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.DragEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnDragListener;
import android.view.View.OnGenericMotionListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ListView;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.loader.app.LoaderManager;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;

import com.android.documentsui.ActionHandler;
import com.android.documentsui.BaseActivity;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.DragHoverListener;
import com.android.documentsui.Injector;
import com.android.documentsui.Injector.Injected;
import com.android.documentsui.ItemDragListener;
import com.android.documentsui.R;
import com.android.documentsui.UserPackage;
import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.Events;
import com.android.documentsui.base.Features;
import com.android.documentsui.base.Providers;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.base.UserId;
import com.android.documentsui.roots.ProvidersAccess;
import com.android.documentsui.roots.ProvidersCache;
import com.android.documentsui.roots.RootsLoader;
import com.android.documentsui.util.CrossProfileUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Display list of known storage backend roots.
 */
public class RootsFragment extends Fragment {

    private static final String TAG = "RootsFragment";
    private static final String EXTRA_INCLUDE_APPS = "includeApps";
    private static final String EXTRA_INCLUDE_APPS_INTENT = "includeAppsIntent";
    private static final int CONTEXT_MENU_ITEM_TIMEOUT = 500;

    private final OnItemClickListener mItemListener = new OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            final Item item = mAdapter.getItem(position);
            item.open();

            getBaseActivity().setRootsDrawerOpen(false);
        }
    };

    private final OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
        @Override
        public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
            final Item item = mAdapter.getItem(position);
            return item.showAppDetails();
        }
    };

    private ListView mList;
    private RootsAdapter mAdapter;
    private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
    private @Nullable OnDragListener mDragListener;

    @Injected
    private Injector<?> mInjector;

    @Injected
    private ActionHandler mActionHandler;

    private List<Item> mApplicationItemList;

    /**
     * Shows the {@link RootsFragment}.
     * @param fm the FragmentManager for interacting with fragments associated with this
     *           fragment's activity
     * @param includeApps if {@code true}, query the intent from the system and include apps in
     *                    the {@RootsFragment}.
     * @param intent the intent to query for package manager
     */
    public static RootsFragment show(FragmentManager fm, boolean includeApps, Intent intent) {
        final Bundle args = new Bundle();
        args.putBoolean(EXTRA_INCLUDE_APPS, includeApps);
        args.putParcelable(EXTRA_INCLUDE_APPS_INTENT, intent);

        final RootsFragment fragment = new RootsFragment();
        fragment.setArguments(args);

        final FragmentTransaction ft = fm.beginTransaction();
        ft.replace(R.id.container_roots, fragment);
        ft.commitAllowingStateLoss();

        return fragment;
    }

    public static RootsFragment get(FragmentManager fm) {
        return (RootsFragment) fm.findFragmentById(R.id.container_roots);
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        mInjector = getBaseActivity().getInjector();

        final View view = inflater.inflate(R.layout.fragment_roots, container, false);
        mList = (ListView) view.findViewById(R.id.roots_list);
        mList.setOnItemClickListener(mItemListener);
        // ListView does not have right-click specific listeners, so we will have a
        // GenericMotionListener to listen for it.
        // Currently, right click is viewed the same as long press, so we will have to quickly
        // register for context menu when we receive a right click event, and quickly unregister
        // it afterwards to prevent context menus popping up upon long presses.
        // All other motion events will then get passed to OnItemClickListener.
        mList.setOnGenericMotionListener(
                new OnGenericMotionListener() {
                    @Override
                    public boolean onGenericMotion(View v, MotionEvent event) {
                        if (Events.isMouseEvent(event)
                                && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
                            int x = (int) event.getX();
                            int y = (int) event.getY();
                            return onRightClick(v, x, y, () -> {
                                mInjector.menuManager.showContextMenu(
                                        RootsFragment.this, v, x, y);
                            });
                        }
                        return false;
            }
        });
        mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        mList.setSelector(new ColorDrawable(Color.TRANSPARENT));
        return view;
    }

    private boolean onRightClick(View v, int x, int y, Runnable callback) {
        final int pos = mList.pointToPosition(x, y);
        final Item item = mAdapter.getItem(pos);

        // If a read-only root, no need to see if top level is writable (it's not)
        if (!(item instanceof RootItem) || !((RootItem) item).root.supportsCreate()) {
            return false;
        }

        final RootItem rootItem = (RootItem) item;
        getRootDocument(rootItem, (DocumentInfo doc) -> {
            rootItem.docInfo = doc;
            callback.run();
        });
        return true;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        final BaseActivity activity = getBaseActivity();
        final ProvidersCache providers = DocumentsApplication.getProvidersCache(activity);
        final State state = activity.getDisplayState();

        mActionHandler = mInjector.actions;

        if (mInjector.config.dragAndDropEnabled()) {
            final DragHost host = new DragHost(
                    activity,
                    DocumentsApplication.getDragAndDropManager(activity),
                    this::getItem,
                    mActionHandler);
            final ItemDragListener<DragHost> listener = new ItemDragListener<DragHost>(host) {
                @Override
                public boolean handleDropEventChecked(View v, DragEvent event) {
                    final Item item = getItem(v);

                    assert (item.isRoot());

                    return item.dropOn(event);
                }
            };
            mDragListener = DragHoverListener.create(listener, mList);
            mList.setOnDragListener(mDragListener);
        }

        mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
            @Override
            public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) {
                return new RootsLoader(activity, providers, state);
            }

            @Override
            public void onLoadFinished(
                    Loader<Collection<RootInfo>> loader, Collection<RootInfo> roots) {
                if (!isAdded()) {
                    return;
                }

                boolean shouldIncludeHandlerApp = getArguments().getBoolean(EXTRA_INCLUDE_APPS,
                        /* defaultValue= */ false);
                Intent handlerAppIntent = getArguments().getParcelable(EXTRA_INCLUDE_APPS_INTENT);

                final Intent intent = activity.getIntent();
                final boolean excludeSelf =
                        intent.getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false);
                final String excludePackage = excludeSelf ? activity.getCallingPackage() : null;
                final boolean maybeShowBadge =
                        getBaseActivity().getDisplayState().supportsCrossProfile();

                // For action which supports cross profile, update the policy value in state if
                // necessary.
                ResolveInfo crossProfileResolveInfo = null;
                if (state.supportsCrossProfile() && handlerAppIntent != null) {
                    crossProfileResolveInfo = CrossProfileUtils.getCrossProfileResolveInfo(
                            getContext().getPackageManager(), handlerAppIntent);
                    updateCrossProfileStateAndMaybeRefresh(
                            /* canShareAcrossProfile= */ crossProfileResolveInfo != null);
                }

                List<Item> sortedItems = sortLoadResult(
                        getContext(),
                        state,
                        roots,
                        excludePackage,
                        shouldIncludeHandlerApp ? handlerAppIntent : null,
                        DocumentsApplication.getProvidersCache(getContext()),
                        getBaseActivity().getSelectedUser(),
                        DocumentsApplication.getUserIdManager(getContext()).getUserIds(),
                        maybeShowBadge);

                // This will be removed when feature flag is removed.
                if (crossProfileResolveInfo != null && !Features.CROSS_PROFILE_TABS) {
                    // Add profile item if we don't support cross-profile tab.
                    sortedItems.add(new SpacerItem());
                    sortedItems.add(new ProfileItem(crossProfileResolveInfo,
                            crossProfileResolveInfo.loadLabel(
                                    getContext().getPackageManager()).toString(),
                            mActionHandler));
                }

                // Disable drawer if only one root
                activity.setRootsDrawerLocked(sortedItems.size() <= 1);

                // Get the first visible position and offset
                final int firstPosition = mList.getFirstVisiblePosition();
                View firstChild = mList.getChildAt(0);
                final int offset =
                        firstChild != null ? firstChild.getTop() - mList.getPaddingTop() : 0;
                final int oriItemCount = mAdapter != null ? mAdapter.getCount() : 0;
                mAdapter = new RootsAdapter(activity, sortedItems, mDragListener);
                mList.setAdapter(mAdapter);

                // recover the position.
                if (oriItemCount == mAdapter.getCount()) {
                    mList.setSelectionFromTop(firstPosition, offset);
                }

                mInjector.shortcutsUpdater.accept(roots);
                mInjector.appsRowManager.updateList(mApplicationItemList);
                mInjector.appsRowManager.updateView(activity);
                onCurrentRootChanged();
            }

            @Override
            public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
                mAdapter = null;
                mList.setAdapter(null);
            }
        };
    }

    /**
     * Updates the state values of whether we can share across profiles, if necessary. Also reload
     * documents stack if the selected user is not the current user.
     */
    private void updateCrossProfileStateAndMaybeRefresh(boolean canShareAcrossProfile) {
        final State state = getBaseActivity().getDisplayState();
        if (state.canShareAcrossProfile != canShareAcrossProfile) {
            state.canShareAcrossProfile = canShareAcrossProfile;
            if (!UserId.CURRENT_USER.equals(getBaseActivity().getSelectedUser())) {
                mActionHandler.loadDocumentsForCurrentStack();
            }
        }
    }

    /**
     * If the package name of other providers or apps capable of handling the original intent
     * include the preferred root source, it will have higher order than others.
     * @param excludePackage Exclude activities from this given package
     * @param handlerAppIntent When not null, apps capable of handling the original intent will
     *            be included in list of roots (in special section at bottom).
     */
    @VisibleForTesting
    List<Item> sortLoadResult(
            Context context,
            State state,
            Collection<RootInfo> roots,
            @Nullable String excludePackage,
            @Nullable Intent handlerAppIntent,
            ProvidersAccess providersAccess,
            UserId selectedUser,
            List<UserId> userIds,
            boolean maybeShowBadge) {
        final List<Item> result = new ArrayList<>();

        final RootItemListBuilder librariesBuilder = new RootItemListBuilder(selectedUser, userIds);
        final RootItemListBuilder storageProvidersBuilder = new RootItemListBuilder(selectedUser,
                userIds);
        final List<RootItem> otherProviders = new ArrayList<>();

        for (final RootInfo root : roots) {
            final RootItem item;

            if (root.isExternalStorageHome()) {
                continue;
            } else if (root.isLibrary() || root.isDownloads()) {
                item = new RootItem(root, mActionHandler, maybeShowBadge);
                librariesBuilder.add(item);
            } else if (root.isStorage()) {
                item = new RootItem(root, mActionHandler, maybeShowBadge);
                storageProvidersBuilder.add(item);
            } else {
                item = new RootItem(root, mActionHandler,
                        providersAccess.getPackageName(root.userId, root.authority),
                        maybeShowBadge);
                otherProviders.add(item);
            }
        }

        final List<RootItem> libraries = librariesBuilder.getList();
        final List<RootItem> storageProviders = storageProvidersBuilder.getList();

        final RootComparator comp = new RootComparator();
        Collections.sort(libraries, comp);
        Collections.sort(storageProviders, comp);

        if (VERBOSE) Log.v(TAG, "Adding library roots: " + libraries);
        result.addAll(libraries);

        // Only add the spacer if it is actually separating something.
        if (!result.isEmpty() && !storageProviders.isEmpty()) {
            result.add(new SpacerItem());
        }
        if (VERBOSE) Log.v(TAG, "Adding storage roots: " + storageProviders);
        result.addAll(storageProviders);

        final List<Item> rootList = new ArrayList<>();
        final List<Item> rootListOtherUser = new ArrayList<>();
        mApplicationItemList = new ArrayList<>();
        if (handlerAppIntent != null) {
            includeHandlerApps(state, handlerAppIntent, excludePackage, rootList, rootListOtherUser,
                    otherProviders, userIds, maybeShowBadge);
        } else {
            // Only add providers
            Collections.sort(otherProviders, comp);
            for (RootItem item : otherProviders) {
                if (UserId.CURRENT_USER.equals(item.userId)) {
                    rootList.add(item);
                } else {
                    rootListOtherUser.add(item);
                }
                mApplicationItemList.add(item);
            }
        }

        List<Item> presentableList = new UserItemsCombiner(
                context.getResources(), context.getSystemService(DevicePolicyManager.class), state)
                .setRootListForCurrentUser(rootList)
                .setRootListForOtherUser(rootListOtherUser)
                .createPresentableList();
        addListToResult(result, presentableList);
        return result;
    }

    private void addListToResult(List<Item> result, List<Item> rootList) {
        if (!result.isEmpty() && !rootList.isEmpty()) {
            result.add(new SpacerItem());
        }
        result.addAll(rootList);
    }

    /**
     * Adds apps capable of handling the original intent will be included in list of roots. If
     * the providers and apps are the same package name, combine them as RootAndAppItems.
     */
    private void includeHandlerApps(State state,
            Intent handlerAppIntent, @Nullable String excludePackage, List<Item> rootList,
            List<Item> rootListOtherUser, List<RootItem> otherProviders, List<UserId> userIds,
            boolean maybeShowBadge) {
        if (VERBOSE) Log.v(TAG, "Adding handler apps for intent: " + handlerAppIntent);

        Context context = getContext();
        final Map<UserPackage, ResolveInfo> appsMapping = new HashMap<>();
        final Map<UserPackage, Item> appItems = new HashMap<>();

        final String myPackageName = context.getPackageName();
        for (UserId userId : userIds) {
            final PackageManager pm = userId.getPackageManager(context);
            final List<ResolveInfo> infos = pm.queryIntentActivities(
                    handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY);

            // Omit ourselves and maybe calling package from the list
            for (ResolveInfo info : infos) {
                if (!info.activityInfo.exported) {
                    if (VERBOSE) {
                        Log.v(TAG, "Non exported activity: " + info.activityInfo);
                    }
                    continue;
                }

                final String packageName = info.activityInfo.packageName;
                if (!myPackageName.equals(packageName)
                        && !TextUtils.equals(excludePackage, packageName)) {
                    UserPackage userPackage = new UserPackage(userId, packageName);
                    appsMapping.put(userPackage, info);

                    if (!CrossProfileUtils.isCrossProfileIntentForwarderActivity(info)) {
                        final Item item = new AppItem(info, info.loadLabel(pm).toString(), userId,
                                mActionHandler);
                        appItems.put(userPackage, item);
                        if (VERBOSE) Log.v(TAG, "Adding handler app: " + item);
                    }
                }
            }
        }

        // If there are some providers and apps has the same package name, combine them as one item.
        for (RootItem rootItem : otherProviders) {
            final UserPackage userPackage = new UserPackage(rootItem.userId,
                    rootItem.getPackageName());
            final ResolveInfo resolveInfo = appsMapping.get(userPackage);

            final Item item;
            if (resolveInfo != null) {
                item = new RootAndAppItem(rootItem.root, resolveInfo, mActionHandler,
                        maybeShowBadge);
                appItems.remove(userPackage);
            } else {
                item = rootItem;
            }

            if (UserId.CURRENT_USER.equals(item.userId)) {
                if (VERBOSE) Log.v(TAG, "Adding provider : " + item);
                rootList.add(item);
            } else {
                if (VERBOSE) Log.v(TAG, "Adding provider to other users : " + item);
                rootListOtherUser.add(item);
            }
        }

        for (Item item : appItems.values()) {
            if (UserId.CURRENT_USER.equals(item.userId)) {
                rootList.add(item);
            } else {
                rootListOtherUser.add(item);
            }
        }

        final String preferredRootPackage = getResources().getString(
                R.string.preferred_root_package, "");
        final ItemComparator comp = new ItemComparator(preferredRootPackage);
        Collections.sort(rootList, comp);
        Collections.sort(rootListOtherUser, comp);

        if (state.supportsCrossProfile() && state.canShareAcrossProfile) {
            mApplicationItemList.addAll(rootList);
            mApplicationItemList.addAll(rootListOtherUser);
        } else {
            mApplicationItemList.addAll(rootList);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        final Context context = getActivity();
        // Update the information for Storage's root
        if (context != null) {
            DocumentsApplication.getProvidersCache(context).updateAuthorityAsync(
                    ((BaseActivity) context).getSelectedUser(), Providers.AUTHORITY_STORAGE);
        }
        onDisplayStateChanged();
    }

    public void onDisplayStateChanged() {
        final Context context = getActivity();
        final State state = ((BaseActivity) context).getDisplayState();

        if (state.action == State.ACTION_GET_CONTENT) {
            mList.setOnItemLongClickListener(mItemLongClickListener);
        } else {
            mList.setOnItemLongClickListener(null);
            mList.setLongClickable(false);
        }

        LoaderManager.getInstance(this).restartLoader(2, null, mCallbacks);
    }

    public void onCurrentRootChanged() {
        if (mAdapter == null) {
            return;
        }

        final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot();
        for (int i = 0; i < mAdapter.getCount(); i++) {
            final Object item = mAdapter.getItem(i);
            if (item instanceof RootItem) {
                final RootInfo testRoot = ((RootItem) item).root;
                if (Objects.equals(testRoot, root)) {
                    // b/37358441 should reload all root title after configuration changed
                    root.title = testRoot.title;
                    mList.setItemChecked(i, true);
                    return;
                }
            }
        }
    }

    /**
     * Called when the selected user is changed. It reloads roots with the current user.
     */
    public void onSelectedUserChanged() {
        LoaderManager.getInstance(this).restartLoader(/* id= */ 2, /* args= */ null, mCallbacks);
    }

    /**
     * Attempts to shift focus back to the navigation drawer.
     */
    public boolean requestFocus() {
        return mList.requestFocus();
    }

    private BaseActivity getBaseActivity() {
        return (BaseActivity) getActivity();
    }

    @Override
    public void onCreateContextMenu(
            ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo;
        final Item item = mAdapter.getItem(adapterMenuInfo.position);

        BaseActivity activity = getBaseActivity();
        item.createContextMenu(menu, activity.getMenuInflater(), mInjector.menuManager);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) item.getMenuInfo();
        // There is a possibility that this is called from DirectoryFragment since
        // all fragments' onContextItemSelected gets called when any menu item is selected
        // This is to guard against it since DirectoryFragment's RecylerView does not have a
        // menuInfo
        if (adapterMenuInfo == null) {
            return false;
        }
        final RootItem rootItem = (RootItem) mAdapter.getItem(adapterMenuInfo.position);
        switch (item.getItemId()) {
            case R.id.root_menu_eject_root:
                final View ejectIcon = adapterMenuInfo.targetView.findViewById(R.id.action_icon);
                ejectClicked(ejectIcon, rootItem.root, mActionHandler);
                return true;
            case R.id.root_menu_open_in_new_window:
                mActionHandler.openInNewWindow(new DocumentStack(rootItem.root));
                return true;
            case R.id.root_menu_paste_into_folder:
                mActionHandler.pasteIntoFolder(rootItem.root);
                return true;
            case R.id.root_menu_settings:
                mActionHandler.openSettings(rootItem.root);
                return true;
            default:
                if (DEBUG) {
                    Log.d(TAG, "Unhandled menu item selected: " + item);
                }
                return false;
        }
    }

    private void getRootDocument(RootItem rootItem, RootUpdater updater) {
        // We need to start a GetRootDocumentTask so we can know whether items can be directly
        // pasted into root
        mActionHandler.getRootDocument(
                rootItem.root,
                CONTEXT_MENU_ITEM_TIMEOUT,
                (DocumentInfo doc) -> {
                    updater.updateDocInfoForRoot(doc);
                });
    }

    private Item getItem(View v) {
        final int pos = (Integer) v.getTag(R.id.item_position_tag);
        return mAdapter.getItem(pos);
    }

    static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) {
        assert(ejectIcon != null);
        assert(!root.ejecting);
        ejectIcon.setEnabled(false);
        root.ejecting = true;
        actionHandler.ejectRoot(
                root,
                new BooleanConsumer() {
                    @Override
                    public void accept(boolean ejected) {
                        // Event if ejected is false, we should reset, since the op failed.
                        // Either way, we are no longer attempting to eject the device.
                        root.ejecting = false;

                        // If the view is still visible, we update its state.
                        if (ejectIcon.getVisibility() == View.VISIBLE) {
                            ejectIcon.setEnabled(!ejected);
                        }
                    }
                });
    }

    private static class RootComparator implements Comparator<RootItem> {
        @Override
        public int compare(RootItem lhs, RootItem rhs) {
            return lhs.root.compareTo(rhs.root);
        }
    }

    /**
     * The comparator of {@link AppItem}, {@link RootItem} and {@link RootAndAppItem}.
     * Sort by if the item's package name starts with the preferred package name,
     * then title, then summary. Because the {@link AppItem} doesn't have summary,
     * it will have lower order than other same title items.
     */
    @VisibleForTesting
    static class ItemComparator implements Comparator<Item> {
        private final String mPreferredPackageName;

        ItemComparator(String preferredPackageName) {
            mPreferredPackageName = preferredPackageName;
        }

        @Override
        public int compare(Item lhs, Item rhs) {
            // Sort by whether the item starts with preferred package name
            if (!mPreferredPackageName.isEmpty()) {
                if (lhs.getPackageName().startsWith(mPreferredPackageName)) {
                    if (!rhs.getPackageName().startsWith(mPreferredPackageName)) {
                        // lhs starts with it, but rhs doesn't start with it
                        return -1;
                    }
                } else {
                    if (rhs.getPackageName().startsWith(mPreferredPackageName)) {
                        // lhs doesn't start with it, but rhs starts with it
                        return 1;
                    }
                }
            }

            // Sort by title
            int score = compareToIgnoreCaseNullable(lhs.title, rhs.title);
            if (score != 0) {
                return score;
            }

            // Sort by summary. If the item is AppItem, it doesn't have summary.
            // So, the RootItem or RootAndAppItem will have higher order than AppItem.
            if (lhs instanceof RootItem) {
                return rhs instanceof RootItem ? compareToIgnoreCaseNullable(
                        ((RootItem) lhs).root.summary, ((RootItem) rhs).root.summary) : 1;
            }
            return rhs instanceof RootItem ? -1 : 0;
        }
    }

    @FunctionalInterface
    interface RootUpdater {
        void updateDocInfoForRoot(DocumentInfo doc);
    }
}
