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> { 32 public ExpectedKeyboardBuilder() { 33 super(); 34 } 35 36 public ExpectedKeyboardBuilder(final ExpectedKey[][] rows) { 37 super(rows); 38 } 39 40 @Override 41 protected ExpectedKey defaultElement() { 42 return ExpectedKey.EMPTY_KEY; 43 } 44 45 @Override 46 ExpectedKey[] newArray(final int size) { 47 return new ExpectedKey[size]; 48 } 49 50 @Override 51 ExpectedKey[][] newArrayOfArray(final int size) { 52 return new ExpectedKey[size][]; 53 } 54 55 @Override 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. 63 ExpectedKey[] replacingKeys(final ExpectedKey oldKey); 64 // Return true if replacing should be stopped at first occurrence. 65 boolean stopAtFirstOccurrence(); 66 } 67 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. 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}. 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 */ 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 */ 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 */ 163 public ExpectedKeyboardBuilder setMoreKeysOf(final int iconId, final Object ... moreKeys) { 164 setMoreKeysOf(ExpectedKeyVisual.newInstance(iconId), joinKeys(moreKeys)); 165 return this; 166 } 167 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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