1/*
2 * Copyright (C) 2008 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
17package android.widget;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.graphics.PixelFormat;
24import android.graphics.Rect;
25import android.os.Handler;
26import android.os.Message;
27import android.util.Log;
28import android.view.Gravity;
29import android.view.KeyEvent;
30import android.view.LayoutInflater;
31import android.view.MotionEvent;
32import android.view.View;
33import android.view.View.OnClickListener;
34import android.view.ViewConfiguration;
35import android.view.ViewGroup;
36import android.view.ViewRootImpl;
37import android.view.WindowManager;
38import android.view.WindowManager.LayoutParams;
39
40/*
41 * Implementation notes:
42 * - The zoom controls are displayed in their own window.
43 *   (Easier for the client and better performance)
44 * - This window is never touchable, and by default is not focusable.
45 *   Its rect is quite big (fills horizontally) but has empty space between the
46 *   edges and center.  Touches there should be given to the owner.  Instead of
47 *   having the window touchable and dispatching these empty touch events to the
48 *   owner, we set the window to not touchable and steal events from owner
49 *   via onTouchListener.
50 * - To make the buttons clickable, it attaches an OnTouchListener to the owner
51 *   view and does the hit detection locally (attaches when visible, detaches when invisible).
52 * - When it is focusable, it forwards uninteresting events to the owner view's
53 *   view hierarchy.
54 */
55/**
56 * The {@link ZoomButtonsController} handles showing and hiding the zoom
57 * controls and positioning it relative to an owner view. It also gives the
58 * client access to the zoom controls container, allowing for additional
59 * accessory buttons to be shown in the zoom controls window.
60 * <p>
61 * Typically, clients should call {@link #setVisible(boolean) setVisible(true)}
62 * on a touch down or move (no need to call {@link #setVisible(boolean)
63 * setVisible(false)} since it will time out on its own). Also, whenever the
64 * owner cannot be zoomed further, the client should update
65 * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}.
66 * <p>
67 * If you are using this with a custom View, please call
68 * {@link #setVisible(boolean) setVisible(false)} from
69 * {@link View#onDetachedFromWindow} and from {@link View#onVisibilityChanged}
70 * when <code>visibility != View.VISIBLE</code>.
71 *
72 * @deprecated This functionality and UI is better handled with custom views and layouts
73 * rather than a dedicated zoom-control widget
74 */
75@Deprecated
76public class ZoomButtonsController implements View.OnTouchListener {
77
78    private static final String TAG = "ZoomButtonsController";
79
80    private static final int ZOOM_CONTROLS_TIMEOUT =
81            (int) ViewConfiguration.getZoomControlsTimeout();
82
83    private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20;
84    private int mTouchPaddingScaledSq;
85
86    private final Context mContext;
87    private final WindowManager mWindowManager;
88    private boolean mAutoDismissControls = true;
89
90    /**
91     * The view that is being zoomed by this zoom controller.
92     */
93    private final View mOwnerView;
94
95    /**
96     * The location of the owner view on the screen. This is recalculated
97     * each time the zoom controller is shown.
98     */
99    private final int[] mOwnerViewRawLocation = new int[2];
100
101    /**
102     * The container that is added as a window.
103     */
104    private final FrameLayout mContainer;
105    private LayoutParams mContainerLayoutParams;
106    private final int[] mContainerRawLocation = new int[2];
107
108    private ZoomControls mControls;
109
110    /**
111     * The view (or null) that should receive touch events. This will get set if
112     * the touch down hits the container. It will be reset on the touch up.
113     */
114    private View mTouchTargetView;
115    /**
116     * The {@link #mTouchTargetView}'s location in window, set on touch down.
117     */
118    private final int[] mTouchTargetWindowLocation = new int[2];
119
120    /**
121     * If the zoom controller is dismissed but the user is still in a touch
122     * interaction, we set this to true. This will ignore all touch events until
123     * up/cancel, and then set the owner's touch listener to null.
124     * <p>
125     * Otherwise, the owner view would get mismatched events (i.e., touch move
126     * even though it never got the touch down.)
127     */
128    private boolean mReleaseTouchListenerOnUp;
129
130    /** Whether the container has been added to the window manager. */
131    private boolean mIsVisible;
132
133    private final Rect mTempRect = new Rect();
134    private final int[] mTempIntArray = new int[2];
135
136    private OnZoomListener mCallback;
137
138    /**
139     * When showing the zoom, we add the view as a new window. However, there is
140     * logic that needs to know the size of the zoom which is determined after
141     * it's laid out. Therefore, we must post this logic onto the UI thread so
142     * it will be exceuted AFTER the layout. This is the logic.
143     */
144    private Runnable mPostedVisibleInitializer;
145
146    private final IntentFilter mConfigurationChangedFilter =
147            new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
148
149    /**
150     * Needed to reposition the zoom controls after configuration changes.
151     */
152    private final BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() {
153        @Override
154        public void onReceive(Context context, Intent intent) {
155            if (!mIsVisible) return;
156
157            mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED);
158            mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED);
159        }
160    };
161
162    /** When configuration changes, this is called after the UI thread is idle. */
163    private static final int MSG_POST_CONFIGURATION_CHANGED = 2;
164    /** Used to delay the zoom controller dismissal. */
165    private static final int MSG_DISMISS_ZOOM_CONTROLS = 3;
166    /**
167     * If setVisible(true) is called and the owner view's window token is null,
168     * we delay the setVisible(true) call until it is not null.
169     */
170    private static final int MSG_POST_SET_VISIBLE = 4;
171
172    private final Handler mHandler = new Handler() {
173        @Override
174        public void handleMessage(Message msg) {
175            switch (msg.what) {
176                case MSG_POST_CONFIGURATION_CHANGED:
177                    onPostConfigurationChanged();
178                    break;
179
180                case MSG_DISMISS_ZOOM_CONTROLS:
181                    setVisible(false);
182                    break;
183
184                case MSG_POST_SET_VISIBLE:
185                    if (mOwnerView.getWindowToken() == null) {
186                        // Doh, it is still null, just ignore the set visible call
187                        Log.e(TAG,
188                                "Cannot make the zoom controller visible if the owner view is " +
189                                "not attached to a window.");
190                    } else {
191                        setVisible(true);
192                    }
193                    break;
194            }
195
196        }
197    };
198
199    /**
200     * Constructor for the {@link ZoomButtonsController}.
201     *
202     * @param ownerView The view that is being zoomed by the zoom controls. The
203     *            zoom controls will be displayed aligned with this view.
204     */
205    public ZoomButtonsController(View ownerView) {
206        mContext = ownerView.getContext();
207        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
208        mOwnerView = ownerView;
209
210        mTouchPaddingScaledSq = (int)
211                (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density);
212        mTouchPaddingScaledSq *= mTouchPaddingScaledSq;
213
214        mContainer = createContainer();
215    }
216
217    /**
218     * Whether to enable the zoom in control.
219     *
220     * @param enabled Whether to enable the zoom in control.
221     */
222    public void setZoomInEnabled(boolean enabled) {
223        mControls.setIsZoomInEnabled(enabled);
224    }
225
226    /**
227     * Whether to enable the zoom out control.
228     *
229     * @param enabled Whether to enable the zoom out control.
230     */
231    public void setZoomOutEnabled(boolean enabled) {
232        mControls.setIsZoomOutEnabled(enabled);
233    }
234
235    /**
236     * Sets the delay between zoom callbacks as the user holds a zoom button.
237     *
238     * @param speed The delay in milliseconds between zoom callbacks.
239     */
240    public void setZoomSpeed(long speed) {
241        mControls.setZoomSpeed(speed);
242    }
243
244    private FrameLayout createContainer() {
245        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
246        // Controls are positioned BOTTOM | CENTER with respect to the owner view.
247        lp.gravity = Gravity.TOP | Gravity.START;
248        lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE |
249                LayoutParams.FLAG_NOT_FOCUSABLE |
250                LayoutParams.FLAG_LAYOUT_NO_LIMITS |
251                LayoutParams.FLAG_ALT_FOCUSABLE_IM;
252        lp.height = LayoutParams.WRAP_CONTENT;
253        lp.width = LayoutParams.MATCH_PARENT;
254        lp.type = LayoutParams.TYPE_APPLICATION_PANEL;
255        lp.format = PixelFormat.TRANSLUCENT;
256        lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons;
257        mContainerLayoutParams = lp;
258
259        FrameLayout container = new Container(mContext);
260        container.setLayoutParams(lp);
261        container.setMeasureAllChildren(true);
262
263        LayoutInflater inflater = (LayoutInflater) mContext
264                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
265        inflater.inflate(com.android.internal.R.layout.zoom_container, container);
266
267        mControls = container.findViewById(com.android.internal.R.id.zoomControls);
268        mControls.setOnZoomInClickListener(new OnClickListener() {
269            public void onClick(View v) {
270                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
271                if (mCallback != null) mCallback.onZoom(true);
272            }
273        });
274        mControls.setOnZoomOutClickListener(new OnClickListener() {
275            public void onClick(View v) {
276                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
277                if (mCallback != null) mCallback.onZoom(false);
278            }
279        });
280
281        return container;
282    }
283
284    /**
285     * Sets the {@link OnZoomListener} listener that receives callbacks to zoom.
286     *
287     * @param listener The listener that will be told to zoom.
288     */
289    public void setOnZoomListener(OnZoomListener listener) {
290        mCallback = listener;
291    }
292
293    /**
294     * Sets whether the zoom controls should be focusable. If the controls are
295     * focusable, then trackball and arrow key interactions are possible.
296     * Otherwise, only touch interactions are possible.
297     *
298     * @param focusable Whether the zoom controls should be focusable.
299     */
300    public void setFocusable(boolean focusable) {
301        int oldFlags = mContainerLayoutParams.flags;
302        if (focusable) {
303            mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
304        } else {
305            mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
306        }
307
308        if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) {
309            mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
310        }
311    }
312
313    /**
314     * Whether the zoom controls will be automatically dismissed after showing.
315     *
316     * @return Whether the zoom controls will be auto dismissed after showing.
317     */
318    public boolean isAutoDismissed() {
319        return mAutoDismissControls;
320    }
321
322    /**
323     * Sets whether the zoom controls will be automatically dismissed after
324     * showing.
325     */
326    public void setAutoDismissed(boolean autoDismiss) {
327        if (mAutoDismissControls == autoDismiss) return;
328        mAutoDismissControls = autoDismiss;
329    }
330
331    /**
332     * Whether the zoom controls are visible to the user.
333     *
334     * @return Whether the zoom controls are visible to the user.
335     */
336    public boolean isVisible() {
337        return mIsVisible;
338    }
339
340    /**
341     * Sets whether the zoom controls should be visible to the user.
342     *
343     * @param visible Whether the zoom controls should be visible to the user.
344     */
345    public void setVisible(boolean visible) {
346
347        if (visible) {
348            if (mOwnerView.getWindowToken() == null) {
349                /*
350                 * We need a window token to show ourselves, maybe the owner's
351                 * window hasn't been created yet but it will have been by the
352                 * time the looper is idle, so post the setVisible(true) call.
353                 */
354                if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) {
355                    mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE);
356                }
357                return;
358            }
359
360            dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
361        }
362
363        if (mIsVisible == visible) {
364            return;
365        }
366        mIsVisible = visible;
367
368        if (visible) {
369            if (mContainerLayoutParams.token == null) {
370                mContainerLayoutParams.token = mOwnerView.getWindowToken();
371            }
372
373            mWindowManager.addView(mContainer, mContainerLayoutParams);
374
375            if (mPostedVisibleInitializer == null) {
376                mPostedVisibleInitializer = new Runnable() {
377                    public void run() {
378                        refreshPositioningVariables();
379
380                        if (mCallback != null) {
381                            mCallback.onVisibilityChanged(true);
382                        }
383                    }
384                };
385            }
386
387            mHandler.post(mPostedVisibleInitializer);
388
389            // Handle configuration changes when visible
390            mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter);
391
392            // Steal touches events from the owner
393            mOwnerView.setOnTouchListener(this);
394            mReleaseTouchListenerOnUp = false;
395
396        } else {
397            // Don't want to steal any more touches
398            if (mTouchTargetView != null) {
399                // We are still stealing the touch events for this touch
400                // sequence, so release the touch listener later
401                mReleaseTouchListenerOnUp = true;
402            } else {
403                mOwnerView.setOnTouchListener(null);
404            }
405
406            // No longer care about configuration changes
407            mContext.unregisterReceiver(mConfigurationChangedReceiver);
408
409            mWindowManager.removeViewImmediate(mContainer);
410            mHandler.removeCallbacks(mPostedVisibleInitializer);
411
412            if (mCallback != null) {
413                mCallback.onVisibilityChanged(false);
414            }
415        }
416
417    }
418
419    /**
420     * Gets the container that is the parent of the zoom controls.
421     * <p>
422     * The client can add other views to this container to link them with the
423     * zoom controls.
424     *
425     * @return The container of the zoom controls. It will be a layout that
426     *         respects the gravity of a child's layout parameters.
427     */
428    public ViewGroup getContainer() {
429        return mContainer;
430    }
431
432    /**
433     * Gets the view for the zoom controls.
434     *
435     * @return The zoom controls view.
436     */
437    public View getZoomControls() {
438        return mControls;
439    }
440
441    private void dismissControlsDelayed(int delay) {
442        if (mAutoDismissControls) {
443            mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS);
444            mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay);
445        }
446    }
447
448    private void refreshPositioningVariables() {
449        // if the mOwnerView is detached from window then skip.
450        if (mOwnerView.getWindowToken() == null) return;
451
452        // Position the zoom controls on the bottom of the owner view.
453        int ownerHeight = mOwnerView.getHeight();
454        int ownerWidth = mOwnerView.getWidth();
455        // The gap between the top of the owner and the top of the container
456        int containerOwnerYOffset = ownerHeight - mContainer.getHeight();
457
458        // Calculate the owner view's bounds
459        mOwnerView.getLocationOnScreen(mOwnerViewRawLocation);
460        mContainerRawLocation[0] = mOwnerViewRawLocation[0];
461        mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset;
462
463        int[] ownerViewWindowLoc = mTempIntArray;
464        mOwnerView.getLocationInWindow(ownerViewWindowLoc);
465
466        // lp.x and lp.y should be relative to the owner's window top-left
467        mContainerLayoutParams.x = ownerViewWindowLoc[0];
468        mContainerLayoutParams.width = ownerWidth;
469        mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset;
470        if (mIsVisible) {
471            mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
472        }
473
474    }
475
476    /* This will only be called when the container has focus. */
477    private boolean onContainerKey(KeyEvent event) {
478        int keyCode = event.getKeyCode();
479        if (isInterestingKey(keyCode)) {
480
481            if (keyCode == KeyEvent.KEYCODE_BACK) {
482                if (event.getAction() == KeyEvent.ACTION_DOWN
483                        && event.getRepeatCount() == 0) {
484                    if (mOwnerView != null) {
485                        KeyEvent.DispatcherState ds = mOwnerView.getKeyDispatcherState();
486                        if (ds != null) {
487                            ds.startTracking(event, this);
488                        }
489                    }
490                    return true;
491                } else if (event.getAction() == KeyEvent.ACTION_UP
492                        && event.isTracking() && !event.isCanceled()) {
493                    setVisible(false);
494                    return true;
495                }
496
497            } else {
498                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
499            }
500
501            // Let the container handle the key
502            return false;
503
504        } else {
505
506            ViewRootImpl viewRoot = mOwnerView.getViewRootImpl();
507            if (viewRoot != null) {
508                viewRoot.dispatchInputEvent(event);
509            }
510
511            // We gave the key to the owner, don't let the container handle this key
512            return true;
513        }
514    }
515
516    private boolean isInterestingKey(int keyCode) {
517        switch (keyCode) {
518            case KeyEvent.KEYCODE_DPAD_CENTER:
519            case KeyEvent.KEYCODE_DPAD_UP:
520            case KeyEvent.KEYCODE_DPAD_DOWN:
521            case KeyEvent.KEYCODE_DPAD_LEFT:
522            case KeyEvent.KEYCODE_DPAD_RIGHT:
523            case KeyEvent.KEYCODE_ENTER:
524            case KeyEvent.KEYCODE_BACK:
525                return true;
526            default:
527                return false;
528        }
529    }
530
531    /**
532     * @hide The ZoomButtonsController implements the OnTouchListener, but this
533     *       does not need to be shown in its public API.
534     */
535    public boolean onTouch(View v, MotionEvent event) {
536        int action = event.getAction();
537
538        if (event.getPointerCount() > 1) {
539            // ZoomButtonsController doesn't handle mutitouch. Give up control.
540            return false;
541        }
542
543        if (mReleaseTouchListenerOnUp) {
544            // The controls were dismissed but we need to throw away all events until the up
545            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
546                mOwnerView.setOnTouchListener(null);
547                setTouchTargetView(null);
548                mReleaseTouchListenerOnUp = false;
549            }
550
551            // Eat this event
552            return true;
553        }
554
555        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
556
557        View targetView = mTouchTargetView;
558
559        switch (action) {
560            case MotionEvent.ACTION_DOWN:
561                targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY());
562                setTouchTargetView(targetView);
563                break;
564
565            case MotionEvent.ACTION_UP:
566            case MotionEvent.ACTION_CANCEL:
567                setTouchTargetView(null);
568                break;
569        }
570
571        if (targetView != null) {
572            // The upperleft corner of the target view in raw coordinates
573            int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0];
574            int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1];
575
576            MotionEvent containerEvent = MotionEvent.obtain(event);
577            // Convert the motion event into the target view's coordinates (from
578            // owner view's coordinates)
579            containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX,
580                    mOwnerViewRawLocation[1] - targetViewRawY);
581            /* Disallow negative coordinates (which can occur due to
582             * ZOOM_CONTROLS_TOUCH_PADDING) */
583            // These are floats because we need to potentially offset away this exact amount
584            float containerX = containerEvent.getX();
585            float containerY = containerEvent.getY();
586            if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) {
587                containerEvent.offsetLocation(-containerX, 0);
588            }
589            if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) {
590                containerEvent.offsetLocation(0, -containerY);
591            }
592            boolean retValue = targetView.dispatchTouchEvent(containerEvent);
593            containerEvent.recycle();
594            return retValue;
595
596        } else {
597            return false;
598        }
599    }
600
601    private void setTouchTargetView(View view) {
602        mTouchTargetView = view;
603        if (view != null) {
604            view.getLocationInWindow(mTouchTargetWindowLocation);
605        }
606    }
607
608    /**
609     * Returns the View that should receive a touch at the given coordinates.
610     *
611     * @param rawX The raw X.
612     * @param rawY The raw Y.
613     * @return The view that should receive the touches, or null if there is not one.
614     */
615    private View findViewForTouch(int rawX, int rawY) {
616        // Reverse order so the child drawn on top gets first dibs.
617        int containerCoordsX = rawX - mContainerRawLocation[0];
618        int containerCoordsY = rawY - mContainerRawLocation[1];
619        Rect frame = mTempRect;
620
621        View closestChild = null;
622        int closestChildDistanceSq = Integer.MAX_VALUE;
623
624        for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
625            View child = mContainer.getChildAt(i);
626            if (child.getVisibility() != View.VISIBLE) {
627                continue;
628            }
629
630            child.getHitRect(frame);
631            if (frame.contains(containerCoordsX, containerCoordsY)) {
632                return child;
633            }
634
635            int distanceX;
636            if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) {
637                distanceX = 0;
638            } else {
639                distanceX = Math.min(Math.abs(frame.left - containerCoordsX),
640                    Math.abs(containerCoordsX - frame.right));
641            }
642            int distanceY;
643            if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) {
644                distanceY = 0;
645            } else {
646                distanceY = Math.min(Math.abs(frame.top - containerCoordsY),
647                        Math.abs(containerCoordsY - frame.bottom));
648            }
649            int distanceSq = distanceX * distanceX + distanceY * distanceY;
650
651            if ((distanceSq < mTouchPaddingScaledSq) &&
652                    (distanceSq < closestChildDistanceSq)) {
653                closestChild = child;
654                closestChildDistanceSq = distanceSq;
655            }
656        }
657
658        return closestChild;
659    }
660
661    private void onPostConfigurationChanged() {
662        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
663        refreshPositioningVariables();
664    }
665
666    /**
667     * Interface that will be called when the user performs an interaction that
668     * triggers some action, for example zooming.
669     */
670    public interface OnZoomListener {
671
672        /**
673         * Called when the zoom controls' visibility changes.
674         *
675         * @param visible Whether the zoom controls are visible.
676         */
677        void onVisibilityChanged(boolean visible);
678
679        /**
680         * Called when the owner view needs to be zoomed.
681         *
682         * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out.
683         */
684        void onZoom(boolean zoomIn);
685    }
686
687    private class Container extends FrameLayout {
688        public Container(Context context) {
689            super(context);
690        }
691
692        /*
693         * Need to override this to intercept the key events. Otherwise, we
694         * would attach a key listener to the container but its superclass
695         * ViewGroup gives it to the focused View instead of calling the key
696         * listener, and so we wouldn't get the events.
697         */
698        @Override
699        public boolean dispatchKeyEvent(KeyEvent event) {
700            return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
701        }
702    }
703
704}
705