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 
17 package com.android.inputmethod.keyboard.layout.expected;
18 
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Locale;
22 
23 /**
24  * This class builds an expected keyboard for unit test.
25  *
26  * An expected keyboard is an array of rows, and a row consists of an array of {@link ExpectedKey}s.
27  * Each row may have different number of {@link ExpectedKey}s. While building an expected keyboard,
28  * an {@link ExpectedKey} can be specified by a row number and a column number, both numbers starts
29  * from 1.
30  */
31 public final class ExpectedKeyboardBuilder extends AbstractKeyboardBuilder<ExpectedKey> {
ExpectedKeyboardBuilder()32     public ExpectedKeyboardBuilder() {
33         super();
34     }
35 
ExpectedKeyboardBuilder(final ExpectedKey[][] rows)36     public ExpectedKeyboardBuilder(final ExpectedKey[][] rows) {
37         super(rows);
38     }
39 
40     @Override
defaultElement()41     protected ExpectedKey defaultElement() {
42         return ExpectedKey.EMPTY_KEY;
43     }
44 
45     @Override
newArray(final int size)46     ExpectedKey[] newArray(final int size) {
47         return new ExpectedKey[size];
48     }
49 
50     @Override
newArrayOfArray(final int size)51     ExpectedKey[][] newArrayOfArray(final int size) {
52         return new ExpectedKey[size][];
53     }
54 
55     @Override
build()56     public ExpectedKey[][] build() {
57         return super.build();
58     }
59 
60     // A replacement job to be performed.
61     private interface ReplaceJob {
62         // Returns a {@link ExpectedKey} objects to replace.
replacingKeys(final ExpectedKey oldKey)63         ExpectedKey[] replacingKeys(final ExpectedKey oldKey);
64         // Return true if replacing should be stopped at first occurrence.
stopAtFirstOccurrence()65         boolean stopAtFirstOccurrence();
66     }
67 
replaceKeyAt(final ExpectedKey[] keys, final int columnIndex, final ExpectedKey[] replacingKeys)68     private static ExpectedKey[] replaceKeyAt(final ExpectedKey[] keys, final int columnIndex,
69             final ExpectedKey[] replacingKeys) {
70         // Optimization for replacing a key with another key.
71         if (replacingKeys.length == 1) {
72             keys[columnIndex] = replacingKeys[0];
73             return keys;
74         }
75         final int newLength = keys.length - 1 + replacingKeys.length;
76         // Remove the key at columnIndex.
77         final ExpectedKey[] newKeys = Arrays.copyOf(keys, newLength);
78         System.arraycopy(keys, columnIndex + 1, newKeys, columnIndex + replacingKeys.length,
79                 keys.length - 1 - columnIndex);
80         // Insert replacing keys at columnIndex.
81         System.arraycopy(replacingKeys, 0, newKeys, columnIndex, replacingKeys.length);
82         return newKeys;
83 
84     }
85 
86     // Replace key(s) that has the specified visual.
replaceKeyOf(final ExpectedKeyVisual visual, final ReplaceJob job)87     private void replaceKeyOf(final ExpectedKeyVisual visual, final ReplaceJob job) {
88         int replacedCount = 0;
89         final int rowCount = getRowCount();
90         for (int row = 1; row <= rowCount; row++) {
91             ExpectedKey[] keys = getRowAt(row);
92             for (int columnIndex = 0; columnIndex < keys.length; /* nothing */) {
93                 final ExpectedKey currentKey = keys[columnIndex];
94                 if (!currentKey.getVisual().hasSameKeyVisual(visual)) {
95                     columnIndex++;
96                     continue;
97                 }
98                 final ExpectedKey[] replacingKeys = job.replacingKeys(currentKey);
99                 keys = replaceKeyAt(keys, columnIndex, replacingKeys);
100                 columnIndex += replacingKeys.length;
101                 setRowAt(row, keys);
102                 replacedCount++;
103                 if (job.stopAtFirstOccurrence()) {
104                     return;
105                 }
106             }
107         }
108         if (replacedCount == 0) {
109             throw new RuntimeException(
110                     "Can't find key that has visual: " + visual + " in\n" + this);
111         }
112     }
113 
114     // Helper method to create {@link ExpectedKey} array by joining {@link ExpectedKey},
115     // {@link ExpectedKey} array, and {@link String}.
joinKeys(final Object ... keys)116     static ExpectedKey[] joinKeys(final Object ... keys) {
117         final ArrayList<ExpectedKey> list = new ArrayList<>();
118         for (final Object key : keys) {
119             if (key instanceof ExpectedKey) {
120                 list.add((ExpectedKey)key);
121             } else if (key instanceof ExpectedKey[]) {
122                 list.addAll(Arrays.asList((ExpectedKey[])key));
123             } else if (key instanceof String) {
124                 list.add(ExpectedKey.newInstance((String)key));
125             } else {
126                 throw new RuntimeException("Unknown expected key type: " + key);
127             }
128         }
129         return list.toArray(new ExpectedKey[list.size()]);
130     }
131 
132     /**
133      * Set the row with specified keys.
134      * @param row the row number to set keys.
135      * @param keys the keys to be set at <code>row</code>. Each key can be {@link ExpectedKey},
136      *        {@link ExpectedKey} array, and {@link String}.
137      * @return this builder.
138      */
setKeysOfRow(final int row, final Object ... keys)139     public ExpectedKeyboardBuilder setKeysOfRow(final int row, final Object ... keys) {
140         setRowAt(row, joinKeys(keys));
141         return this;
142     }
143 
144     /**
145      * Set the "more keys" of the key that has the specified label.
146      * @param label the label of the key to set the "more keys".
147      * @param moreKeys the array of "more key" to be set. Each "more key" can be
148      *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
149      * @return this builder.
150      */
setMoreKeysOf(final String label, final Object ... moreKeys)151     public ExpectedKeyboardBuilder setMoreKeysOf(final String label, final Object ... moreKeys) {
152         setMoreKeysOf(ExpectedKeyVisual.newInstance(label), joinKeys(moreKeys));
153         return this;
154     }
155 
156     /**
157      * Set the "more keys" of the key that has the specified icon.
158      * @param iconId the icon id of the key to set the "more keys".
159      * @param moreKeys the array of "more key" to be set. Each "more key" can be
160      *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
161      * @return this builder.
162      */
setMoreKeysOf(final int iconId, final Object ... moreKeys)163     public ExpectedKeyboardBuilder setMoreKeysOf(final int iconId, final Object ... moreKeys) {
164         setMoreKeysOf(ExpectedKeyVisual.newInstance(iconId), joinKeys(moreKeys));
165         return this;
166     }
167 
setMoreKeysOf(final ExpectedKeyVisual visual, final ExpectedKey[] moreKeys)168     private void setMoreKeysOf(final ExpectedKeyVisual visual, final ExpectedKey[] moreKeys) {
169         replaceKeyOf(visual, new ReplaceJob() {
170             @Override
171             public ExpectedKey[] replacingKeys(final ExpectedKey oldKey) {
172                 return new ExpectedKey[] { oldKey.setMoreKeys(moreKeys) };
173             }
174             @Override
175             public boolean stopAtFirstOccurrence() {
176                 return true;
177             }
178         });
179     }
180 
181     /**
182      * Set the "additional more keys position" of the key that has the specified label.
183      * @param label the label of the key to set the "additional more keys".
184      * @param additionalMoreKeysPosition the position in the "more keys" where
185      *        "additional more keys" will be merged. The position starts from 1.
186      * @return this builder.
187      */
setAdditionalMoreKeysPositionOf(final String label, final int additionalMoreKeysPosition)188     public ExpectedKeyboardBuilder setAdditionalMoreKeysPositionOf(final String label,
189             final int additionalMoreKeysPosition) {
190         final int additionalMoreKeysIndex = additionalMoreKeysPosition - 1;
191         if (additionalMoreKeysIndex < 0) {
192             throw new RuntimeException("Illegal additional more keys position: "
193                     + additionalMoreKeysPosition);
194         }
195         final ExpectedKeyVisual visual = ExpectedKeyVisual.newInstance(label);
196         replaceKeyOf(visual, new ReplaceJob() {
197             @Override
198             public ExpectedKey[] replacingKeys(final ExpectedKey oldKey) {
199                 return new ExpectedKey[] {
200                         oldKey.setAdditionalMoreKeysIndex(additionalMoreKeysIndex)
201                 };
202             }
203             @Override
204             public boolean stopAtFirstOccurrence() {
205                 return true;
206             }
207         });
208         return this;
209     }
210 
211     /**
212      * Insert the keys at specified position.
213      * @param row the row number to insert the <code>keys</code>.
214      * @param column the column number to insert the <code>keys</code>.
215      * @param keys the array of keys to insert at <code>row,column</code>. Each key can be
216      *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
217      * @return this builder.
218      * @throws RuntimeException if <code>row</code> or <code>column</code> is illegal.
219      */
insertKeysAtRow(final int row, final int column, final Object ... keys)220     public ExpectedKeyboardBuilder insertKeysAtRow(final int row, final int column,
221             final Object ... keys) {
222         final ExpectedKey[] expectedKeys = joinKeys(keys);
223         for (int index = 0; index < keys.length; index++) {
224             setElementAt(row, column + index, expectedKeys[index], true /* insert */);
225         }
226         return this;
227     }
228 
229     /**
230      * Add the keys on the left most of the row.
231      * @param row the row number to add the <code>keys</code>.
232      * @param keys the array of keys to add on the left most of the row. Each key can be
233      *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
234      * @return this builder.
235      * @throws RuntimeException if <code>row</code> is illegal.
236      */
addKeysOnTheLeftOfRow(final int row, final Object ... keys)237     public ExpectedKeyboardBuilder addKeysOnTheLeftOfRow(final int row,
238             final Object ... keys) {
239         final ExpectedKey[] expectedKeys = joinKeys(keys);
240         // Keys should be inserted from the last to preserve the order.
241         for (int index = keys.length - 1; index >= 0; index--) {
242             setElementAt(row, 1, expectedKeys[index], true /* insert */);
243         }
244         return this;
245     }
246 
247     /**
248      * Add the keys on the right most of the row.
249      * @param row the row number to add the <code>keys</code>.
250      * @param keys the array of keys to add on the right most of the row. Each key can be
251      *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
252      * @return this builder.
253      * @throws RuntimeException if <code>row</code> is illegal.
254      */
addKeysOnTheRightOfRow(final int row, final Object ... keys)255     public ExpectedKeyboardBuilder addKeysOnTheRightOfRow(final int row,
256             final Object ... keys) {
257         final int rightEnd = getRowAt(row).length + 1;
258         insertKeysAtRow(row, rightEnd, keys);
259         return this;
260     }
261 
262     /**
263      * Replace the most top-left key that has the specified label with the new keys.
264      * @param label the label of the key to set <code>newKeys</code>.
265      * @param newKeys the keys to be set. Each key can be {@link ExpectedKey}, {@link ExpectedKey}
266      *        array, and {@link String}.
267      * @return this builder.
268      */
replaceKeyOfLabel(final String label, final Object ... newKeys)269     public ExpectedKeyboardBuilder replaceKeyOfLabel(final String label,
270             final Object ... newKeys) {
271         final ExpectedKeyVisual visual = ExpectedKeyVisual.newInstance(label);
272         replaceKeyOf(visual, new ReplaceJob() {
273             @Override
274             public ExpectedKey[] replacingKeys(final ExpectedKey oldKey) {
275                 return joinKeys(newKeys);
276             }
277             @Override
278             public boolean stopAtFirstOccurrence() {
279                 return true;
280             }
281         });
282         return this;
283     }
284 
285     /**
286      * Replace the all specified keys with the new keys.
287      * @param key the key to be replaced by <code>newKeys</code>.
288      * @param newKeys the keys to be set. Each key can be {@link ExpectedKey}, {@link ExpectedKey}
289      *        array, and {@link String}.
290      * @return this builder.
291      */
replaceKeysOfAll(final ExpectedKey key, final Object ... newKeys)292     public ExpectedKeyboardBuilder replaceKeysOfAll(final ExpectedKey key,
293             final Object ... newKeys) {
294         replaceKeyOf(key.getVisual(), new ReplaceJob() {
295             @Override
296             public ExpectedKey[] replacingKeys(final ExpectedKey oldKey) {
297                 return joinKeys(newKeys);
298             }
299             @Override
300             public boolean stopAtFirstOccurrence() {
301                 return false;
302             }
303         });
304         return this;
305     }
306 
307     /**
308      * Convert all keys of this keyboard builder to upper case keys.
309      * @param locale the locale used to convert cases.
310      * @return this builder
311      */
toUpperCase(final Locale locale)312     public ExpectedKeyboardBuilder toUpperCase(final Locale locale) {
313         final int rowCount = getRowCount();
314         for (int row = 1; row <= rowCount; row++) {
315             final ExpectedKey[] lowerCaseKeys = getRowAt(row);
316             final ExpectedKey[] upperCaseKeys = new ExpectedKey[lowerCaseKeys.length];
317             for (int columnIndex = 0; columnIndex < lowerCaseKeys.length; columnIndex++) {
318                 upperCaseKeys[columnIndex] = lowerCaseKeys[columnIndex].toUpperCase(locale);
319             }
320             setRowAt(row, upperCaseKeys);
321         }
322         return this;
323     }
324 
325     @Override
toString()326     public String toString() {
327         return toString(build());
328     }
329 
330     /**
331      * Convert the keyboard to human readable string.
332      * @param rows the keyboard to be converted to string.
333      * @return the human readable representation of <code>rows</code>.
334      */
toString(final ExpectedKey[][] rows)335     public static String toString(final ExpectedKey[][] rows) {
336         final StringBuilder sb = new StringBuilder();
337         for (int rowIndex = 0; rowIndex < rows.length; rowIndex++) {
338             if (rowIndex > 0) {
339                 sb.append("\n");
340             }
341             sb.append(Arrays.toString(rows[rowIndex]));
342         }
343         return sb.toString();
344     }
345 }
346