1 /* 2 * Copyright (C) 2006 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 android.text.method; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.icu.text.DecimalFormatSymbols; 22 import android.text.Editable; 23 import android.text.InputFilter; 24 import android.text.Selection; 25 import android.text.Spannable; 26 import android.text.SpannableStringBuilder; 27 import android.text.Spanned; 28 import android.text.format.DateFormat; 29 import android.view.KeyEvent; 30 import android.view.View; 31 32 import java.util.Collection; 33 import java.util.Locale; 34 35 /** 36 * For numeric text entry 37 * <p></p> 38 * As for all implementations of {@link KeyListener}, this class is only concerned 39 * with hardware keyboards. Software input methods have no obligation to trigger 40 * the methods in this class. 41 */ 42 @android.ravenwood.annotation.RavenwoodKeepWholeClass 43 public abstract class NumberKeyListener extends BaseKeyListener 44 implements InputFilter 45 { 46 /** 47 * You can say which characters you can accept. 48 */ 49 @NonNull getAcceptedChars()50 protected abstract char[] getAcceptedChars(); 51 lookup(KeyEvent event, Spannable content)52 protected int lookup(KeyEvent event, Spannable content) { 53 return event.getMatch(getAcceptedChars(), getMetaState(content, event)); 54 } 55 filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)56 public CharSequence filter(CharSequence source, int start, int end, 57 Spanned dest, int dstart, int dend) { 58 char[] accept = getAcceptedChars(); 59 boolean filter = false; 60 61 int i; 62 for (i = start; i < end; i++) { 63 if (!ok(accept, source.charAt(i))) { 64 break; 65 } 66 } 67 68 if (i == end) { 69 // It was all OK. 70 return null; 71 } 72 73 if (end - start == 1) { 74 // It was not OK, and there is only one char, so nothing remains. 75 return ""; 76 } 77 78 SpannableStringBuilder filtered = 79 new SpannableStringBuilder(source, start, end); 80 i -= start; 81 end -= start; 82 83 int len = end - start; 84 // Only count down to i because the chars before that were all OK. 85 for (int j = end - 1; j >= i; j--) { 86 if (!ok(accept, source.charAt(j))) { 87 filtered.delete(j, j + 1); 88 } 89 } 90 91 return filtered; 92 } 93 ok(char[] accept, char c)94 protected static boolean ok(char[] accept, char c) { 95 for (int i = accept.length - 1; i >= 0; i--) { 96 if (accept[i] == c) { 97 return true; 98 } 99 } 100 101 return false; 102 } 103 104 @Override onKeyDown(View view, Editable content, int keyCode, KeyEvent event)105 public boolean onKeyDown(View view, Editable content, 106 int keyCode, KeyEvent event) { 107 int selStart, selEnd; 108 109 { 110 int a = Selection.getSelectionStart(content); 111 int b = Selection.getSelectionEnd(content); 112 113 selStart = Math.min(a, b); 114 selEnd = Math.max(a, b); 115 } 116 117 if (selStart < 0 || selEnd < 0) { 118 selStart = selEnd = 0; 119 Selection.setSelection(content, 0); 120 } 121 122 int i = event != null ? lookup(event, content) : 0; 123 int repeatCount = event != null ? event.getRepeatCount() : 0; 124 if (repeatCount == 0) { 125 if (i != 0) { 126 if (selStart != selEnd) { 127 Selection.setSelection(content, selEnd); 128 } 129 130 content.replace(selStart, selEnd, String.valueOf((char) i)); 131 132 adjustMetaAfterKeypress(content); 133 return true; 134 } 135 } else if (i == '0' && repeatCount == 1) { 136 // Pretty hackish, it replaces the 0 with the + 137 138 if (selStart == selEnd && selEnd > 0 && 139 content.charAt(selStart - 1) == '0') { 140 content.replace(selStart - 1, selEnd, String.valueOf('+')); 141 adjustMetaAfterKeypress(content); 142 return true; 143 } 144 } 145 146 adjustMetaAfterKeypress(content); 147 return super.onKeyDown(view, content, keyCode, event); 148 } 149 150 /* package */ 151 @Nullable addDigits(@onNull Collection<Character> collection, @Nullable Locale locale)152 static boolean addDigits(@NonNull Collection<Character> collection, @Nullable Locale locale) { 153 if (locale == null) { 154 return false; 155 } 156 final String[] digits = DecimalFormatSymbols.getInstance(locale).getDigitStrings(); 157 for (int i = 0; i < 10; i++) { 158 if (digits[i].length() > 1) { // multi-codeunit digits. Not supported. 159 return false; 160 } 161 collection.add(Character.valueOf(digits[i].charAt(0))); 162 } 163 return true; 164 } 165 166 // From http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns 167 private static final String DATE_TIME_FORMAT_SYMBOLS = 168 "GyYuUrQqMLlwWdDFgEecabBhHKkjJCmsSAzZOvVXx"; 169 private static final char SINGLE_QUOTE = '\''; 170 171 /* package */ addFormatCharsFromSkeleton( @onNull Collection<Character> collection, @Nullable Locale locale, @NonNull String skeleton, @NonNull String symbolsToIgnore)172 static boolean addFormatCharsFromSkeleton( 173 @NonNull Collection<Character> collection, @Nullable Locale locale, 174 @NonNull String skeleton, @NonNull String symbolsToIgnore) { 175 if (locale == null) { 176 return false; 177 } 178 final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton); 179 boolean outsideQuotes = true; 180 for (int i = 0; i < pattern.length(); i++) { 181 final char ch = pattern.charAt(i); 182 if (Character.isSurrogate(ch)) { // characters outside BMP are not supported. 183 return false; 184 } else if (ch == SINGLE_QUOTE) { 185 outsideQuotes = !outsideQuotes; 186 // Single quote characters should be considered if and only if they follow 187 // another single quote. 188 if (i == 0 || pattern.charAt(i - 1) != SINGLE_QUOTE) { 189 continue; 190 } 191 } 192 193 if (outsideQuotes) { 194 if (symbolsToIgnore.indexOf(ch) != -1) { 195 // Skip expected pattern characters. 196 continue; 197 } else if (DATE_TIME_FORMAT_SYMBOLS.indexOf(ch) != -1) { 198 // An unexpected symbols is seen. We've failed. 199 return false; 200 } 201 } 202 // If we are here, we are either inside quotes, or we have seen a non-pattern 203 // character outside quotes. So ch is a valid character in a date. 204 collection.add(Character.valueOf(ch)); 205 } 206 return true; 207 } 208 209 /* package */ addFormatCharsFromSkeletons( @onNull Collection<Character> collection, @Nullable Locale locale, @NonNull String[] skeletons, @NonNull String symbolsToIgnore)210 static boolean addFormatCharsFromSkeletons( 211 @NonNull Collection<Character> collection, @Nullable Locale locale, 212 @NonNull String[] skeletons, @NonNull String symbolsToIgnore) { 213 for (int i = 0; i < skeletons.length; i++) { 214 final boolean success = addFormatCharsFromSkeleton( 215 collection, locale, skeletons[i], symbolsToIgnore); 216 if (!success) { 217 return false; 218 } 219 } 220 return true; 221 } 222 223 224 /* package */ addAmPmChars(@onNull Collection<Character> collection, @Nullable Locale locale)225 static boolean addAmPmChars(@NonNull Collection<Character> collection, 226 @Nullable Locale locale) { 227 if (locale == null) { 228 return false; 229 } 230 final String[] amPm = DateFormat.getIcuDateFormatSymbols(locale).getAmPmStrings(); 231 for (int i = 0; i < amPm.length; i++) { 232 for (int j = 0; j < amPm[i].length(); j++) { 233 final char ch = amPm[i].charAt(j); 234 if (Character.isBmpCodePoint(ch)) { 235 collection.add(Character.valueOf(ch)); 236 } else { // We don't support non-BMP characters. 237 return false; 238 } 239 } 240 } 241 return true; 242 } 243 244 /* package */ 245 @NonNull collectionToArray(@onNull Collection<Character> chars)246 static char[] collectionToArray(@NonNull Collection<Character> chars) { 247 final char[] result = new char[chars.size()]; 248 int i = 0; 249 for (Character ch : chars) { 250 result[i++] = ch; 251 } 252 return result; 253 } 254 } 255