1/*
2 * Copyright (C) 2013 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.keyboard.emoji;
18
19import android.content.SharedPreferences;
20import android.text.TextUtils;
21import android.util.Log;
22
23import com.android.inputmethod.keyboard.Key;
24import com.android.inputmethod.keyboard.Keyboard;
25import com.android.inputmethod.latin.settings.Settings;
26import com.android.inputmethod.latin.utils.JsonUtils;
27
28import java.util.ArrayDeque;
29import java.util.ArrayList;
30import java.util.Collection;
31import java.util.Collections;
32import java.util.List;
33
34/**
35 * This is a Keyboard class where you can add keys dynamically shown in a grid layout
36 */
37final class DynamicGridKeyboard extends Keyboard {
38    private static final String TAG = DynamicGridKeyboard.class.getSimpleName();
39    private static final int TEMPLATE_KEY_CODE_0 = 0x30;
40    private static final int TEMPLATE_KEY_CODE_1 = 0x31;
41    private final Object mLock = new Object();
42
43    private final SharedPreferences mPrefs;
44    private final int mHorizontalStep;
45    private final int mVerticalStep;
46    private final int mColumnsNum;
47    private final int mMaxKeyCount;
48    private final boolean mIsRecents;
49    private final ArrayDeque<GridKey> mGridKeys = new ArrayDeque<>();
50    private final ArrayDeque<Key> mPendingKeys = new ArrayDeque<>();
51
52    private List<Key> mCachedGridKeys;
53
54    public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templateKeyboard,
55            final int maxKeyCount, final int categoryId) {
56        super(templateKeyboard);
57        final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0);
58        final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1);
59        mHorizontalStep = Math.abs(key1.getX() - key0.getX());
60        mVerticalStep = key0.getHeight() + mVerticalGap;
61        mColumnsNum = mBaseWidth / mHorizontalStep;
62        mMaxKeyCount = maxKeyCount;
63        mIsRecents = categoryId == EmojiCategory.ID_RECENTS;
64        mPrefs = prefs;
65    }
66
67    private Key getTemplateKey(final int code) {
68        for (final Key key : super.getSortedKeys()) {
69            if (key.getCode() == code) {
70                return key;
71            }
72        }
73        throw new RuntimeException("Can't find template key: code=" + code);
74    }
75
76    public void addPendingKey(final Key usedKey) {
77        synchronized (mLock) {
78            mPendingKeys.addLast(usedKey);
79        }
80    }
81
82    public void flushPendingRecentKeys() {
83        synchronized (mLock) {
84            while (!mPendingKeys.isEmpty()) {
85                addKey(mPendingKeys.pollFirst(), true);
86            }
87            saveRecentKeys();
88        }
89    }
90
91    public void addKeyFirst(final Key usedKey) {
92        addKey(usedKey, true);
93        if (mIsRecents) {
94            saveRecentKeys();
95        }
96    }
97
98    public void addKeyLast(final Key usedKey) {
99        addKey(usedKey, false);
100    }
101
102    private void addKey(final Key usedKey, final boolean addFirst) {
103        if (usedKey == null) {
104            return;
105        }
106        synchronized (mLock) {
107            mCachedGridKeys = null;
108            final GridKey key = new GridKey(usedKey);
109            while (mGridKeys.remove(key)) {
110                // Remove duplicate keys.
111            }
112            if (addFirst) {
113                mGridKeys.addFirst(key);
114            } else {
115                mGridKeys.addLast(key);
116            }
117            while (mGridKeys.size() > mMaxKeyCount) {
118                mGridKeys.removeLast();
119            }
120            int index = 0;
121            for (final GridKey gridKey : mGridKeys) {
122                final int keyX0 = getKeyX0(index);
123                final int keyY0 = getKeyY0(index);
124                final int keyX1 = getKeyX1(index);
125                final int keyY1 = getKeyY1(index);
126                gridKey.updateCoordinates(keyX0, keyY0, keyX1, keyY1);
127                index++;
128            }
129        }
130    }
131
132    private void saveRecentKeys() {
133        final ArrayList<Object> keys = new ArrayList<>();
134        for (final Key key : mGridKeys) {
135            if (key.getOutputText() != null) {
136                keys.add(key.getOutputText());
137            } else {
138                keys.add(key.getCode());
139            }
140        }
141        final String jsonStr = JsonUtils.listToJsonStr(keys);
142        Settings.writeEmojiRecentKeys(mPrefs, jsonStr);
143    }
144
145    private static Key getKeyByCode(final Collection<DynamicGridKeyboard> keyboards,
146            final int code) {
147        for (final DynamicGridKeyboard keyboard : keyboards) {
148            for (final Key key : keyboard.getSortedKeys()) {
149                if (key.getCode() == code) {
150                    return key;
151                }
152            }
153        }
154        return null;
155    }
156
157    private static Key getKeyByOutputText(final Collection<DynamicGridKeyboard> keyboards,
158            final String outputText) {
159        for (final DynamicGridKeyboard keyboard : keyboards) {
160            for (final Key key : keyboard.getSortedKeys()) {
161                if (outputText.equals(key.getOutputText())) {
162                    return key;
163                }
164            }
165        }
166        return null;
167    }
168
169    public void loadRecentKeys(final Collection<DynamicGridKeyboard> keyboards) {
170        final String str = Settings.readEmojiRecentKeys(mPrefs);
171        final List<Object> keys = JsonUtils.jsonStrToList(str);
172        for (final Object o : keys) {
173            final Key key;
174            if (o instanceof Integer) {
175                final int code = (Integer)o;
176                key = getKeyByCode(keyboards, code);
177            } else if (o instanceof String) {
178                final String outputText = (String)o;
179                key = getKeyByOutputText(keyboards, outputText);
180            } else {
181                Log.w(TAG, "Invalid object: " + o);
182                continue;
183            }
184            addKeyLast(key);
185        }
186    }
187
188    private int getKeyX0(final int index) {
189        final int column = index % mColumnsNum;
190        return column * mHorizontalStep;
191    }
192
193    private int getKeyX1(final int index) {
194        final int column = index % mColumnsNum + 1;
195        return column * mHorizontalStep;
196    }
197
198    private int getKeyY0(final int index) {
199        final int row = index / mColumnsNum;
200        return row * mVerticalStep + mVerticalGap / 2;
201    }
202
203    private int getKeyY1(final int index) {
204        final int row = index / mColumnsNum + 1;
205        return row * mVerticalStep + mVerticalGap / 2;
206    }
207
208    @Override
209    public List<Key> getSortedKeys() {
210        synchronized (mLock) {
211            if (mCachedGridKeys != null) {
212                return mCachedGridKeys;
213            }
214            final ArrayList<Key> cachedKeys = new ArrayList<Key>(mGridKeys);
215            mCachedGridKeys = Collections.unmodifiableList(cachedKeys);
216            return mCachedGridKeys;
217        }
218    }
219
220    @Override
221    public List<Key> getNearestKeys(final int x, final int y) {
222        // TODO: Calculate the nearest key index in mGridKeys from x and y.
223        return getSortedKeys();
224    }
225
226    static final class GridKey extends Key {
227        private int mCurrentX;
228        private int mCurrentY;
229
230        public GridKey(final Key originalKey) {
231            super(originalKey);
232        }
233
234        public void updateCoordinates(final int x0, final int y0, final int x1, final int y1) {
235            mCurrentX = x0;
236            mCurrentY = y0;
237            getHitBox().set(x0, y0, x1, y1);
238        }
239
240        @Override
241        public int getX() {
242            return mCurrentX;
243        }
244
245        @Override
246        public int getY() {
247            return mCurrentY;
248        }
249
250        @Override
251        public boolean equals(final Object o) {
252            if (!(o instanceof Key)) return false;
253            final Key key = (Key)o;
254            if (getCode() != key.getCode()) return false;
255            if (!TextUtils.equals(getLabel(), key.getLabel())) return false;
256            return TextUtils.equals(getOutputText(), key.getOutputText());
257        }
258
259        @Override
260        public String toString() {
261            return "GridKey: " + super.toString();
262        }
263    }
264}
265