1/*
2 * Copyright (C) 2009 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 com.android.inputmethod.pinyin;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.inputmethodservice.InputMethodService;
22import android.os.Handler;
23import android.os.SystemClock;
24import android.os.SystemProperties;
25import android.util.AttributeSet;
26import android.view.GestureDetector;
27import android.view.Gravity;
28import android.view.MotionEvent;
29import android.view.View;
30import android.view.View.OnTouchListener;
31import android.widget.PopupWindow;
32import android.widget.RelativeLayout;
33import android.widget.ViewFlipper;
34
35/**
36 * The top container to host soft keyboard view(s).
37 */
38public class SkbContainer extends RelativeLayout implements OnTouchListener {
39    /**
40     * For finger touch, user tends to press the bottom part of the target key,
41     * or he/she even presses the area out of it, so it is necessary to make a
42     * simple bias correction. If the input method runs on emulator, no bias
43     * correction will be used.
44     */
45    private static final int Y_BIAS_CORRECTION = -10;
46
47    /**
48     * Used to skip these move events whose position is too close to the
49     * previous touch events.
50     */
51    private static final int MOVE_TOLERANCE = 6;
52
53    /**
54     * If this member is true, PopupWindow is used to show on-key highlight
55     * effect.
56     */
57    private static boolean POPUPWINDOW_FOR_PRESSED_UI = false;
58
59    /**
60     * The current soft keyboard layout.
61     *
62     * @see com.android.inputmethod.pinyin.InputModeSwitcher for detailed layout
63     *      definitions.
64     */
65    private int mSkbLayout = 0;
66
67    /**
68     * The input method service.
69     */
70    private InputMethodService mService;
71
72    /**
73     * Input mode switcher used to switch between different modes like Chinese,
74     * English, etc.
75     */
76    private InputModeSwitcher mInputModeSwitcher;
77
78    /**
79     * The gesture detector.
80     */
81    private GestureDetector mGestureDetector;
82
83    private Environment mEnvironment;
84
85    private ViewFlipper mSkbFlipper;
86
87    /**
88     * The popup balloon hint for key press/release.
89     */
90    private BalloonHint mBalloonPopup;
91
92    /**
93     * The on-key balloon hint for key press/release.
94     */
95    private BalloonHint mBalloonOnKey = null;
96
97    /** The major sub soft keyboard. */
98    private SoftKeyboardView mMajorView;
99
100    /**
101     * The last parameter when function {@link #toggleCandidateMode(boolean)}
102     * was called.
103     */
104    private boolean mLastCandidatesShowing;
105
106    /** Used to indicate whether a popup soft keyboard is shown. */
107    private boolean mPopupSkbShow = false;
108
109    /**
110     * Used to indicate whether a popup soft keyboard is just shown, and waits
111     * for the touch event to release. After the release, the popup window can
112     * response to touch events.
113     **/
114    private boolean mPopupSkbNoResponse = false;
115
116    /** Popup sub keyboard. */
117    private PopupWindow mPopupSkb;
118
119    /** The view of the popup sub soft keyboard. */
120    private SoftKeyboardView mPopupSkbView;
121
122    private int mPopupX;
123
124    private int mPopupY;
125
126    /**
127     * When user presses a key, a timer is started, when it times out, it is
128     * necessary to detect whether user still holds the key.
129     */
130    private volatile boolean mWaitForTouchUp = false;
131
132    /**
133     * When user drags on the soft keyboard and the distance is enough, this
134     * drag will be recognized as a gesture and a gesture-based action will be
135     * taken, in this situation, ignore the consequent events.
136     */
137    private volatile boolean mDiscardEvent = false;
138
139    /**
140     * For finger touch, user tends to press the bottom part of the target key,
141     * or he/she even presses the area out of it, so it is necessary to make a
142     * simple bias correction in Y.
143     */
144    private int mYBiasCorrection = 0;
145
146    /**
147     * The x coordination of the last touch event.
148     */
149    private int mXLast;
150
151    /**
152     * The y coordination of the last touch event.
153     */
154    private int mYLast;
155
156    /**
157     * The soft keyboard view.
158     */
159    private SoftKeyboardView mSkv;
160
161    /**
162     * The position of the soft keyboard view in the container.
163     */
164    private int mSkvPosInContainer[] = new int[2];
165
166    /**
167     * The key pressed by user.
168     */
169    private SoftKey mSoftKeyDown = null;
170
171    /**
172     * Used to timeout a press if user holds the key for a long time.
173     */
174    private LongPressTimer mLongPressTimer;
175
176    /**
177     * For temporary use.
178     */
179    private int mXyPosTmp[] = new int[2];
180
181    public SkbContainer(Context context, AttributeSet attrs) {
182        super(context, attrs);
183
184        mEnvironment = Environment.getInstance();
185
186        mLongPressTimer = new LongPressTimer(this);
187
188        // If it runs on an emulator, no bias correction
189        if ("1".equals(SystemProperties.get("ro.kernel.qemu"))) {
190            mYBiasCorrection = 0;
191        } else {
192            mYBiasCorrection = Y_BIAS_CORRECTION;
193        }
194        mBalloonPopup = new BalloonHint(context, this, MeasureSpec.AT_MOST);
195        if (POPUPWINDOW_FOR_PRESSED_UI) {
196            mBalloonOnKey = new BalloonHint(context, this, MeasureSpec.AT_MOST);
197        }
198
199        mPopupSkb = new PopupWindow(mContext);
200        mPopupSkb.setBackgroundDrawable(null);
201        mPopupSkb.setClippingEnabled(false);
202    }
203
204    public void setService(InputMethodService service) {
205        mService = service;
206    }
207
208    public void setInputModeSwitcher(InputModeSwitcher inputModeSwitcher) {
209        mInputModeSwitcher = inputModeSwitcher;
210    }
211
212    public void setGestureDetector(GestureDetector gestureDetector) {
213        mGestureDetector = gestureDetector;
214    }
215
216    public boolean isCurrentSkbSticky() {
217        if (null == mMajorView) return true;
218        SoftKeyboard skb = mMajorView.getSoftKeyboard();
219        if (null != skb) {
220            return skb.getStickyFlag();
221        }
222        return true;
223    }
224
225    public void toggleCandidateMode(boolean candidatesShowing) {
226        if (null == mMajorView || !mInputModeSwitcher.isChineseText()
227                || mLastCandidatesShowing == candidatesShowing) return;
228        mLastCandidatesShowing = candidatesShowing;
229
230        SoftKeyboard skb = mMajorView.getSoftKeyboard();
231        if (null == skb) return;
232
233        int state = mInputModeSwitcher.getTooggleStateForCnCand();
234        if (!candidatesShowing) {
235            skb.disableToggleState(state, false);
236            skb.enableToggleStates(mInputModeSwitcher.getToggleStates());
237        } else {
238            skb.enableToggleState(state, false);
239        }
240
241        mMajorView.invalidate();
242    }
243
244    public void updateInputMode() {
245        int skbLayout = mInputModeSwitcher.getSkbLayout();
246        if (mSkbLayout != skbLayout) {
247            mSkbLayout = skbLayout;
248            updateSkbLayout();
249        }
250
251        mLastCandidatesShowing = false;
252
253        if (null == mMajorView) return;
254
255        SoftKeyboard skb = mMajorView.getSoftKeyboard();
256        if (null == skb) return;
257        skb.enableToggleStates(mInputModeSwitcher.getToggleStates());
258        invalidate();
259        return;
260    }
261
262    private void updateSkbLayout() {
263        int screenWidth = mEnvironment.getScreenWidth();
264        int keyHeight = mEnvironment.getKeyHeight();
265        int skbHeight = mEnvironment.getSkbHeight();
266
267        Resources r = mContext.getResources();
268        if (null == mSkbFlipper) {
269            mSkbFlipper = (ViewFlipper) findViewById(R.id.alpha_floatable);
270        }
271        mMajorView = (SoftKeyboardView) mSkbFlipper.getChildAt(0);
272
273        SoftKeyboard majorSkb = null;
274        SkbPool skbPool = SkbPool.getInstance();
275
276        switch (mSkbLayout) {
277        case R.xml.skb_qwerty:
278            majorSkb = skbPool.getSoftKeyboard(R.xml.skb_qwerty,
279                    R.xml.skb_qwerty, screenWidth, skbHeight, mContext);
280            break;
281
282        case R.xml.skb_sym1:
283            majorSkb = skbPool.getSoftKeyboard(R.xml.skb_sym1, R.xml.skb_sym1,
284                    screenWidth, skbHeight, mContext);
285            break;
286
287        case R.xml.skb_sym2:
288            majorSkb = skbPool.getSoftKeyboard(R.xml.skb_sym2, R.xml.skb_sym2,
289                    screenWidth, skbHeight, mContext);
290            break;
291
292        case R.xml.skb_smiley:
293            majorSkb = skbPool.getSoftKeyboard(R.xml.skb_smiley,
294                    R.xml.skb_smiley, screenWidth, skbHeight, mContext);
295            break;
296
297        case R.xml.skb_phone:
298            majorSkb = skbPool.getSoftKeyboard(R.xml.skb_phone,
299                    R.xml.skb_phone, screenWidth, skbHeight, mContext);
300            break;
301        default:
302        }
303
304        if (null == majorSkb || !mMajorView.setSoftKeyboard(majorSkb)) {
305            return;
306        }
307        mMajorView.setBalloonHint(mBalloonOnKey, mBalloonPopup, false);
308        mMajorView.invalidate();
309    }
310
311    private void responseKeyEvent(SoftKey sKey) {
312        if (null == sKey) return;
313        ((PinyinIME) mService).responseSoftKeyEvent(sKey);
314        return;
315    }
316
317    private SoftKeyboardView inKeyboardView(int x, int y,
318            int positionInParent[]) {
319        if (mPopupSkbShow) {
320            if (mPopupX <= x && mPopupX + mPopupSkb.getWidth() > x
321                    && mPopupY <= y && mPopupY + mPopupSkb.getHeight() > y) {
322                positionInParent[0] = mPopupX;
323                positionInParent[1] = mPopupY;
324                mPopupSkbView.setOffsetToSkbContainer(positionInParent);
325                return mPopupSkbView;
326            }
327            return null;
328        }
329
330        return mMajorView;
331    }
332
333    private void popupSymbols() {
334        int popupResId = mSoftKeyDown.getPopupResId();
335        if (popupResId > 0) {
336            int skbContainerWidth = getWidth();
337            int skbContainerHeight = getHeight();
338            // The paddings of the background are not included.
339            int miniSkbWidth = (int) (skbContainerWidth * 0.8);
340            int miniSkbHeight = (int) (skbContainerHeight * 0.23);
341
342            SkbPool skbPool = SkbPool.getInstance();
343            SoftKeyboard skb = skbPool.getSoftKeyboard(popupResId, popupResId,
344                    miniSkbWidth, miniSkbHeight, mContext);
345            if (null == skb) return;
346
347            mPopupX = (skbContainerWidth - skb.getSkbTotalWidth()) / 2;
348            mPopupY = (skbContainerHeight - skb.getSkbTotalHeight()) / 2;
349
350            if (null == mPopupSkbView) {
351                mPopupSkbView = new SoftKeyboardView(mContext, null);
352                mPopupSkbView.onMeasure(LayoutParams.WRAP_CONTENT,
353                        LayoutParams.WRAP_CONTENT);
354            }
355            mPopupSkbView.setOnTouchListener(this);
356            mPopupSkbView.setSoftKeyboard(skb);
357            mPopupSkbView.setBalloonHint(mBalloonOnKey, mBalloonPopup, true);
358
359            mPopupSkb.setContentView(mPopupSkbView);
360            mPopupSkb.setWidth(skb.getSkbCoreWidth()
361                    + mPopupSkbView.getPaddingLeft()
362                    + mPopupSkbView.getPaddingRight());
363            mPopupSkb.setHeight(skb.getSkbCoreHeight()
364                    + mPopupSkbView.getPaddingTop()
365                    + mPopupSkbView.getPaddingBottom());
366
367            getLocationInWindow(mXyPosTmp);
368            mPopupSkb.showAtLocation(this, Gravity.NO_GRAVITY, mPopupX, mPopupY
369                    + mXyPosTmp[1]);
370            mPopupSkbShow = true;
371            mPopupSkbNoResponse = true;
372            // Invalidate itself to dim the current soft keyboards.
373            dimSoftKeyboard(true);
374            resetKeyPress(0);
375        }
376    }
377
378    private void dimSoftKeyboard(boolean dimSkb) {
379        mMajorView.dimSoftKeyboard(dimSkb);
380    }
381
382    private void dismissPopupSkb() {
383        mPopupSkb.dismiss();
384        mPopupSkbShow = false;
385        dimSoftKeyboard(false);
386        resetKeyPress(0);
387    }
388
389    private void resetKeyPress(long delay) {
390        mLongPressTimer.removeTimer();
391
392        if (null != mSkv) {
393            mSkv.resetKeyPress(delay);
394        }
395    }
396
397    public boolean handleBack(boolean realAction) {
398        if (mPopupSkbShow) {
399            if (!realAction) return true;
400
401            dismissPopupSkb();
402            mDiscardEvent = true;
403            return true;
404        }
405        return false;
406    }
407
408    public void dismissPopups() {
409        handleBack(true);
410        resetKeyPress(0);
411    }
412
413    @Override
414    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
415        Environment env = Environment.getInstance();
416        int measuredWidth = env.getScreenWidth();
417        int measuredHeight = getPaddingTop();
418        measuredHeight += env.getSkbHeight();
419        widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth,
420                MeasureSpec.EXACTLY);
421        heightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight,
422                MeasureSpec.EXACTLY);
423        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
424    }
425
426    @Override
427    public boolean onTouchEvent(MotionEvent event) {
428        super.onTouchEvent(event);
429
430        if (mSkbFlipper.isFlipping()) {
431            resetKeyPress(0);
432            return true;
433        }
434
435        int x = (int) event.getX();
436        int y = (int) event.getY();
437        // Bias correction
438        y = y + mYBiasCorrection;
439
440        // Ignore short-distance movement event to get better performance.
441        if (event.getAction() == MotionEvent.ACTION_MOVE) {
442            if (Math.abs(x - mXLast) <= MOVE_TOLERANCE
443                    && Math.abs(y - mYLast) <= MOVE_TOLERANCE) {
444                return true;
445            }
446        }
447
448        mXLast = x;
449        mYLast = y;
450
451        if (!mPopupSkbShow) {
452            if (mGestureDetector.onTouchEvent(event)) {
453                resetKeyPress(0);
454                mDiscardEvent = true;
455                return true;
456            }
457        }
458
459        switch (event.getAction()) {
460        case MotionEvent.ACTION_DOWN:
461            resetKeyPress(0);
462
463            mWaitForTouchUp = true;
464            mDiscardEvent = false;
465
466            mSkv = null;
467            mSoftKeyDown = null;
468            mSkv = inKeyboardView(x, y, mSkvPosInContainer);
469            if (null != mSkv) {
470                mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y
471                        - mSkvPosInContainer[1], mLongPressTimer, false);
472            }
473            break;
474
475        case MotionEvent.ACTION_MOVE:
476            if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) {
477                break;
478            }
479            if (mDiscardEvent) {
480                resetKeyPress(0);
481                break;
482            }
483
484            if (mPopupSkbShow && mPopupSkbNoResponse) {
485                break;
486            }
487
488            SoftKeyboardView skv = inKeyboardView(x, y, mSkvPosInContainer);
489            if (null != skv) {
490                if (skv != mSkv) {
491                    mSkv = skv;
492                    mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y
493                            - mSkvPosInContainer[1], mLongPressTimer, true);
494                } else if (null != skv) {
495                    if (null != mSkv) {
496                        mSoftKeyDown = mSkv.onKeyMove(
497                                x - mSkvPosInContainer[0], y
498                                        - mSkvPosInContainer[1]);
499                        if (null == mSoftKeyDown) {
500                            mDiscardEvent = true;
501                        }
502                    }
503                }
504            }
505            break;
506
507        case MotionEvent.ACTION_UP:
508            if (mDiscardEvent) {
509                resetKeyPress(0);
510                break;
511            }
512
513            mWaitForTouchUp = false;
514
515            // The view which got the {@link MotionEvent#ACTION_DOWN} event is
516            // always used to handle this event.
517            if (null != mSkv) {
518                mSkv.onKeyRelease(x - mSkvPosInContainer[0], y
519                        - mSkvPosInContainer[1]);
520            }
521
522            if (!mPopupSkbShow || !mPopupSkbNoResponse) {
523                responseKeyEvent(mSoftKeyDown);
524            }
525
526            if (mSkv == mPopupSkbView && !mPopupSkbNoResponse) {
527                dismissPopupSkb();
528            }
529            mPopupSkbNoResponse = false;
530            break;
531
532        case MotionEvent.ACTION_CANCEL:
533            break;
534        }
535
536        if (null == mSkv) {
537            return false;
538        }
539
540        return true;
541    }
542
543    // Function for interface OnTouchListener, it is used to handle touch events
544    // which will be delivered to the popup soft keyboard view.
545    public boolean onTouch(View v, MotionEvent event) {
546        // Translate the event to fit to the container.
547        MotionEvent newEv = MotionEvent.obtain(event.getDownTime(), event
548                .getEventTime(), event.getAction(), event.getX() + mPopupX,
549                event.getY() + mPopupY, event.getPressure(), event.getSize(),
550                event.getMetaState(), event.getXPrecision(), event
551                        .getYPrecision(), event.getDeviceId(), event
552                        .getEdgeFlags());
553        boolean ret = onTouchEvent(newEv);
554        return ret;
555    }
556
557    class LongPressTimer extends Handler implements Runnable {
558        /**
559         * When user presses a key for a long time, the timeout interval to
560         * generate first {@link #LONG_PRESS_KEYNUM1} key events.
561         */
562        public static final int LONG_PRESS_TIMEOUT1 = 500;
563
564        /**
565         * When user presses a key for a long time, after the first
566         * {@link #LONG_PRESS_KEYNUM1} key events, this timeout interval will be
567         * used.
568         */
569        private static final int LONG_PRESS_TIMEOUT2 = 100;
570
571        /**
572         * When user presses a key for a long time, after the first
573         * {@link #LONG_PRESS_KEYNUM2} key events, this timeout interval will be
574         * used.
575         */
576        private static final int LONG_PRESS_TIMEOUT3 = 100;
577
578        /**
579         * When user presses a key for a long time, after the first
580         * {@link #LONG_PRESS_KEYNUM1} key events, timeout interval
581         * {@link #LONG_PRESS_TIMEOUT2} will be used instead.
582         */
583        public static final int LONG_PRESS_KEYNUM1 = 1;
584
585        /**
586         * When user presses a key for a long time, after the first
587         * {@link #LONG_PRESS_KEYNUM2} key events, timeout interval
588         * {@link #LONG_PRESS_TIMEOUT3} will be used instead.
589         */
590        public static final int LONG_PRESS_KEYNUM2 = 3;
591
592        SkbContainer mSkbContainer;
593
594        private int mResponseTimes = 0;
595
596        public LongPressTimer(SkbContainer skbContainer) {
597            mSkbContainer = skbContainer;
598        }
599
600        public void startTimer() {
601            postAtTime(this, SystemClock.uptimeMillis() + LONG_PRESS_TIMEOUT1);
602            mResponseTimes = 0;
603        }
604
605        public boolean removeTimer() {
606            removeCallbacks(this);
607            return true;
608        }
609
610        public void run() {
611            if (mWaitForTouchUp) {
612                mResponseTimes++;
613                if (mSoftKeyDown.repeatable()) {
614                    if (mSoftKeyDown.isUserDefKey()) {
615                        if (1 == mResponseTimes) {
616                            if (mInputModeSwitcher
617                                    .tryHandleLongPressSwitch(mSoftKeyDown.mKeyCode)) {
618                                mDiscardEvent = true;
619                                resetKeyPress(0);
620                            }
621                        }
622                    } else {
623                        responseKeyEvent(mSoftKeyDown);
624                        long timeout;
625                        if (mResponseTimes < LONG_PRESS_KEYNUM1) {
626                            timeout = LONG_PRESS_TIMEOUT1;
627                        } else if (mResponseTimes < LONG_PRESS_KEYNUM2) {
628                            timeout = LONG_PRESS_TIMEOUT2;
629                        } else {
630                            timeout = LONG_PRESS_TIMEOUT3;
631                        }
632                        postAtTime(this, SystemClock.uptimeMillis() + timeout);
633                    }
634                } else {
635                    if (1 == mResponseTimes) {
636                        popupSymbols();
637                    }
638                }
639            }
640        }
641    }
642}
643