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