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