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