1 /*
2  * Copyright (C) 2016 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.view.inputmethod;
18 
19 import static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.os.Bundle;
25 
26 import java.lang.annotation.Retention;
27 import java.lang.reflect.Method;
28 import java.lang.reflect.Modifier;
29 import java.util.Collections;
30 import java.util.Map;
31 import java.util.WeakHashMap;
32 
33 /**
34  * @hide
35  */
36 public final class InputConnectionInspector {
37 
38     @Retention(SOURCE)
39     @IntDef({MissingMethodFlags.GET_SELECTED_TEXT,
40             MissingMethodFlags.SET_COMPOSING_REGION,
41             MissingMethodFlags.COMMIT_CORRECTION,
42             MissingMethodFlags.REQUEST_CURSOR_UPDATES,
43             MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS,
44             MissingMethodFlags.GET_HANDLER,
45             MissingMethodFlags.CLOSE_CONNECTION,
46             MissingMethodFlags.COMMIT_CONTENT,
47     })
48     public @interface MissingMethodFlags {
49         /**
50          * {@link InputConnection#getSelectedText(int)} is available in
51          * {@link android.os.Build.VERSION_CODES#GINGERBREAD} and later.
52          */
53         int GET_SELECTED_TEXT = 1 << 0;
54         /**
55          * {@link InputConnection#setComposingRegion(int, int)} is available in
56          * {@link android.os.Build.VERSION_CODES#GINGERBREAD} and later.
57          */
58         int SET_COMPOSING_REGION = 1 << 1;
59         /**
60          * {@link InputConnection#commitCorrection(CorrectionInfo)} is available in
61          * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and later.
62          */
63         int COMMIT_CORRECTION = 1 << 2;
64         /**
65          * {@link InputConnection#requestCursorUpdates(int)} is available in
66          * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later.
67          */
68         int REQUEST_CURSOR_UPDATES = 1 << 3;
69         /**
70          * {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)}} is available in
71          * {@link android.os.Build.VERSION_CODES#N} and later.
72          */
73         int DELETE_SURROUNDING_TEXT_IN_CODE_POINTS = 1 << 4;
74         /**
75          * {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)}} is available in
76          * {@link android.os.Build.VERSION_CODES#N} and later.
77          */
78         int GET_HANDLER = 1 << 5;
79         /**
80          * {@link InputConnection#closeConnection()}} is available in
81          * {@link android.os.Build.VERSION_CODES#N} and later.
82          */
83         int CLOSE_CONNECTION = 1 << 6;
84         /**
85          * {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} is available in
86          * {@link android.os.Build.VERSION_CODES#N} MR-1 and later.
87          */
88         int COMMIT_CONTENT = 1 << 7;
89     }
90 
91     private static final Map<Class, Integer> sMissingMethodsMap = Collections.synchronizedMap(
92             new WeakHashMap<>());
93 
94     @MissingMethodFlags
getMissingMethodFlags(@ullable final InputConnection ic)95     public static int getMissingMethodFlags(@Nullable final InputConnection ic) {
96         if (ic == null) {
97             return 0;
98         }
99         // Optimization for a known class.
100         if (ic instanceof BaseInputConnection) {
101             return 0;
102         }
103         // Optimization for a known class.
104         if (ic instanceof InputConnectionWrapper) {
105             return ((InputConnectionWrapper) ic).getMissingMethodFlags();
106         }
107         return getMissingMethodFlagsInternal(ic.getClass());
108     }
109 
110     @MissingMethodFlags
getMissingMethodFlagsInternal(@onNull final Class clazz)111     public static int getMissingMethodFlagsInternal(@NonNull final Class clazz) {
112         final Integer cachedFlags = sMissingMethodsMap.get(clazz);
113         if (cachedFlags != null) {
114             return cachedFlags;
115         }
116         int flags = 0;
117         if (!hasGetSelectedText(clazz)) {
118             flags |= MissingMethodFlags.GET_SELECTED_TEXT;
119         }
120         if (!hasSetComposingRegion(clazz)) {
121             flags |= MissingMethodFlags.SET_COMPOSING_REGION;
122         }
123         if (!hasCommitCorrection(clazz)) {
124             flags |= MissingMethodFlags.COMMIT_CORRECTION;
125         }
126         if (!hasRequestCursorUpdate(clazz)) {
127             flags |= MissingMethodFlags.REQUEST_CURSOR_UPDATES;
128         }
129         if (!hasDeleteSurroundingTextInCodePoints(clazz)) {
130             flags |= MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS;
131         }
132         if (!hasGetHandler(clazz)) {
133             flags |= MissingMethodFlags.GET_HANDLER;
134         }
135         if (!hasCloseConnection(clazz)) {
136             flags |= MissingMethodFlags.CLOSE_CONNECTION;
137         }
138         if (!hasCommitContent(clazz)) {
139             flags |= MissingMethodFlags.COMMIT_CONTENT;
140         }
141         sMissingMethodsMap.put(clazz, flags);
142         return flags;
143     }
144 
hasGetSelectedText(@onNull final Class clazz)145     private static boolean hasGetSelectedText(@NonNull final Class clazz) {
146         try {
147             final Method method = clazz.getMethod("getSelectedText", int.class);
148             return !Modifier.isAbstract(method.getModifiers());
149         } catch (NoSuchMethodException e) {
150             return false;
151         }
152     }
153 
hasSetComposingRegion(@onNull final Class clazz)154     private static boolean hasSetComposingRegion(@NonNull final Class clazz) {
155         try {
156             final Method method = clazz.getMethod("setComposingRegion", int.class, int.class);
157             return !Modifier.isAbstract(method.getModifiers());
158         } catch (NoSuchMethodException e) {
159             return false;
160         }
161     }
162 
hasCommitCorrection(@onNull final Class clazz)163     private static boolean hasCommitCorrection(@NonNull final Class clazz) {
164         try {
165             final Method method = clazz.getMethod("commitCorrection", CorrectionInfo.class);
166             return !Modifier.isAbstract(method.getModifiers());
167         } catch (NoSuchMethodException e) {
168             return false;
169         }
170     }
171 
hasRequestCursorUpdate(@onNull final Class clazz)172     private static boolean hasRequestCursorUpdate(@NonNull final Class clazz) {
173         try {
174             final Method method = clazz.getMethod("requestCursorUpdates", int.class);
175             return !Modifier.isAbstract(method.getModifiers());
176         } catch (NoSuchMethodException e) {
177             return false;
178         }
179     }
180 
hasDeleteSurroundingTextInCodePoints(@onNull final Class clazz)181     private static boolean hasDeleteSurroundingTextInCodePoints(@NonNull final Class clazz) {
182         try {
183             final Method method = clazz.getMethod("deleteSurroundingTextInCodePoints", int.class,
184                     int.class);
185             return !Modifier.isAbstract(method.getModifiers());
186         } catch (NoSuchMethodException e) {
187             return false;
188         }
189     }
190 
hasGetHandler(@onNull final Class clazz)191     private static boolean hasGetHandler(@NonNull final Class clazz) {
192         try {
193             final Method method = clazz.getMethod("getHandler");
194             return !Modifier.isAbstract(method.getModifiers());
195         } catch (NoSuchMethodException e) {
196             return false;
197         }
198     }
199 
hasCloseConnection(@onNull final Class clazz)200     private static boolean hasCloseConnection(@NonNull final Class clazz) {
201         try {
202             final Method method = clazz.getMethod("closeConnection");
203             return !Modifier.isAbstract(method.getModifiers());
204         } catch (NoSuchMethodException e) {
205             return false;
206         }
207     }
208 
hasCommitContent(@onNull final Class clazz)209     private static boolean hasCommitContent(@NonNull final Class clazz) {
210         try {
211             final Method method = clazz.getMethod("commitContent", InputContentInfo.class,
212                     int.class, Bundle.class);
213             return !Modifier.isAbstract(method.getModifiers());
214         } catch (NoSuchMethodException e) {
215             return false;
216         }
217     }
218 
getMissingMethodFlagsAsString(@issingMethodFlags final int flags)219     public static String getMissingMethodFlagsAsString(@MissingMethodFlags final int flags) {
220         final StringBuilder sb = new StringBuilder();
221         boolean isEmpty = true;
222         if ((flags & MissingMethodFlags.GET_SELECTED_TEXT) != 0) {
223             sb.append("getSelectedText(int)");
224             isEmpty = false;
225         }
226         if ((flags & MissingMethodFlags.SET_COMPOSING_REGION) != 0) {
227             if (!isEmpty) {
228                 sb.append(",");
229             }
230             sb.append("setComposingRegion(int, int)");
231             isEmpty = false;
232         }
233         if ((flags & MissingMethodFlags.COMMIT_CORRECTION) != 0) {
234             if (!isEmpty) {
235                 sb.append(",");
236             }
237             sb.append("commitCorrection(CorrectionInfo)");
238             isEmpty = false;
239         }
240         if ((flags & MissingMethodFlags.REQUEST_CURSOR_UPDATES) != 0) {
241             if (!isEmpty) {
242                 sb.append(",");
243             }
244             sb.append("requestCursorUpdate(int)");
245             isEmpty = false;
246         }
247         if ((flags & MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS) != 0) {
248             if (!isEmpty) {
249                 sb.append(",");
250             }
251             sb.append("deleteSurroundingTextInCodePoints(int, int)");
252             isEmpty = false;
253         }
254         if ((flags & MissingMethodFlags.GET_HANDLER) != 0) {
255             if (!isEmpty) {
256                 sb.append(",");
257             }
258             sb.append("getHandler()");
259         }
260         if ((flags & MissingMethodFlags.CLOSE_CONNECTION) != 0) {
261             if (!isEmpty) {
262                 sb.append(",");
263             }
264             sb.append("closeConnection()");
265         }
266         if ((flags & MissingMethodFlags.COMMIT_CONTENT) != 0) {
267             if (!isEmpty) {
268                 sb.append(",");
269             }
270             sb.append("commitContent(InputContentInfo, Bundle)");
271         }
272         return sb.toString();
273     }
274 }
275