Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 04718263 authored by Steve McKay's avatar Steve McKay
Browse files

Shared input handling and injection.

Change-Id: I41ae072e55ecc60b708274b5c67bed3a486bf080
parent 30535bce
Loading
Loading
Loading
Loading
+11 −115
Original line number Diff line number Diff line
@@ -37,40 +37,29 @@ import android.support.annotation.CallSuper;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;

import com.android.documentsui.AbstractActionHandler.CommonAddons;
import com.android.documentsui.MenuManager.SelectionDetails;
import com.android.documentsui.NavigationViewManager.Breadcrumb;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.Events;
import com.android.documentsui.base.LocalPreferences;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.ScopedPreferences;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
import com.android.documentsui.base.State.ViewMode;
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.documentsui.dirlist.DocumentsAdapter;
import com.android.documentsui.dirlist.Model;
import com.android.documentsui.queries.SearchViewManager;
import com.android.documentsui.queries.SearchViewManager.SearchManagerListener;
import com.android.documentsui.roots.GetRootDocumentTask;
import com.android.documentsui.roots.RootsCache;
import com.android.documentsui.selection.Selection;
import com.android.documentsui.selection.SelectionManager;
import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
import com.android.documentsui.sidebar.RootsFragment;
import com.android.documentsui.sorting.SortController;
import com.android.documentsui.sorting.SortModel;
import com.android.documentsui.ui.DialogController;
import com.android.documentsui.ui.MessageBuilder;

import java.util.ArrayList;
@@ -79,7 +68,8 @@ import java.util.List;
import java.util.concurrent.Executor;

