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