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