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