1 /* 2 * Copyright (C) 2022 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.internal.inputmethod; 18 19 import static java.lang.annotation.RetentionPolicy.SOURCE; 20 21 import android.annotation.AnyThread; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.content.ComponentName; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.text.TextUtils.SimpleStringSplitter; 28 import android.view.inputmethod.InputMethodInfo; 29 import android.view.inputmethod.InputMethodSubtype; 30 31 import java.lang.annotation.ElementType; 32 import java.lang.annotation.Retention; 33 import java.lang.annotation.Target; 34 import java.security.InvalidParameterException; 35 import java.util.Objects; 36 37 /** 38 * A stable and serializable identifier for the pair of {@link InputMethodInfo#getId()} and 39 * {@link android.view.inputmethod.InputMethodSubtype}. 40 * 41 * <p>To save {@link InputMethodSubtypeHandle} to storage, call {@link #toStringHandle()} to get a 42 * {@link String} handle and just save it. Once you load a {@link String} handle, you can obtain a 43 * {@link InputMethodSubtypeHandle} instance from {@link #of(String)}.</p> 44 * 45 * <p>For better readability, consider specifying {@link RawHandle} annotation to {@link String} 46 * object when it is a raw {@link String} handle.</p> 47 */ 48 public final class InputMethodSubtypeHandle implements Parcelable { 49 private static final String SUBTYPE_TAG = "subtype"; 50 private static final char DATA_SEPARATOR = ':'; 51 52 /** 53 * Can be used to annotate {@link String} object if it is raw handle format. 54 */ 55 @Retention(SOURCE) 56 @Target({ElementType.METHOD, ElementType.FIELD, ElementType.LOCAL_VARIABLE, 57 ElementType.PARAMETER}) 58 public @interface RawHandle { 59 } 60 61 /** 62 * The main content of this {@link InputMethodSubtypeHandle}. Is designed to be safe to be 63 * saved into storage. 64 */ 65 @RawHandle 66 private final String mHandle; 67 68 /** 69 * Encode {@link InputMethodInfo} and {@link InputMethodSubtype#hashCode()} into 70 * {@link RawHandle}. 71 * 72 * @param imeId {@link InputMethodInfo#getId()} to be used. 73 * @param subtypeHashCode {@link InputMethodSubtype#hashCode()} to be used. 74 * @return The encoded {@link RawHandle} string. 75 */ 76 @AnyThread 77 @RawHandle 78 @NonNull encodeHandle(@onNull String imeId, int subtypeHashCode)79 private static String encodeHandle(@NonNull String imeId, int subtypeHashCode) { 80 return imeId + DATA_SEPARATOR + SUBTYPE_TAG + DATA_SEPARATOR + subtypeHashCode; 81 } 82 InputMethodSubtypeHandle(@onNull String handle)83 private InputMethodSubtypeHandle(@NonNull String handle) { 84 mHandle = handle; 85 } 86 87 /** 88 * Creates {@link InputMethodSubtypeHandle} from {@link InputMethodInfo} and 89 * {@link InputMethodSubtype}. 90 * 91 * @param imi {@link InputMethodInfo} to be used. 92 * @param subtype {@link InputMethodSubtype} to be used. 93 * @return A {@link InputMethodSubtypeHandle} object. 94 */ 95 @AnyThread 96 @NonNull of( @onNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype)97 public static InputMethodSubtypeHandle of( 98 @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { 99 final int subtypeHashCode = 100 subtype != null ? subtype.hashCode() : InputMethodSubtype.SUBTYPE_ID_NONE; 101 return new InputMethodSubtypeHandle(encodeHandle(imi.getId(), subtypeHashCode)); 102 } 103 104 /** 105 * Creates {@link InputMethodSubtypeHandle} from a {@link RawHandle} {@link String}, which can 106 * be obtained by {@link #toStringHandle()}. 107 * 108 * @param stringHandle {@link RawHandle} {@link String} to be parsed. 109 * @return A {@link InputMethodSubtypeHandle} object. 110 * @throws NullPointerException when {@code stringHandle} is {@code null} 111 * @throws InvalidParameterException when {@code stringHandle} is not a valid {@link RawHandle}. 112 */ 113 @AnyThread 114 @NonNull of(@awHandle @onNull String stringHandle)115 public static InputMethodSubtypeHandle of(@RawHandle @NonNull String stringHandle) { 116 final SimpleStringSplitter splitter = new SimpleStringSplitter(DATA_SEPARATOR); 117 splitter.setString(Objects.requireNonNull(stringHandle)); 118 if (!splitter.hasNext()) { 119 throw new InvalidParameterException("Invalid handle=" + stringHandle); 120 } 121 final String imeId = splitter.next(); 122 final ComponentName componentName = ComponentName.unflattenFromString(imeId); 123 if (componentName == null) { 124 throw new InvalidParameterException("Invalid handle=" + stringHandle); 125 } 126 // TODO: Consolidate IME ID validation logic into one place. 127 if (!Objects.equals(componentName.flattenToShortString(), imeId)) { 128 throw new InvalidParameterException("Invalid handle=" + stringHandle); 129 } 130 if (!splitter.hasNext()) { 131 throw new InvalidParameterException("Invalid handle=" + stringHandle); 132 } 133 final String source = splitter.next(); 134 if (!Objects.equals(source, SUBTYPE_TAG)) { 135 throw new InvalidParameterException("Invalid handle=" + stringHandle); 136 } 137 if (!splitter.hasNext()) { 138 throw new InvalidParameterException("Invalid handle=" + stringHandle); 139 } 140 final String hashCodeStr = splitter.next(); 141 if (splitter.hasNext()) { 142 throw new InvalidParameterException("Invalid handle=" + stringHandle); 143 } 144 final int subtypeHashCode; 145 try { 146 subtypeHashCode = Integer.parseInt(hashCodeStr); 147 } catch (NumberFormatException ignore) { 148 throw new InvalidParameterException("Invalid handle=" + stringHandle); 149 } 150 151 // Redundant expressions (e.g. "0001" instead of "1") are not allowed. 152 if (!Objects.equals(encodeHandle(imeId, subtypeHashCode), stringHandle)) { 153 throw new InvalidParameterException("Invalid handle=" + stringHandle); 154 } 155 156 return new InputMethodSubtypeHandle(stringHandle); 157 } 158 159 /** 160 * @return {@link ComponentName} of the input method. 161 * @see InputMethodInfo#getComponent() 162 */ 163 @AnyThread 164 @NonNull getComponentName()165 public ComponentName getComponentName() { 166 return ComponentName.unflattenFromString(getImeId()); 167 } 168 169 /** 170 * @return IME ID. 171 * @see InputMethodInfo#getId() 172 */ 173 @AnyThread 174 @NonNull getImeId()175 public String getImeId() { 176 return mHandle.substring(0, mHandle.indexOf(DATA_SEPARATOR)); 177 } 178 179 /** 180 * @return {@link RawHandle} {@link String} data that should be stable and persistable. 181 * @see #of(String) 182 */ 183 @RawHandle 184 @AnyThread 185 @NonNull toStringHandle()186 public String toStringHandle() { 187 return mHandle; 188 } 189 190 /** 191 * {@inheritDoc} 192 */ 193 @AnyThread 194 @Override equals(Object obj)195 public boolean equals(Object obj) { 196 if (!(obj instanceof InputMethodSubtypeHandle)) { 197 return false; 198 } 199 final InputMethodSubtypeHandle that = (InputMethodSubtypeHandle) obj; 200 return Objects.equals(mHandle, that.mHandle); 201 } 202 203 /** 204 * {@inheritDoc} 205 */ 206 @AnyThread 207 @Override hashCode()208 public int hashCode() { 209 return Objects.hashCode(mHandle); 210 } 211 212 /** 213 * {@inheritDoc} 214 */ 215 @AnyThread 216 @NonNull 217 @Override toString()218 public String toString() { 219 return "InputMethodSubtypeHandle{mHandle=" + mHandle + "}"; 220 } 221 222 /** 223 * {@link Creator} for parcelable. 224 */ 225 public static final Creator<InputMethodSubtypeHandle> CREATOR = new Creator<>() { 226 @Override 227 public InputMethodSubtypeHandle createFromParcel(Parcel in) { 228 return of(in.readString8()); 229 } 230 231 @Override 232 public InputMethodSubtypeHandle[] newArray(int size) { 233 return new InputMethodSubtypeHandle[size]; 234 } 235 }; 236 237 /** 238 * {@inheritDoc} 239 */ 240 @AnyThread 241 @Override describeContents()242 public int describeContents() { 243 return 0; 244 } 245 246 /** 247 * {@inheritDoc} 248 */ 249 @AnyThread 250 @Override writeToParcel(@onNull Parcel dest, int flags)251 public void writeToParcel(@NonNull Parcel dest, int flags) { 252 dest.writeString8(toStringHandle()); 253 } 254 } 255