ZoomButtonsController.java revision ba87e3e6c985e7175152993b5efcc7dd2f0e1c93
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.app.AlertDialog;
20import android.app.Dialog;
21import android.content.BroadcastReceiver;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.content.SharedPreferences;
27import android.graphics.Canvas;
28import android.graphics.PixelFormat;
29import android.graphics.Rect;
30import android.os.Handler;
31import android.os.Message;
32import android.os.SystemClock;
33import android.provider.Settings;
34import android.util.Log;
35import android.view.GestureDetector;
36import android.view.Gravity;
37import android.view.KeyEvent;
38import android.view.LayoutInflater;
39import android.view.MotionEvent;
40import android.view.View;
41import android.view.ViewConfiguration;
42import android.view.ViewGroup;
43import android.view.ViewParent;
44import android.view.ViewRoot;
45import android.view.Window;
46import android.view.WindowManager;
47import android.view.View.OnClickListener;
48import android.view.WindowManager.LayoutParams;
49
50/*
51 * Implementation notes:
52 * - The zoom controls are displayed in their own window.
53 *   (Easier for the client and better performance)
54 * - This window is not touchable, and by default is not focusable.
55 * - To make the buttons clickable, it attaches a OnTouchListener to the owner
56 *   view and does the hit detection locally.
57 * - When it is focusable, it forwards uninteresting events to the owner view's
58 *   view hierarchy.
59 */
60/**
61 * The {@link ZoomButtonsController} handles showing and hiding the zoom
62 * controls relative to an owner view. It also gives the client access to the
63 * zoom controls container, allowing for additional accessory buttons to be
64 * shown in the zoom controls window.
65 * <p>
66 * Typical usage involves the client using the {@link GestureDetector} to
67 * forward events from
68 * {@link GestureDetector.OnDoubleTapListener#onDoubleTapEvent(MotionEvent)} to
69 * {@link #handleDoubleTapEvent(MotionEvent)}. Also, whenever the owner cannot
70 * be zoomed further, the client should update
71 * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}.
72 * <p>
73 * If you are using this with a custom View, please call
74 * {@link #setVisible(boolean) setVisible(false)} from the
75 * {@link View#onDetachedFromWindow}.
76 *
77 * @hide
78 */
79public class ZoomButtonsController implements View.OnTouchListener {
80
81    private static final String TAG = "ZoomButtonsController";
82
83    private static final int ZOOM_CONTROLS_TIMEOUT =
84            (int) ViewConfiguration.getZoomControlsTimeout();
85
86    private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20;
87    private int mTouchPaddingScaledSq;
88
89    private Context mContext;
90    private WindowManager mWindowManager;
91
92    /**
93     * The view that is being zoomed by this zoom controller.
94     */
95    private View mOwnerView;
96
97    /**
98     * The location of the owner view on the screen. This is recalculated
99     * each time the zoom controller is shown.
100     */
101    private int[] mOwnerViewRawLocation = new int[2];
102
103    /**
104     * The container that is added as a window.
105     */
106    private FrameLayout mContainer;
107    private LayoutParams mContainerLayoutParams;
108    private int[] mContainerRawLocation = new int[2];
109
110    private ZoomControls mControls;
111
112    /**
113     * The view (or null) that should receive touch events. This will get set if
114     * the touch down hits the container. It will be reset on the touch up.
115     */
116    private View mTouchTargetView;
117    /**
118     * The {@link #mTouchTargetView}'s location in window, set on touch down.
119     */
120    private int[] mTouchTargetWindowLocation = new int[2];
121    /**
122     * If the zoom controller is dismissed but the user is still in a touch
123     * interaction, we set this to true. This will ignore all touch events until
124     * up/cancel, and then set the owner's touch listener to null.
125     */
126    private boolean mReleaseTouchListenerOnUp;
127
128    /**
129     * Whether we are currently in the double-tap gesture, with the second tap
130     * still being performed (i.e., we're waiting for the second tap's touch up).
131     */
132    private boolean mIsSecondTapDown;
133
134    /** Whether the container has been added to the window manager. */
135    private boolean mIsVisible;
136
137    private Rect mTempRect = new Rect();
138    private int[] mTempIntArray = new int[2];
139
140    private OnZoomListener mCallback;
141
142    /**
143     * In 1.0, the ZoomControls were to be added to the UI by the client of
144     * WebView, MapView, etc. We didn't want apps to break, so we return a dummy
145     * view in place now.
146     */
147    private InvisibleView mDummyZoomControls;
148
149    /**
150     * When showing the zoom, we add the view as a new window. However, there is
151     * logic that needs to know the size of the zoom which is determined after
152     * it's laid out. Therefore, we must post this logic onto the UI thread so
153     * it will be exceuted AFTER the layout. This is the logic.
154     */
155    private Runnable mPostedVisibleInitializer;
156
157    private IntentFilter mConfigurationChangedFilter =
158            new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
159
160    /**
161     * Needed to reposition the zoom controls after configuration changes.
162     */
163    private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() {
164        @Override
165        public void onReceive(Context context, Intent intent) {
166            if (!mIsVisible) return;
167
168            mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED);
169            mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED);
170        }
171    };
172
173    /**
174     * The setting name that tracks whether we've shown the zoom tutorial.
175     */
176    private static final String SETTING_NAME_SHOWN_TUTORIAL = "shown_zoom_tutorial";
177    private static Dialog sTutorialDialog;
178
179    /** When configuration changes, this is called after the UI thread is idle. */
180    private static final int MSG_POST_CONFIGURATION_CHANGED = 2;
181    /** Used to delay the zoom controller dismissal. */
182    private static final int MSG_DISMISS_ZOOM_CONTROLS = 3;
183    /**
184     * If setVisible(true) is called and the owner view's window token is null,
185     * we delay the setVisible(true) call until it is not null.
186     */
187    private static final int MSG_POST_SET_VISIBLE = 4;
188
189    private Handler mHandler = new Handler() {
190        @Override
191        public void handleMessage(Message msg) {
192            switch (msg.what) {
193                case MSG_POST_CONFIGURATION_CHANGED:
194                    onPostConfigurationChanged();
195                    break;
196
197                case MSG_DISMISS_ZOOM_CONTROLS:
198                    setVisible(false);
199                    break;
200
201                case MSG_POST_SET_VISIBLE:
202                    if (mOwnerView.getWindowToken() == null) {
203                        // Doh, it is still null, just ignore the set visible call
204                        Log.e(TAG,
205                                "Cannot make the zoom controller visible if the owner view is " +
206                                "not attached to a window.");
207                    } else {
208                        setVisible(true);
209                    }
210                    break;
211            }
212
213        }
214    };
215
216    /**
217     * Constructor for the {@link ZoomButtonsController}.
218     *
219     * @param ownerView The view that is being zoomed by the zoom controls. The
220     *            zoom controls will be displayed aligned with this view.
221     */
222    public ZoomButtonsController(View ownerView) {
223        mContext = ownerView.getContext();
224        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
225        mOwnerView = ownerView;
226
227        mTouchPaddingScaledSq = (int)
228                (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density);
229        mTouchPaddingScaledSq *= mTouchPaddingScaledSq;
230
231        mContainer = createContainer();
232    }
233
234    /**
235     * Whether to enable the zoom in control.
236     *
237     * @param enabled Whether to enable the zoom in control.
238     */
239    public void setZoomInEnabled(boolean enabled) {
240        mControls.setIsZoomInEnabled(enabled);
241    }
242
243    /**
244     * Whether to enable the zoom out control.
245     *
246     * @param enabled Whether to enable the zoom out control.
247     */
248    public void setZoomOutEnabled(boolean enabled) {
249        mControls.setIsZoomOutEnabled(enabled);
250    }
251
252    /**
253     * Sets the delay between zoom callbacks as the user holds a zoom button.
254     *
255     * @param speed The delay in milliseconds between zoom callbacks.
256     */
257    public void setZoomSpeed(long speed) {
258        mControls.setZoomSpeed(speed);
259    }
260
261    private FrameLayout createContainer() {
262        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
263        // Controls are positioned BOTTOM | CENTER with respect to the owner view.
264        lp.gravity = Gravity.TOP | Gravity.LEFT;
265        lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE |
266                LayoutParams.FLAG_NOT_FOCUSABLE |
267                LayoutParams.FLAG_LAYOUT_NO_LIMITS;
268        lp.height = LayoutParams.WRAP_CONTENT;
269        lp.width = LayoutParams.FILL_PARENT;
270        lp.type = LayoutParams.TYPE_APPLICATION_PANEL;
271        lp.format = PixelFormat.TRANSPARENT;
272        lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons;
273        mContainerLayoutParams = lp;
274
275        FrameLayout container = new Container(mContext);
276        container.setLayoutParams(lp);
277        container.setMeasureAllChildren(true);
278
279        LayoutInflater inflater = (LayoutInflater) mContext
280                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
281        inflater.inflate(com.android.internal.R.layout.zoom_container, container);
282
283        mControls = (ZoomControls) container.findViewById(com.android.internal.R.id.zoomControls);
284        mControls.setOnZoomInClickListener(new OnClickListener() {
285            public void onClick(View v) {
286                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
287                if (mCallback != null) mCallback.onZoom(true);
288            }
289        });
290        mControls.setOnZoomOutClickListener(new OnClickListener() {
291            public void onClick(View v) {
292                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
293                if (mCallback != null) mCallback.onZoom(false);
294            }
295        });
296
297        return container;
298    }
299
300    /**
301     * Sets the {@link OnZoomListener} listener that receives callbacks to zoom.
302     *
303     * @param listener The listener that will be told to zoom.
304     */
305    public void setOnZoomListener(OnZoomListener listener) {
306        mCallback = listener;
307    }
308
309    /**
310     * Sets whether the zoom controls should be focusable. If the controls are
311     * focusable, then trackball and arrow key interactions are possible.
312     * Otherwise, only touch interactions are possible.
313     *
314     * @param focusable Whether the zoom controls should be focusable.
315     */
316    public void setFocusable(boolean focusable) {
317        int oldFlags = mContainerLayoutParams.flags;
318        if (focusable) {
319            mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
320        } else {
321            mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
322        }
323
324        if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) {
325            mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
326        }
327    }
328
329    /**
330     * Whether the zoom controls are visible to the user.
331     *
332     * @return Whether the zoom controls are visible to the user.
333     */
334    public boolean isVisible() {
335        return mIsVisible;
336    }
337
338    /**
339     * Sets whether the zoom controls should be visible to the user.
340     *
341     * @param visible Whether the zoom controls should be visible to the user.
342     */
343    public void setVisible(boolean visible) {
344
345        if (visible) {
346            if (mOwnerView.getWindowToken() == null) {
347                /*
348                 * We need a window token to show ourselves, maybe the owner's
349                 * window hasn't been created yet but it will have been by the
350                 * time the looper is idle, so post the setVisible(true) call.
351                 */
352                if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) {
353                    mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE);
354                }
355                return;
356            }
357
358            dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
359        }
360
361        if (mIsVisible == visible) {
362            return;
363        }
364        mIsVisible = visible;
365
366        if (visible) {
367            if (mContainerLayoutParams.token == null) {
368                mContainerLayoutParams.token = mOwnerView.getWindowToken();
369            }
370
371            mWindowManager.addView(mContainer, mContainerLayoutParams);
372
373            if (mPostedVisibleInitializer == null) {
374                mPostedVisibleInitializer = new Runnable() {
375                    public void run() {
376                        refreshPositioningVariables();
377
378                        if (mCallback != null) {
379                            mCallback.onVisibilityChanged(true);
380                        }
381                    }
382                };
383            }
384
385            mHandler.post(mPostedVisibleInitializer);
386
387            // Handle configuration changes when visible
388            mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter);
389
390            // Steal touches events from the owner
391            mOwnerView.setOnTouchListener(this);
392            mReleaseTouchListenerOnUp = false;
393
394        } else {
395            // Don't want to steal any more touches
396            if (mTouchTargetView != null) {
397                // We are still stealing the touch events for this touch
398                // sequence, so release the touch listener later
399                mReleaseTouchListenerOnUp = true;
400            } else {
401                mOwnerView.setOnTouchListener(null);
402            }
403
404            // No longer care about configuration changes
405            mContext.unregisterReceiver(mConfigurationChangedReceiver);
406
407            mWindowManager.removeView(mContainer);
408            mHandler.removeCallbacks(mPostedVisibleInitializer);
409
410            if (mCallback != null) {
411                mCallback.onVisibilityChanged(false);
412            }
413        }
414
415    }
416
417    /**
418     * Gets the container that is the parent of the zoom controls.
419     * <p>
420     * The client can add other views to this container to link them with the
421     * zoom controls.
422     *
423     * @return The container of the zoom controls. It will be a layout that
424     *         respects the gravity of a child's layout parameters.
425     */
426    public ViewGroup getContainer() {
427        return mContainer;
428    }
429
430    private void dismissControlsDelayed(int delay) {
431        mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS);
432        mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay);
433    }
434
435    /**
436     * Should be called by the client for each event belonging to the second tap
437     * (the down, move, up, and/or cancel events).
438     *
439     * @param event The event belonging to the second tap.
440     * @return Whether the event was consumed.
441     */
442    public boolean handleDoubleTapEvent(MotionEvent event) {
443        int action = event.getAction();
444
445        if (action == MotionEvent.ACTION_DOWN) {
446            int x = (int) event.getX();
447            int y = (int) event.getY();
448
449            /*
450             * This class will consume all events in the second tap (down,
451             * move(s), up). But, the owner already got the second tap's down,
452             * so cancel that. Do this before setVisible, since that call
453             * will set us as a touch listener.
454             */
455            MotionEvent cancelEvent = MotionEvent.obtain(event.getDownTime(),
456                    SystemClock.elapsedRealtime(),
457                    MotionEvent.ACTION_CANCEL, 0, 0, 0);
458            mOwnerView.dispatchTouchEvent(cancelEvent);
459            cancelEvent.recycle();
460
461            setVisible(true);
462            centerPoint(x, y);
463            mIsSecondTapDown = true;
464        }
465
466        return true;
467    }
468
469    private void refreshPositioningVariables() {
470        // Position the zoom controls on the bottom of the owner view.
471        int ownerHeight = mOwnerView.getHeight();
472        int ownerWidth = mOwnerView.getWidth();
473        // The gap between the top of the owner and the top of the container
474        int containerOwnerYOffset = ownerHeight - mContainer.getHeight();
475
476        // Calculate the owner view's bounds
477        mOwnerView.getLocationOnScreen(mOwnerViewRawLocation);
478        mContainerRawLocation[0] = mOwnerViewRawLocation[0];
479        mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset;
480
481        int[] ownerViewWindowLoc = mTempIntArray;
482        mOwnerView.getLocationInWindow(ownerViewWindowLoc);
483
484        // lp.x and lp.y should be relative to the owner's window top-left
485        mContainerLayoutParams.x = ownerViewWindowLoc[0];
486        mContainerLayoutParams.width = ownerWidth;
487        mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset;
488        if (mIsVisible) {
489            mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
490        }
491
492    }
493
494    /**
495     * Centers the point (in owner view's coordinates).
496     */
497    private void centerPoint(int x, int y) {
498        if (mCallback != null) {
499            mCallback.onCenter(x, y);
500        }
501    }
502
503    /* This will only be called when the container has focus. */
504    private boolean onContainerKey(KeyEvent event) {
505        int keyCode = event.getKeyCode();
506        if (isInterestingKey(keyCode)) {
507
508            if (keyCode == KeyEvent.KEYCODE_BACK) {
509                setVisible(false);
510            } else {
511                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
512            }
513
514            // Let the container handle the key
515            return false;
516
517        } else {
518
519            ViewRoot viewRoot = getOwnerViewRoot();
520            if (viewRoot != null) {
521                viewRoot.dispatchKey(event);
522            }
523
524            // We gave the key to the owner, don't let the container handle this key
525            return true;
526        }
527    }
528
529    private boolean isInterestingKey(int keyCode) {
530        switch (keyCode) {
531            case KeyEvent.KEYCODE_DPAD_CENTER:
532            case KeyEvent.KEYCODE_DPAD_UP:
533            case KeyEvent.KEYCODE_DPAD_DOWN:
534            case KeyEvent.KEYCODE_DPAD_LEFT:
535            case KeyEvent.KEYCODE_DPAD_RIGHT:
536            case KeyEvent.KEYCODE_ENTER:
537            case KeyEvent.KEYCODE_BACK:
538                return true;
539            default:
540                return false;
541        }
542    }
543
544    private ViewRoot getOwnerViewRoot() {
545        View rootViewOfOwner = mOwnerView.getRootView();
546        if (rootViewOfOwner == null) {
547            return null;
548        }
549
550        ViewParent parentOfRootView = rootViewOfOwner.getParent();
551        if (parentOfRootView instanceof ViewRoot) {
552            return (ViewRoot) parentOfRootView;
553        } else {
554            return null;
555        }
556    }
557
558    /**
559     * @hide The ZoomButtonsController implements the OnTouchListener, but this
560     *       does not need to be shown in its public API.
561     */
562    public boolean onTouch(View v, MotionEvent event) {
563        int action = event.getAction();
564
565        // Consume all events during the second-tap interaction (down, move, up/cancel)
566        boolean consumeEvent = mIsSecondTapDown;
567        if ((action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL)) {
568            // The second tap can no longer be down
569            mIsSecondTapDown = false;
570        }
571
572        if (mReleaseTouchListenerOnUp) {
573            // The controls were dismissed but we need to throw away all events until the up
574            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
575                mOwnerView.setOnTouchListener(null);
576                setTouchTargetView(null);
577                mReleaseTouchListenerOnUp = false;
578            }
579
580            // Eat this event
581            return true;
582        }
583
584        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
585
586        View targetView = mTouchTargetView;
587
588        switch (action) {
589            case MotionEvent.ACTION_DOWN:
590                targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY());
591                setTouchTargetView(targetView);
592                break;
593
594            case MotionEvent.ACTION_UP:
595            case MotionEvent.ACTION_CANCEL:
596                setTouchTargetView(null);
597                break;
598        }
599
600        if (targetView != null) {
601            // The upperleft corner of the target view in raw coordinates
602            int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0];
603            int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1];
604
605            MotionEvent containerEvent = MotionEvent.obtain(event);
606            // Convert the motion event into the target view's coordinates (from
607            // owner view's coordinates)
608            containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX,
609                    mOwnerViewRawLocation[1] - targetViewRawY);
610            /* Disallow negative coordinates (which can occur due to
611             * ZOOM_CONTROLS_TOUCH_PADDING) */
612            if (containerEvent.getX() < 0) {
613                containerEvent.offsetLocation(-containerEvent.getX(), 0);
614            }
615            if (containerEvent.getY() < 0) {
616                containerEvent.offsetLocation(0, -containerEvent.getY());
617            }
618            boolean retValue = targetView.dispatchTouchEvent(containerEvent);
619            containerEvent.recycle();
620            return retValue || consumeEvent;
621
622        } else {
623            return consumeEvent;
624        }
625    }
626
627    private void setTouchTargetView(View view) {
628        mTouchTargetView = view;
629        if (view != null) {
630            view.getLocationInWindow(mTouchTargetWindowLocation);
631        }
632    }
633
634    /**
635     * Returns the View that should receive a touch at the given coordinates.
636     *
637     * @param rawX The raw X.
638     * @param rawY The raw Y.
639     * @return The view that should receive the touches, or null if there is not one.
640     */
641    private View findViewForTouch(int rawX, int rawY) {
642        // Reverse order so the child drawn on top gets first dibs.
643        int containerCoordsX = rawX - mContainerRawLocation[0];
644        int containerCoordsY = rawY - mContainerRawLocation[1];
645        Rect frame = mTempRect;
646
647        View closestChild = null;
648        int closestChildDistanceSq = Integer.MAX_VALUE;
649
650        for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
651            View child = mContainer.getChildAt(i);
652            if (child.getVisibility() != View.VISIBLE) {
653                continue;
654            }
655
656            child.getHitRect(frame);
657            if (frame.contains(containerCoordsX, containerCoordsY)) {
658                return child;
659            }
660
661            int distanceX = Math.min(Math.abs(frame.left - containerCoordsX),
662                    Math.abs(containerCoordsX - frame.right));
663            int distanceY = Math.min(Math.abs(frame.top - containerCoordsY),
664                    Math.abs(containerCoordsY - frame.bottom));
665            int distanceSq = distanceX * distanceX + distanceY * distanceY;
666
667            if ((distanceSq < mTouchPaddingScaledSq) &&
668                    (distanceSq < closestChildDistanceSq)) {
669                closestChild = child;
670                closestChildDistanceSq = distanceSq;
671            }
672        }
673
674        return closestChild;
675    }
676
677    private void onPostConfigurationChanged() {
678        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
679        refreshPositioningVariables();
680    }
681
682    /*
683     * This is static so Activities can call this instead of the Views
684     * (Activities usually do not have a reference to the ZoomButtonsController
685     * instance.)
686     */
687    /**
688     * Shows a "tutorial" (some text) to the user teaching her the new zoom
689     * invocation method. Must call from the main thread.
690     * <p>
691     * It checks the global system setting to ensure this has not been seen
692     * before. Furthermore, if the application does not have privilege to write
693     * to the system settings, it will store this bit locally in a shared
694     * preference.
695     *
696     * @hide This should only be used by our main apps--browser, maps, and
697     *       gallery
698     */
699    public static void showZoomTutorialOnce(Context context) {
700
701        // TODO: remove this code, but to hit the weekend build, just never show
702        if (true) return;
703
704        ContentResolver cr = context.getContentResolver();
705        if (Settings.System.getInt(cr, SETTING_NAME_SHOWN_TUTORIAL, 0) == 1) {
706            return;
707        }
708
709        SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE);
710        if (sp.getInt(SETTING_NAME_SHOWN_TUTORIAL, 0) == 1) {
711            return;
712        }
713
714        if (sTutorialDialog != null && sTutorialDialog.isShowing()) {
715            sTutorialDialog.dismiss();
716        }
717
718        LayoutInflater layoutInflater =
719                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
720        TextView textView = (TextView) layoutInflater.inflate(
721                com.android.internal.R.layout.alert_dialog_simple_text, null)
722                .findViewById(android.R.id.text1);
723        textView.setText(com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short);
724
725        sTutorialDialog = new AlertDialog.Builder(context)
726                .setView(textView)
727                .setIcon(0)
728                .create();
729
730        Window window = sTutorialDialog.getWindow();
731        window.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
732        window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND |
733                WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
734        window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
735                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
736
737        sTutorialDialog.show();
738    }
739
740    /** @hide Should only be used by Android platform apps */
741    public static void finishZoomTutorial(Context context, boolean userNotified) {
742        if (sTutorialDialog == null) return;
743
744        sTutorialDialog.dismiss();
745        sTutorialDialog = null;
746
747        // Record that they have seen the tutorial
748        if (userNotified) {
749            try {
750                Settings.System.putInt(context.getContentResolver(), SETTING_NAME_SHOWN_TUTORIAL,
751                        1);
752            } catch (SecurityException e) {
753                /*
754                 * The app does not have permission to clear this global flag, make
755                 * sure the user does not see the message when he comes back to this
756                 * same app at least.
757                 */
758                SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE);
759                sp.edit().putInt(SETTING_NAME_SHOWN_TUTORIAL, 1).commit();
760            }
761        }
762    }
763
764    /** @hide Should only be used by Android platform apps */
765    public void finishZoomTutorial() {
766        finishZoomTutorial(mContext, true);
767    }
768
769    /** @hide Should only be used only be WebView and MapView */
770    public View getDummyZoomControls() {
771        if (mDummyZoomControls == null) {
772            mDummyZoomControls = new InvisibleView(mContext);
773        }
774        return mDummyZoomControls;
775    }
776
777    /**
778     * Interface that will be called when the user performs an interaction that
779     * triggers some action, for example zooming.
780     */
781    public interface OnZoomListener {
782        /**
783         * Called when the given point should be centered. The point will be in
784         * owner view coordinates.
785         *
786         * @param x The x of the point.
787         * @param y The y of the point.
788         */
789        void onCenter(int x, int y);
790
791        /**
792         * Called when the zoom controls' visibility changes.
793         *
794         * @param visible Whether the zoom controls are visible.
795         */
796        void onVisibilityChanged(boolean visible);
797
798        /**
799         * Called when the owner view needs to be zoomed.
800         *
801         * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out.
802         */
803        void onZoom(boolean zoomIn);
804    }
805
806    private class Container extends FrameLayout {
807        public Container(Context context) {
808            super(context);
809        }
810
811        /*
812         * Need to override this to intercept the key events. Otherwise, we
813         * would attach a key listener to the container but its superclass
814         * ViewGroup gives it to the focused View instead of calling the key
815         * listener, and so we wouldn't get the events.
816         */
817        @Override
818        public boolean dispatchKeyEvent(KeyEvent event) {
819            return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
820        }
821    }
822
823    /**
824     * An InvisibleView is an invisible, zero-sized View for backwards
825     * compatibility
826     */
827    private final class InvisibleView extends View {
828
829        private InvisibleView(Context context) {
830            super(context);
831            setVisibility(GONE);
832        }
833
834        @Override
835        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
836            setMeasuredDimension(0, 0);
837        }
838
839        @Override
840        public void draw(Canvas canvas) {
841        }
842
843        @Override
844        protected void dispatchDraw(Canvas canvas) {
845        }
846    }
847
848}
849