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#createInputContentUriToken(Uri, String,
148      * AndroidFuture)}.
149      *
150      * @param contentUri Content URI to which a temporary read permission should be granted
151      * @param packageName Indicates what package needs to have a temporary read permission
152      * @return special Binder token that should be set to
153      *         {@link android.view.inputmethod.InputContentInfo#setUriToken(IInputContentUriToken)}
154      */
155     @AnyThread
createInputContentUriToken(Uri contentUri, String packageName)156     public IInputContentUriToken createInputContentUriToken(Uri contentUri, String packageName) {
157         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
158         if (ops == null) {
159             return null;
160         }
161         try {
162             final AndroidFuture<IBinder> future = new AndroidFuture<>();
163             ops.createInputContentUriToken(contentUri, packageName, future);
164             return IInputContentUriToken.Stub.asInterface(CompletableFutureUtil.getResult(future));
165         } catch (RemoteException e) {
166             // For historical reasons, this error was silently ignored.
167             // Note that the caller already logs error so we do not need additional Log.e() here.
168             // TODO(team): Check if it is safe to rethrow error here.
169             return null;
170         }
171     }
172 
173     /**
174      * Calls {@link IInputMethodPrivilegedOperations#reportFullscreenModeAsync(boolean)}.
175      *
176      * @param fullscreen {@code true} if the IME enters full screen mode
177      */
178     @AnyThread
reportFullscreenModeAsync(boolean fullscreen)179     public void reportFullscreenModeAsync(boolean fullscreen) {
180         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
181         if (ops == null) {
182             return;
183         }
184         try {
185             ops.reportFullscreenModeAsync(fullscreen);
186         } catch (RemoteException e) {
187             throw e.rethrowFromSystemServer();
188         }
189     }
190 
191     /**
192      * Calls {@link IInputMethodPrivilegedOperations#updateStatusIconAsync(String, int)}.
193      *
194      * @param packageName package name from which the status icon should be loaded
195      * @param iconResId resource ID of the icon to be loaded
196      */
197     @AnyThread
updateStatusIconAsync(String packageName, @DrawableRes int iconResId)198     public void updateStatusIconAsync(String packageName, @DrawableRes int iconResId) {
199         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
200         if (ops == null) {
201             return;
202         }
203         try {
204             ops.updateStatusIconAsync(packageName, iconResId);
205         } catch (RemoteException e) {
206             throw e.rethrowFromSystemServer();
207         }
208     }
209 
210     /**
211      * Calls {@link IInputMethodPrivilegedOperations#setInputMethod(String, AndroidFuture)}.
212      *
213      * @param id IME ID of the IME to switch to
214      * @see android.view.inputmethod.InputMethodInfo#getId()
215      */
216     @AnyThread
setInputMethod(String id)217     public void setInputMethod(String id) {
218         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
219         if (ops == null) {
220             return;
221         }
222         try {
223             final AndroidFuture<Void> future = new AndroidFuture<>();
224             ops.setInputMethod(id, future);
225             CompletableFutureUtil.getResult(future);
226         } catch (RemoteException e) {
227             throw e.rethrowFromSystemServer();
228         }
229     }
230 
231     /**
232      * Calls {@link IInputMethodPrivilegedOperations#setInputMethodAndSubtype(String,
233      * InputMethodSubtype, AndroidFuture)}
234      *
235      * @param id IME ID of the IME to switch to
236      * @param subtype {@link InputMethodSubtype} to switch to
237      * @see android.view.inputmethod.InputMethodInfo#getId()
238      */
239     @AnyThread
setInputMethodAndSubtype(String id, InputMethodSubtype subtype)240     public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype) {
241         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
242         if (ops == null) {
243             return;
244         }
245         try {
246             final AndroidFuture<Void> future = new AndroidFuture<>();
247             ops.setInputMethodAndSubtype(id, subtype, future);
248             CompletableFutureUtil.getResult(future);
249         } catch (RemoteException e) {
250             throw e.rethrowFromSystemServer();
251         }
252     }
253 
254     /**
255      * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, int, AndroidFuture)}
256      *
257      * @param reason the reason to hide soft input
258      */
259     @AnyThread
hideMySoftInput(@nputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason)260     public void hideMySoftInput(@InputMethodManager.HideFlags int flags,
261             @SoftInputShowHideReason int reason) {
262         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
263         if (ops == null) {
264             return;
265         }
266         try {
267             final AndroidFuture<Void> future = new AndroidFuture<>();
268             ops.hideMySoftInput(flags, reason, future);
269             CompletableFutureUtil.getResult(future);
270         } catch (RemoteException e) {
271             throw e.rethrowFromSystemServer();
272         }
273     }
274 
275     /**
276      * Calls {@link IInputMethodPrivilegedOperations#showMySoftInput(int, AndroidFuture)}
277      */
278     @AnyThread
showMySoftInput(@nputMethodManager.ShowFlags int flags)279     public void showMySoftInput(@InputMethodManager.ShowFlags int flags) {
280         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
281         if (ops == null) {
282             return;
283         }
284         try {
285             final AndroidFuture<Void> future = new AndroidFuture<>();
286             ops.showMySoftInput(flags, future);
287             CompletableFutureUtil.getResult(future);
288         } catch (RemoteException e) {
289             throw e.rethrowFromSystemServer();
290         }
291     }
292 
293     /**
294      * Calls {@link IInputMethodPrivilegedOperations#switchToPreviousInputMethod(AndroidFuture)}
295      *
296      * @return {@code true} if handled
297      */
298     @AnyThread
switchToPreviousInputMethod()299     public boolean switchToPreviousInputMethod() {
300         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
301         if (ops == null) {
302             return false;
303         }
304         try {
305             final AndroidFuture<Boolean> value = new AndroidFuture<>();
306             ops.switchToPreviousInputMethod(value);
307             return CompletableFutureUtil.getResult(value);
308         } catch (RemoteException e) {
309             throw e.rethrowFromSystemServer();
310         }
311     }
312 
313     /**
314      * Calls {@link IInputMethodPrivilegedOperations#switchToNextInputMethod(boolean,
315      * AndroidFuture)}
316      *
317      * @param onlyCurrentIme {@code true} to switch to a {@link InputMethodSubtype} within the same
318      *                       IME
319      * @return {@code true} if handled
320      */
321     @AnyThread
switchToNextInputMethod(boolean onlyCurrentIme)322     public boolean switchToNextInputMethod(boolean onlyCurrentIme) {
323         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
324         if (ops == null) {
325             return false;
326         }
327         try {
328             final AndroidFuture<Boolean> future = new AndroidFuture<>();
329             ops.switchToNextInputMethod(onlyCurrentIme, future);
330             return CompletableFutureUtil.getResult(future);
331         } catch (RemoteException e) {
332             throw e.rethrowFromSystemServer();
333         }
334     }
335 
336     /**
337      * Calls {@link IInputMethodPrivilegedOperations#shouldOfferSwitchingToNextInputMethod(
338      * AndroidFuture)}
339      *
340      * @return {@code true} if the IEM should offer a way to globally switch IME
341      */
342     @AnyThread
shouldOfferSwitchingToNextInputMethod()343     public boolean shouldOfferSwitchingToNextInputMethod() {
344         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
345         if (ops == null) {
346             return false;
347         }
348         try {
349             final AndroidFuture<Boolean> future = new AndroidFuture<>();
350             ops.shouldOfferSwitchingToNextInputMethod(future);
351             return CompletableFutureUtil.getResult(future);
352         } catch (RemoteException e) {
353             throw e.rethrowFromSystemServer();
354         }
355     }
356 
357     /**
358      * Calls {@link IInputMethodPrivilegedOperations#notifyUserActionAsync()}
359      */
360     @AnyThread
notifyUserActionAsync()361     public void notifyUserActionAsync() {
362         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
363         if (ops == null) {
364             return;
365         }
366         try {
367             ops.notifyUserActionAsync();
368         } catch (RemoteException e) {
369             throw e.rethrowFromSystemServer();
370         }
371     }
372 
373     /**
374      * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean,
375      * ImeTracker.Token)}.
376      *
377      * @param showOrHideInputToken placeholder token that maps to window requesting
378      *        {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or
379      *        {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder,
380      *        int)}
381      * @param setVisible {@code true} to set IME visible, else hidden.
382      * @param statsToken the token tracking the current IME request or {@code null} otherwise.
383      */
384     @AnyThread
applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible, @Nullable ImeTracker.Token statsToken)385     public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
386             @Nullable ImeTracker.Token statsToken) {
387         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
388         if (ops == null) {
389             ImeTracker.forLogging().onFailed(statsToken,
390                     ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
391             return;
392         }
393         ImeTracker.forLogging().onProgress(statsToken,
394                 ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
395         try {
396             ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken);
397         } catch (RemoteException e) {
398             throw e.rethrowFromSystemServer();
399         }
400     }
401 
402     /**
403      * Calls {@link IInputMethodPrivilegedOperations#onStylusHandwritingReady(int, int)}
404      */
405     @AnyThread
onStylusHandwritingReady(int requestId, int pid)406     public void onStylusHandwritingReady(int requestId, int pid) {
407         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
408         if (ops == null) {
409             return;
410         }
411         try {
412             ops.onStylusHandwritingReady(requestId, pid);
413         } catch (RemoteException e) {
414             throw e.rethrowFromSystemServer();
415         }
416     }
417 
418     /**
419      * IME notifies that the current handwriting session should be closed.
420      * @param requestId
421      */
422     @AnyThread
resetStylusHandwriting(int requestId)423     public void resetStylusHandwriting(int requestId) {
424         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
425         if (ops == null) {
426             return;
427         }
428         try {
429             ops.resetStylusHandwriting(requestId);
430         } catch (RemoteException e) {
431             throw e.rethrowFromSystemServer();
432         }
433     }
434 }
435