1 /*
2  * Copyright (C) 2018 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 android.annotation.AnyThread;
20 import android.annotation.DrawableRes;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.net.Uri;
24 import android.os.IBinder;
25 import android.os.RemoteException;
26 import android.util.Log;
27 import android.view.View;
28 import android.view.inputmethod.InputMethodSubtype;
29 
30 import com.android.internal.annotations.GuardedBy;
31 
32 import java.util.Objects;
33 
34 /**
35  * A utility class to take care of boilerplate code around IPCs.
36  */
37 public final class InputMethodPrivilegedOperations {
38     private static final String TAG = "InputMethodPrivilegedOperations";
39 
40     private static final class OpsHolder {
41         @Nullable
42         @GuardedBy("this")
43         private IInputMethodPrivilegedOperations mPrivOps;
44 
45         /**
46          * Sets {@link IInputMethodPrivilegedOperations}.
47          *
48          * <p>This method can be called only once.</p>
49          *
50          * @param privOps Binder interface to be set
51          */
52         @AnyThread
set(@onNull IInputMethodPrivilegedOperations privOps)53         public synchronized void set(@NonNull IInputMethodPrivilegedOperations privOps) {
54             if (mPrivOps != null) {
55                 throw new IllegalStateException(
56                         "IInputMethodPrivilegedOperations must be set at most once."
57                                 + " privOps=" + privOps);
58             }
59             mPrivOps = privOps;
60         }
61 
62         /**
63          * A simplified version of {@link android.os.Debug#getCaller()}.
64          *
65          * @return method name of the caller.
66          */
67         @AnyThread
getCallerMethodName()68         private static String getCallerMethodName() {
69             final StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
70             if (callStack.length <= 4) {
71                 return "<bottom of call stack>";
72             }
73             return callStack[4].getMethodName();
74         }
75 
76         @AnyThread
77         @Nullable
getAndWarnIfNull()78         public synchronized IInputMethodPrivilegedOperations getAndWarnIfNull() {
79             if (mPrivOps == null) {
80                 Log.e(TAG, getCallerMethodName() + " is ignored."
81                         + " Call it within attachToken() and InputMethodService.onDestroy()");
82             }
83             return mPrivOps;
84         }
85     }
86     private final OpsHolder mOps = new OpsHolder();
87 
88     /**
89      * Sets {@link IInputMethodPrivilegedOperations}.
90      *
91      * <p>This method can be called only once.</p>
92      *
93      * @param privOps Binder interface to be set
94      */
95     @AnyThread
set(@onNull IInputMethodPrivilegedOperations privOps)96     public void set(@NonNull IInputMethodPrivilegedOperations privOps) {
97         Objects.requireNonNull(privOps, "privOps must not be null");
98         mOps.set(privOps);
99     }
100 
101     /**
102      * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int}.
103      *
104      * @param vis visibility flags
105      * @param backDisposition disposition flags
106      * @see android.inputmethodservice.InputMethodService#IME_ACTIVE
107      * @see android.inputmethodservice.InputMethodService#IME_VISIBLE
108      * @see android.inputmethodservice.InputMethodService#IME_INVISIBLE
109      * @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_DEFAULT
110      * @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_ADJUST_NOTHING
111      */
112     @AnyThread
setImeWindowStatusAsync(int vis, int backDisposition)113     public void setImeWindowStatusAsync(int vis, int backDisposition) {
114         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
115         if (ops == null) {
116             return;
117         }
118         try {
119             ops.setImeWindowStatusAsync(vis, backDisposition);
120         } catch (RemoteException e) {
121             throw e.rethrowFromSystemServer();
122         }
123     }
124 
125     /**
126      * Calls {@link IInputMethodPrivilegedOperations#reportStartInputAsync(IBinder)}.
127      *
128      * @param startInputToken {@link IBinder} token to distinguish startInput session
129      */
130     @AnyThread
reportStartInputAsync(IBinder startInputToken)131     public void reportStartInputAsync(IBinder startInputToken) {
132         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
133         if (ops == null) {
134             return;
135         }
136         try {
137             ops.reportStartInputAsync(startInputToken);
138         } catch (RemoteException e) {
139             throw e.rethrowFromSystemServer();
140         }
141     }
142 
143     /**
144      * Calls {@link IInputMethodPrivilegedOperations#createInputContentUriToken(Uri, String,
145      * IIInputContentUriTokenResultCallback)}.
146      *
147      * @param contentUri Content URI to which a temporary read permission should be granted
148      * @param packageName Indicates what package needs to have a temporary read permission
149      * @return special Binder token that should be set to
150      *         {@link android.view.inputmethod.InputContentInfo#setUriToken(IInputContentUriToken)}
151      */
152     @AnyThread
createInputContentUriToken(Uri contentUri, String packageName)153     public IInputContentUriToken createInputContentUriToken(Uri contentUri, String packageName) {
154         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
155         if (ops == null) {
156             return null;
157         }
158         try {
159             final Completable.IInputContentUriToken value =
160                     Completable.createIInputContentUriToken();
161             ops.createInputContentUriToken(contentUri, packageName, ResultCallbacks.of(value));
162             return Completable.getResult(value);
163         } catch (RemoteException e) {
164             // For historical reasons, this error was silently ignored.
165             // Note that the caller already logs error so we do not need additional Log.e() here.
166             // TODO(team): Check if it is safe to rethrow error here.
167             return null;
168         }
169     }
170 
171     /**
172      * Calls {@link IInputMethodPrivilegedOperations#reportFullscreenModeAsync(boolean)}.
173      *
174      * @param fullscreen {@code true} if the IME enters full screen mode
175      */
176     @AnyThread
reportFullscreenModeAsync(boolean fullscreen)177     public void reportFullscreenModeAsync(boolean fullscreen) {
178         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
179         if (ops == null) {
180             return;
181         }
182         try {
183             ops.reportFullscreenModeAsync(fullscreen);
184         } catch (RemoteException e) {
185             throw e.rethrowFromSystemServer();
186         }
187     }
188 
189     /**
190      * Calls {@link IInputMethodPrivilegedOperations#updateStatusIconAsync(String, int)}.
191      *
192      * @param packageName package name from which the status icon should be loaded
193      * @param iconResId resource ID of the icon to be loaded
194      */
195     @AnyThread
updateStatusIconAsync(String packageName, @DrawableRes int iconResId)196     public void updateStatusIconAsync(String packageName, @DrawableRes int iconResId) {
197         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
198         if (ops == null) {
199             return;
200         }
201         try {
202             ops.updateStatusIconAsync(packageName, iconResId);
203         } catch (RemoteException e) {
204             throw e.rethrowFromSystemServer();
205         }
206     }
207 
208     /**
209      * Calls {@link IInputMethodPrivilegedOperations#setInputMethod(String, IVoidResultCallback)}.
210      *
211      * @param id IME ID of the IME to switch to
212      * @see android.view.inputmethod.InputMethodInfo#getId()
213      */
214     @AnyThread
setInputMethod(String id)215     public void setInputMethod(String id) {
216         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
217         if (ops == null) {
218             return;
219         }
220         try {
221             final Completable.Void value = Completable.createVoid();
222             ops.setInputMethod(id, ResultCallbacks.of(value));
223             Completable.getResult(value);
224         } catch (RemoteException e) {
225             throw e.rethrowFromSystemServer();
226         }
227     }
228 
229     /**
230      * Calls {@link IInputMethodPrivilegedOperations#setInputMethodAndSubtype(String,
231      * InputMethodSubtype, IVoidResultCallback)}
232      *
233      * @param id IME ID of the IME to switch to
234      * @param subtype {@link InputMethodSubtype} to switch to
235      * @see android.view.inputmethod.InputMethodInfo#getId()
236      */
237     @AnyThread
setInputMethodAndSubtype(String id, InputMethodSubtype subtype)238     public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype) {
239         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
240         if (ops == null) {
241             return;
242         }
243         try {
244             final Completable.Void value = Completable.createVoid();
245             ops.setInputMethodAndSubtype(id, subtype, ResultCallbacks.of(value));
246             Completable.getResult(value);
247         } catch (RemoteException e) {
248             throw e.rethrowFromSystemServer();
249         }
250     }
251 
252     /**
253      * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, IVoidResultCallback)}
254      *
255      * @param flags additional operating flags
256      * @see android.view.inputmethod.InputMethodManager#HIDE_IMPLICIT_ONLY
257      * @see android.view.inputmethod.InputMethodManager#HIDE_NOT_ALWAYS
258      */
259     @AnyThread
hideMySoftInput(int flags)260     public void hideMySoftInput(int flags) {
261         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
262         if (ops == null) {
263             return;
264         }
265         try {
266             final Completable.Void value = Completable.createVoid();
267             ops.hideMySoftInput(flags, ResultCallbacks.of(value));
268             Completable.getResult(value);
269         } catch (RemoteException e) {
270             throw e.rethrowFromSystemServer();
271         }
272     }
273 
274     /**
275      * Calls {@link IInputMethodPrivilegedOperations#showMySoftInput(int, IVoidResultCallback)}
276      *
277      * @param flags additional operating flags
278      * @see android.view.inputmethod.InputMethodManager#SHOW_IMPLICIT
279      * @see android.view.inputmethod.InputMethodManager#SHOW_FORCED
280      */
281     @AnyThread
showMySoftInput(int flags)282     public void showMySoftInput(int flags) {
283         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
284         if (ops == null) {
285             return;
286         }
287         try {
288             final Completable.Void value = Completable.createVoid();
289             ops.showMySoftInput(flags, ResultCallbacks.of(value));
290             Completable.getResult(value);
291         } catch (RemoteException e) {
292             throw e.rethrowFromSystemServer();
293         }
294     }
295 
296     /**
297      * Calls {@link IInputMethodPrivilegedOperations#switchToPreviousInputMethod(
298      * IBooleanResultCallback)}
299      *
300      * @return {@code true} if handled
301      */
302     @AnyThread
switchToPreviousInputMethod()303     public boolean switchToPreviousInputMethod() {
304         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
305         if (ops == null) {
306             return false;
307         }
308         try {
309             final Completable.Boolean value = Completable.createBoolean();
310             ops.switchToPreviousInputMethod(ResultCallbacks.of(value));
311             return Completable.getResult(value);
312         } catch (RemoteException e) {
313             throw e.rethrowFromSystemServer();
314         }
315     }
316 
317     /**
318      * Calls {@link IInputMethodPrivilegedOperations#switchToNextInputMethod(boolean,
319      * IBooleanResultCallback)}
320      *
321      * @param onlyCurrentIme {@code true} to switch to a {@link InputMethodSubtype} within the same
322      *                       IME
323      * @return {@code true} if handled
324      */
325     @AnyThread
switchToNextInputMethod(boolean onlyCurrentIme)326     public boolean switchToNextInputMethod(boolean onlyCurrentIme) {
327         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
328         if (ops == null) {
329             return false;
330         }
331         try {
332             final Completable.Boolean value = Completable.createBoolean();
333             ops.switchToNextInputMethod(onlyCurrentIme, ResultCallbacks.of(value));
334             return Completable.getResult(value);
335         } catch (RemoteException e) {
336             throw e.rethrowFromSystemServer();
337         }
338     }
339 
340     /**
341      * Calls {@link IInputMethodPrivilegedOperations#shouldOfferSwitchingToNextInputMethod(
342      * IBooleanResultCallback)}
343      *
344      * @return {@code true} if the IEM should offer a way to globally switch IME
345      */
346     @AnyThread
shouldOfferSwitchingToNextInputMethod()347     public boolean shouldOfferSwitchingToNextInputMethod() {
348         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
349         if (ops == null) {
350             return false;
351         }
352         try {
353             final Completable.Boolean value = Completable.createBoolean();
354             ops.shouldOfferSwitchingToNextInputMethod(ResultCallbacks.of(value));
355             return Completable.getResult(value);
356         } catch (RemoteException e) {
357             throw e.rethrowFromSystemServer();
358         }
359     }
360 
361     /**
362      * Calls {@link IInputMethodPrivilegedOperations#notifyUserActionAsync()}
363      */
364     @AnyThread
notifyUserActionAsync()365     public void notifyUserActionAsync() {
366         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
367         if (ops == null) {
368             return;
369         }
370         try {
371             ops.notifyUserActionAsync();
372         } catch (RemoteException e) {
373             throw e.rethrowFromSystemServer();
374         }
375     }
376 
377     /**
378      * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean)}.
379      *
380      * @param showOrHideInputToken placeholder token that maps to window requesting
381      *        {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or
382      *        {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow
383      *        (IBinder, int)}
384      * @param setVisible {@code true} to set IME visible, else hidden.
385      */
386     @AnyThread
applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible)387     public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible) {
388         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
389         if (ops == null) {
390             return;
391         }
392         try {
393             ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible);
394         } catch (RemoteException e) {
395             throw e.rethrowFromSystemServer();
396         }
397     }
398 }
399