1/*
2 * Copyright (C) 2014, 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.latin;
18
19import android.content.res.Resources;
20import android.util.Log;
21import android.util.Pair;
22import android.view.KeyEvent;
23
24import com.android.inputmethod.keyboard.KeyboardSwitcher;
25import com.android.inputmethod.latin.settings.Settings;
26
27import java.util.ArrayList;
28import java.util.HashSet;
29import java.util.List;
30import java.util.Set;
31
32import javax.annotation.Nonnull;
33
34/**
35 * A class for detecting Emoji-Alt physical key.
36 */
37final class EmojiAltPhysicalKeyDetector {
38    private static final String TAG = "EmojiAltPhysicalKeyDetector";
39    private static final boolean DEBUG = false;
40
41    private List<EmojiHotKeys> mHotKeysList;
42
43    private static class HotKeySet extends HashSet<Pair<Integer, Integer>> { };
44
45    private abstract class EmojiHotKeys {
46        private final String mName;
47        private final HotKeySet mKeySet;
48
49        boolean mCanFire;
50        int mMetaState;
51
52        public EmojiHotKeys(final String name, HotKeySet keySet) {
53            mName = name;
54            mKeySet = keySet;
55            mCanFire = false;
56        }
57
58        public void onKeyDown(@Nonnull final KeyEvent keyEvent) {
59            if (DEBUG) {
60                Log.d(TAG, "EmojiHotKeys.onKeyDown() - " + mName + " - considering " + keyEvent);
61            }
62
63            final Pair<Integer, Integer> key =
64                    Pair.create(keyEvent.getKeyCode(), keyEvent.getMetaState());
65            if (mKeySet.contains(key)) {
66                if (DEBUG) {
67                   Log.d(TAG, "EmojiHotKeys.onKeyDown() - " + mName + " - enabling action");
68                }
69                mCanFire = true;
70                mMetaState = keyEvent.getMetaState();
71            } else if (mCanFire) {
72                if (DEBUG) {
73                   Log.d(TAG, "EmojiHotKeys.onKeyDown() - " + mName + " - disabling action");
74                }
75                mCanFire = false;
76            }
77        }
78
79        public void onKeyUp(@Nonnull final KeyEvent keyEvent) {
80            if (DEBUG) {
81                Log.d(TAG, "EmojiHotKeys.onKeyUp() - " + mName + " - considering " + keyEvent);
82            }
83
84            final int keyCode = keyEvent.getKeyCode();
85            int metaState = keyEvent.getMetaState();
86            if (KeyEvent.isModifierKey(keyCode)) {
87                 // Try restoring meta stat in case the released key was a modifier.
88                 // I am sure one can come up with scenarios to break this, but it
89                 // seems to work well in practice.
90                 metaState |= mMetaState;
91            }
92
93            final Pair<Integer, Integer> key = Pair.create(keyCode, metaState);
94            if (mKeySet.contains(key)) {
95                if (mCanFire) {
96                    if (!keyEvent.isCanceled()) {
97                        if (DEBUG) {
98                            Log.d(TAG, "EmojiHotKeys.onKeyUp() - " + mName + " - firing action");
99                        }
100                        action();
101                    } else {
102                        // This key up event was a part of key combinations and
103                        // should be ignored.
104                        if (DEBUG) {
105                            Log.d(TAG, "EmojiHotKeys.onKeyUp() - " + mName + " - canceled, ignoring action");
106                        }
107                    }
108                    mCanFire = false;
109                }
110            }
111
112            if (mCanFire) {
113                if (DEBUG) {
114                    Log.d(TAG, "EmojiHotKeys.onKeyUp() - " + mName + " - disabling action");
115                }
116                mCanFire = false;
117            }
118        }
119
120        protected abstract void action();
121    }
122
123    public EmojiAltPhysicalKeyDetector(@Nonnull final Resources resources) {
124        mHotKeysList = new ArrayList<EmojiHotKeys>();
125
126        final HotKeySet emojiSwitchSet = parseHotKeys(
127                resources, R.array.keyboard_switcher_emoji);
128        final EmojiHotKeys emojiHotKeys = new EmojiHotKeys("emoji", emojiSwitchSet) {
129            @Override
130            protected void action() {
131                final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
132                switcher.onToggleKeyboard(KeyboardSwitcher.KeyboardSwitchState.EMOJI);
133            }
134        };
135        mHotKeysList.add(emojiHotKeys);
136
137        final HotKeySet symbolsSwitchSet = parseHotKeys(
138                resources, R.array.keyboard_switcher_symbols_shifted);
139        final EmojiHotKeys symbolsHotKeys = new EmojiHotKeys("symbols", symbolsSwitchSet) {
140            @Override
141            protected void action() {
142                final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
143                switcher.onToggleKeyboard(KeyboardSwitcher.KeyboardSwitchState.SYMBOLS_SHIFTED);
144            }
145        };
146        mHotKeysList.add(symbolsHotKeys);
147    }
148
149    public void onKeyDown(@Nonnull final KeyEvent keyEvent) {
150        if (DEBUG) {
151            Log.d(TAG, "onKeyDown(): " + keyEvent);
152        }
153
154        if (shouldProcessEvent(keyEvent)) {
155            for (EmojiHotKeys hotKeys : mHotKeysList) {
156                hotKeys.onKeyDown(keyEvent);
157            }
158        }
159    }
160
161    public void onKeyUp(@Nonnull final KeyEvent keyEvent) {
162        if (DEBUG) {
163            Log.d(TAG, "onKeyUp(): " + keyEvent);
164        }
165
166        if (shouldProcessEvent(keyEvent)) {
167            for (EmojiHotKeys hotKeys : mHotKeysList) {
168                hotKeys.onKeyUp(keyEvent);
169            }
170        }
171    }
172
173    private static boolean shouldProcessEvent(@Nonnull final KeyEvent keyEvent) {
174        if (!Settings.getInstance().getCurrent().mEnableEmojiAltPhysicalKey) {
175            // The feature is disabled.
176            if (DEBUG) {
177                Log.d(TAG, "shouldProcessEvent(): Disabled");
178            }
179            return false;
180        }
181
182        return true;
183    }
184
185    private static HotKeySet parseHotKeys(
186            @Nonnull final Resources resources, final int resourceId) {
187        final HotKeySet keySet = new HotKeySet();
188        final String name = resources.getResourceEntryName(resourceId);
189        final String[] values = resources.getStringArray(resourceId);
190        for (int i = 0; values != null && i < values.length; i++) {
191            String[] valuePair = values[i].split(",");
192            if (valuePair.length != 2) {
193                Log.w(TAG, "Expected 2 integers in " + name + "[" + i + "] : " + values[i]);
194            }
195            try {
196                final Integer keyCode = Integer.parseInt(valuePair[0]);
197                final Integer metaState = Integer.parseInt(valuePair[1]);
198                final Pair<Integer, Integer> key = Pair.create(
199                        keyCode, KeyEvent.normalizeMetaState(metaState));
200                keySet.add(key);
201            } catch (NumberFormatException e) {
202                Log.w(TAG, "Failed to parse " + name + "[" + i + "] : " + values[i], e);
203            }
204        }
205        return keySet;
206    }
207}
208