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