public abstract class BaseActivity<T extends ActionHandler>
        extends Activity implements CommonAddons, NavigationViewManager.Environment {
        extends Activity
        implements CommonAddons, Injector, NavigationViewManager.Environment {

    private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests";

@@ -104,7 +94,6 @@ public abstract class BaseActivity<T extends ActionHandler>

    private RootsMonitor<BaseActivity<?>> mRootsMonitor;

    private boolean mNavDrawerHasFocus;
    private long mStartTime;

    public BaseActivity(@LayoutRes int layoutId, String tag) {
@@ -118,54 +107,8 @@ public abstract class BaseActivity<T extends ActionHandler>
    protected abstract void includeState(State initialState);
    protected abstract void onDirectoryCreated(DocumentInfo doc);

    /**
     * Provides Activity a means of injection into and specialization of
     * DirectoryFragment.
     */
    public abstract ActivityConfig getActivityConfig();

    /**
     * Provides Activity a means of injection into and specialization of
     * DirectoryFragment.
     */
    public abstract ScopedPreferences getScopedPreferences();

    /**
     * Provides Activity a means of injection into and specialization of
     * DirectoryFragment.
     */
    public abstract SelectionManager getSelectionManager(
            DocumentsAdapter adapter, SelectionPredicate canSetState);

    /**
     * Provides Activity a means of injection into and specialization of
     * DirectoryFragment hosted menus.
     */
    public abstract MenuManager getMenuManager();

    /**
     * Provides Activity a means of injection into and specialization of
     * DirectoryFragment.
     */
    public abstract DialogController getDialogController();

    /**
     * Provides Activity a means of injection into and specialization of
     * fragment actions.
     *
     * Args can be null when called from a context lacking fragment, such as RootsFragment.
     */
    public abstract ActionHandler getActionHandler(@Nullable Model model, boolean searchMode);

    /**
     * Provides Activity a means of injection into and specialization of
     * DirectoryFragment.
     */
    public abstract ActionModeController getActionModeController(
            SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker, View view);


    public abstract FocusManager getFocusManager(RecyclerView view, Model model);
    // Get ref to to focus manager without reset. Presumes it has had scrope vars initialized.
    protected abstract FocusManager getFocusManager();

    public final MessageBuilder getMessages() {
        assert(mMessages != null);
@@ -607,37 +550,6 @@ public abstract class BaseActivity<T extends ActionHandler>
        super.onBackPressed();
    }

    /**
     * Declare a global key handler to route key events when there isn't a specific focus view. This
     * covers the scenario where a user opens DocumentsUI and just starts typing.
     *
     * @param keyCode
     * @param event
     * @return
     */
    @CallSuper
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (Events.isNavigationKeyCode(keyCode)) {
            // Forward all unclaimed navigation keystrokes to the DirectoryFragment. This causes any
            // stray navigation keystrokes focus the content pane, which is probably what the user
            // is trying to do.
            DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
            if (df != null) {
                df.requestFocus();
                return true;
            }
        } else if (keyCode == KeyEvent.KEYCODE_TAB) {
            // Tab toggles focus on the navigation drawer.
            toggleNavDrawerFocus();
            return true;
        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
            popDir();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    @VisibleForTesting
    public void addEventListener(EventListener listener) {
        mEventListeners.add(listener);
@@ -663,35 +575,13 @@ public abstract class BaseActivity<T extends ActionHandler>
        }
    }

    /**
     * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't
     * locked, open/close it as appropriate.
     */
    void toggleNavDrawerFocus() {
        boolean toogleHappened = false;
        if (mNavDrawerHasFocus) {
            mDrawer.setOpen(false);
            DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
            assert (df != null);
            toogleHappened = df.requestFocus();
        } else {
            mDrawer.setOpen(true);
            RootsFragment rf = RootsFragment.get(getFragmentManager());
            assert (rf != null);
            toogleHappened = rf.requestFocus();
        }
        if (toogleHappened) {
            mNavDrawerHasFocus = !mNavDrawerHasFocus;
        }
    }

    /**
     * Pops the top entry off the directory stack, and returns the user to the previous directory.
     * If the directory stack only contains one item, this method does nothing.
     *
     * @return Whether the stack was popped.
     */
    private boolean popDir() {
    protected boolean popDir() {
        if (mState.stack.size() > 1) {
            mState.stack.pop();
            refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
@@ -700,6 +590,12 @@ public abstract class BaseActivity<T extends ActionHandler>
        return false;
    }

    protected boolean focusRoots() {
        RootsFragment rf = RootsFragment.get(getFragmentManager());
        assert (rf != null);
        return rf.requestFocus();
    }

    /**
     * Closes the activity when it's idle.
     */
+87 −48
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import android.widget.TextView;

import com.android.documentsui.base.EventListener;
import com.android.documentsui.base.Events;
import com.android.documentsui.base.Procedure;
import com.android.documentsui.dirlist.DocumentHolder;
import com.android.documentsui.dirlist.DocumentsAdapter;
import com.android.documentsui.dirlist.FocusHandler;
@@ -53,21 +54,51 @@ import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

/**
 * A class that handles navigation and focus within the DirectoryFragment.
 */
/** A class that handles navigation and focus within the DirectoryFragment. */
public final class FocusManager implements FocusHandler {
    private static final String TAG = "FocusManager";

    private final ContentScope mScope = new ContentScope();
    private final TitleSearchHelper mSearchHelper;

    private final SelectionManager mSelectionMgr;
    private final DrawerController mDrawer;
    private final Procedure mRootsFocuser;
    private final TitleSearchHelper mSearchHelper;

    private boolean mNavDrawerHasFocus;

    public FocusManager(
            SelectionManager selectionMgr,
            DrawerController drawer,
            Procedure rootsFocuser,
            @ColorRes int color) {

    public FocusManager(@ColorRes int color, SelectionManager selectionMgr) {
        mSelectionMgr = selectionMgr;
        mDrawer = drawer;
        mRootsFocuser = rootsFocuser;

        mSearchHelper = new TitleSearchHelper(color);
    }

    @Override
    public boolean advanceFocusArea() {
        boolean toogleHappened = false;
        if (mNavDrawerHasFocus) {
            mDrawer.setOpen(false);
            focusDirectoryList();
        } else {
            mDrawer.setOpen(true);
            toogleHappened = mRootsFocuser.run();
        }

        if (toogleHappened) {
            mNavDrawerHasFocus = !mNavDrawerHasFocus;
            return true;
        }

        return false;
    }

    @Override
    public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
        // Search helper gets first crack, for doing type-to-focus.
@@ -98,22 +129,26 @@ public final class FocusManager implements FocusHandler {
    }

    @Override
    public boolean requestFocus() {
    public boolean focusDirectoryList() {
        if (mScope.adapter.getItemCount() == 0) {
            if (DEBUG) Log.v(TAG, "Nothing to focus.");
            if (DEBUG)
                Log.v(TAG, "Nothing to focus.");
            return false;
        }

        // If there's a selection going on, we don't want to grant user the ability to focus
        // on any individual item to prevent ambiguity in operations (Cut selection vs. Cut focused
        // on any individfocusSomethingual item to prevent ambiguity in operations (Cut selection
        // vs. Cut focused
        // item)
        if (mSelectionMgr.hasSelection()) {
            if (DEBUG) Log.v(TAG, "Existing selection found. No focus will be done.");
            if (DEBUG)
                Log.v(TAG, "Existing selection found. No focus will be done.");
            return false;
        }

        final int focusPos = (mScope.lastFocusPosition != RecyclerView.NO_POSITION)
                ? mScope.lastFocusPosition : mScope.layout.findFirstVisibleItemPosition();
                ? mScope.lastFocusPosition
                : mScope.layout.findFirstVisibleItemPosition();
        focusItem(focusPos);
        return true;
    }
@@ -137,8 +172,8 @@ public final class FocusManager implements FocusHandler {

    /*
     * Attempts to put focus on the document associated with the given modelId. If item does not
     * exist yet in the layout, this sets a pending modelId to be used when
     * {@code #applyPendingFocus()} is called next time.
     * exist yet in the layout, this sets a pending modelId to be used when {@code
     * #applyPendingFocus()} is called next time.
     */
    @Override
    public void focusDocument(String modelId) {
@@ -244,13 +279,14 @@ public final class FocusManager implements FocusHandler {
    }

    /**
     * Given a PgUp/PgDn event and the current view, find the position of the target view.
     * This returns:
     * <li>The position of the topmost (or bottom-most) visible item, if the current item is not
     *     the top- or bottom-most visible item.
     * Given a PgUp/PgDn event and the current view, find the position of the target view. This
     * returns:
     * <li>The position of the topmost (or bottom-most) visible item, if the current item is not the
     * top- or bottom-most visible item.
     * <li>The position of an item that is one page's worth of items up (or down) if the current
     * item is the top- or bottom-most visible item.
     * <li>The first (or last) item, if paging up (or down) would go past those limits.
     *
     * @param view The view that received the key event.
     * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN.
     * @param event
@@ -305,7 +341,8 @@ public final class FocusManager implements FocusHandler {
     * @param pos
     * @param callback A callback to call after the given item has been focused.
     */
    private void focusItem(final int pos, @Nullable final FocusCallback callback) {
    private void focusItem(final int pos, @Nullable
    final FocusCallback callback) {
        if (mScope.pendingFocusId != null) {
            Log.v(TAG, "clearing pending focus id: " + mScope.pendingFocusId);
            mScope.pendingFocusId = null;
@@ -325,8 +362,8 @@ public final class FocusManager implements FocusHandler {
                        public void onScrollStateChanged(RecyclerView view, int newState) {
                            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                                // When scrolling stops, find the item and focus it.
                                RecyclerView.ViewHolder vh =
                                        view.findViewHolderForAdapterPosition(pos);
                                RecyclerView.ViewHolder vh = view
                                        .findViewHolderForAdapterPosition(pos);
                                if (vh != null) {
                                    if (vh.itemView.requestFocus() && callback != null) {
                                        callback.onFocus(vh.itemView);
@@ -345,9 +382,7 @@ public final class FocusManager implements FocusHandler {
        }
    }

    /**
     * @return Whether the layout manager is currently in a grid-configuration.
     */
    /** @return Whether the layout manager is currently in a grid-configuration. */
    private boolean inGridMode() {
        return mScope.layout.getSpanCount() > 1;
    }
@@ -364,7 +399,7 @@ public final class FocusManager implements FocusHandler {
     * highlights instances of the search term found in the view.
     */
    private class TitleSearchHelper {
        static private final int SEARCH_TIMEOUT = 500;  // ms
        private static final int SEARCH_TIMEOUT = 500; // ms

        private final KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false);
        private final Editable mSearchString = Editable.Factory.getInstance().newEditable("");
@@ -470,13 +505,18 @@ public final class FocusManager implements FocusHandler {
            for (int pos = 0; pos < mIndex.size(); pos++) {
                String title = mIndex.get(pos);
                if (title != null && title.startsWith(searchString)) {
                    focusItem(pos, new FocusCallback() {
                    focusItem(
                            pos,
                            new FocusCallback() {
                                @Override
                                public void onFocus(View view) {
                                    mHighlighter.applyHighlight(view);
                            // Using a timer repeat period of SEARCH_TIMEOUT/2 means the amount of
                            // time between the last keystroke and a search expiring is actually
                            // between 500 and 750 ms. A smaller timer period results in less
                                    // Using a timer repeat period of SEARCH_TIMEOUT/2 means the
                                    // amount of
                                    // time between the last keystroke and a search expiring is
                                    // actually
                                    // between 500 and 750 ms. A smaller timer period results in
                                    // less
                                    // variability but does more polling.
                                    mTimer.schedule(new TimeoutTask(), 0, SEARCH_TIMEOUT / 2);
                                }
@@ -486,9 +526,7 @@ public final class FocusManager implements FocusHandler {
            }
        }

        /**
         * Ends the current search (see {@link #search()}.
         */
        /** Ends the current search (see {@link #search()}. */
        private void endSearch() {
            if (mActive) {
                mScope.model.removeUpdateListener(mModelListener);
@@ -538,7 +576,8 @@ public final class FocusManager implements FocusHandler {
                long now = SystemClock.uptimeMillis();
                if ((now - last) > SEARCH_TIMEOUT) {
                    // endSearch must run on the main thread because it does UI work
                    mUiRunner.post(new Runnable() {
                    mUiRunner.post(
                            new Runnable() {
                                @Override
                                public void run() {
                                    endSearch();
@@ -552,8 +591,8 @@ public final class FocusManager implements FocusHandler {
            private Spannable mCurrentHighlight;

            /**
             * Applies title highlights to the given view. The view must have a title field that is a
             * spannable text field.  If this condition is not met, this function does nothing.
             * Applies title highlights to the given view. The view must have a title field that is
             * a spannable text field. If this condition is not met, this function does nothing.
             *
             * @param view
             */
@@ -575,8 +614,8 @@ public final class FocusManager implements FocusHandler {
            }

            /**
             * Removes title highlights from the given view. The view must have a title field that is a
             * spannable text field.  If this condition is not met, this function does nothing.
             * Removes title highlights from the given view. The view must have a title field that
             * is a spannable text field. If this condition is not met, this function does nothing.
             *
             * @param view
             */
+46 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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;

import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.MenuItem;
import android.view.View;

import com.android.documentsui.MenuManager.SelectionDetails;
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.ScopedPreferences;
import com.android.documentsui.dirlist.DocumentsAdapter;
import com.android.documentsui.dirlist.Model;
import com.android.documentsui.selection.SelectionManager;
import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
import com.android.documentsui.ui.DialogController;

/**
 * Provides access to runtime dependencies.
 */
public interface Injector {

    ActivityConfig getActivityConfig();
    ScopedPreferences getScopedPreferences();
    SelectionManager getSelectionManager(DocumentsAdapter adapter, SelectionPredicate canSetState);
    MenuManager getMenuManager();
    DialogController getDialogController();
    ActionHandler getActionHandler(@Nullable Model model, boolean searchMode);
    ActionModeController getActionModeController(
            SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker, View view);
    FocusManager getFocusManager(RecyclerView view, Model model);
}
+51 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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;

import android.view.KeyEvent;

import com.android.documentsui.base.Events;
import com.android.documentsui.base.Procedure;

public class SharedInputHandler {

    private final FocusManager mFocusManager;
    private Procedure mDirPopper;

    public SharedInputHandler(FocusManager focusManager, Procedure dirPopper) {
        mFocusManager = focusManager;
        mDirPopper = dirPopper;
    }

    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (Events.isNavigationKeyCode(keyCode)) {
            // Forward all unclaimed navigation keystrokes to the directory list.
            // This causes any stray navigation keystrokes to focus the content pane,
            // which is probably what the user is trying to do.
            mFocusManager.focusDirectoryList();
            return true;
        } else if (keyCode == KeyEvent.KEYCODE_TAB) {
            // Tab toggles focus on the navigation drawer.
            mFocusManager.advanceFocusArea();
            return true;
        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
            mDirPopper.run();
            return true;
        }

        return false;
    }
}
+26 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.base;

/**
 * Functional interface like a {@link Runnable}, but returning a boolean value
 * indicating if the Procedure succeeded.
 */
@FunctionalInterface
public interface Procedure {

    boolean run();
}
Loading