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
17package android.view.inputmethod;
18
19import static java.lang.annotation.RetentionPolicy.SOURCE;
20
21import android.annotation.IntDef;
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.os.Bundle;
25
26import java.lang.annotation.Retention;
27import java.lang.reflect.Method;
28import java.lang.reflect.Modifier;
29import java.util.Collections;
30import java.util.Map;
31import java.util.WeakHashMap;
32
33/**
34 * @hide
35 */
36public 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
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
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
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
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
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
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
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
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
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
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
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