1 /*
2  * Copyright (C) 2017 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.cts.mockime;
18 
19 import static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.os.Bundle;
22 import android.os.PersistableBundle;
23 import android.os.Process;
24 import android.os.RemoteCallback;
25 import android.os.UserHandle;
26 import android.view.inputmethod.InputMethodSubtype;
27 
28 import androidx.annotation.ColorInt;
29 import androidx.annotation.IntDef;
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 import androidx.window.extensions.layout.WindowLayoutInfo;
33 
34 import java.lang.annotation.Retention;
35 import java.util.Objects;
36 
37 /**
38  * An immutable data store to control the behavior of {@link MockIme}.
39  */
40 public class ImeSettings {
41 
42     @NonNull
43     private final String mClientPackageName;
44 
45     @NonNull
46     private final String mEventCallbackActionName;
47 
48     private static final String EVENT_CALLBACK_INTENT_ACTION_KEY = "eventCallbackActionName";
49     private static final String CHANNEL_KEY = "channel";
50     private static final String DATA_KEY = "data";
51 
52     private static final String BACKGROUND_COLOR_KEY = "BackgroundColor";
53     private static final String NAVIGATION_BAR_COLOR_KEY = "NavigationBarColor";
54     private static final String INPUT_VIEW_HEIGHT =
55             "InputViewHeightWithoutSystemWindowInset";
56     private static final String DRAWS_BEHIND_NAV_BAR = "drawsBehindNavBar";
57     private static final String WINDOW_FLAGS = "WindowFlags";
58     private static final String WINDOW_FLAGS_MASK = "WindowFlagsMask";
59     private static final String FULLSCREEN_MODE_POLICY = "FullscreenModePolicy";
60     private static final String INPUT_VIEW_SYSTEM_UI_VISIBILITY = "InputViewSystemUiVisibility";
61     private static final String WATERMARK_ENABLED = "WatermarkEnabled";
62     private static final String WATERMARK_GRAVITY = "WatermarkGravity";
63     private static final String HARD_KEYBOARD_CONFIGURATION_BEHAVIOR_ALLOWED =
64             "HardKeyboardConfigurationBehaviorAllowed";
65     private static final String INLINE_SUGGESTIONS_ENABLED = "InlineSuggestionsEnabled";
66     private static final String INLINE_SUGGESTION_VIEW_CONTENT_DESC =
67             "InlineSuggestionViewContentDesc";
68     private static final String STRICT_MODE_ENABLED = "StrictModeEnabled";
69     private static final String VERIFY_CONTEXT_APIS_IN_ON_CREATE = "VerifyContextApisInOnCreate";
70     private static final String WINDOW_LAYOUT_INFO_CALLBACK_ENABLED =
71             "WindowLayoutInfoCallbackEnabled";
72     private static final String CONNECTIONLESS_HANDWRITING_ENABLED =
73             "ConnectionlessHandwritingEnabled";
74 
75     /**
76      * Simulate the manifest flag enableOnBackInvokedCallback being true for the IME.
77      */
78     private static final String ON_BACK_CALLBACK_ENABLED = "onBackCallbackEnabled";
79 
80     private static final String USE_CUSTOM_EXTRACT_TEXT_VIEW = "useCustomExtractTextView";
81 
82     private static final String ZERO_INSETS = "zeroInsets";
83 
84     @NonNull
85     private final PersistableBundle mBundle;
86     private final SessionChannel mChannel;
87 
88     @Retention(SOURCE)
89     @IntDef(value = {
90             FullscreenModePolicy.NO_FULLSCREEN,
91             FullscreenModePolicy.FORCE_FULLSCREEN,
92             FullscreenModePolicy.OS_DEFAULT,
93     })
94     public @interface FullscreenModePolicy {
95         /**
96          * Let {@link MockIme} always return {@code false} from
97          * {@link android.inputmethodservice.InputMethodService#onEvaluateFullscreenMode()}.
98          *
99          * <p>This is chosen to be the default behavior of {@link MockIme} to make CTS tests most
100          * deterministic.</p>
101          */
102         int NO_FULLSCREEN = 0;
103 
104         /**
105          * Let {@link MockIme} always return {@code true} from
106          * {@link android.inputmethodservice.InputMethodService#onEvaluateFullscreenMode()}.
107          *
108          * <p>This can be used to test the behaviors when a full-screen IME is running.</p>
109          */
110         int FORCE_FULLSCREEN = 1;
111 
112         /**
113          * Let {@link MockIme} always return the default behavior of
114          * {@link android.inputmethodservice.InputMethodService#onEvaluateFullscreenMode()}.
115          *
116          * <p>This can be used to test the default behavior of that public API.</p>
117          */
118         int OS_DEFAULT = 2;
119     }
120 
ImeSettings(@onNull String clientPackageName, @NonNull Bundle bundle)121     ImeSettings(@NonNull String clientPackageName, @NonNull Bundle bundle) {
122         mClientPackageName = clientPackageName;
123         mEventCallbackActionName = bundle.getString(EVENT_CALLBACK_INTENT_ACTION_KEY);
124         mBundle = bundle.getParcelable(DATA_KEY);
125         mChannel = new SessionChannel(bundle.getParcelable(CHANNEL_KEY, RemoteCallback.class));
126     }
127 
128     @Nullable
getEventCallbackActionName()129     String getEventCallbackActionName() {
130         return mEventCallbackActionName;
131     }
132 
getChannel()133     SessionChannel getChannel() {
134         return mChannel;
135     }
136 
137     @NonNull
getClientPackageName()138     String getClientPackageName() {
139         return mClientPackageName;
140     }
141 
142     @FullscreenModePolicy
fullscreenModePolicy()143     public int fullscreenModePolicy() {
144         return mBundle.getInt(FULLSCREEN_MODE_POLICY);
145     }
146 
147     @ColorInt
getBackgroundColor(@olorInt int defaultColor)148     public int getBackgroundColor(@ColorInt int defaultColor) {
149         return mBundle.getInt(BACKGROUND_COLOR_KEY, defaultColor);
150     }
151 
hasNavigationBarColor()152     public boolean hasNavigationBarColor() {
153         return mBundle.keySet().contains(NAVIGATION_BAR_COLOR_KEY);
154     }
155 
156     @ColorInt
getNavigationBarColor()157     public int getNavigationBarColor() {
158         return mBundle.getInt(NAVIGATION_BAR_COLOR_KEY);
159     }
160 
getInputViewHeight(int defaultHeight)161     public int getInputViewHeight(int defaultHeight) {
162         return mBundle.getInt(INPUT_VIEW_HEIGHT, defaultHeight);
163     }
164 
getDrawsBehindNavBar()165     public boolean getDrawsBehindNavBar() {
166         return mBundle.getBoolean(DRAWS_BEHIND_NAV_BAR, false);
167     }
168 
getWindowFlags(int defaultFlags)169     public int getWindowFlags(int defaultFlags) {
170         return mBundle.getInt(WINDOW_FLAGS, defaultFlags);
171     }
172 
getWindowFlagsMask(int defaultFlags)173     public int getWindowFlagsMask(int defaultFlags) {
174         return mBundle.getInt(WINDOW_FLAGS_MASK, defaultFlags);
175     }
176 
getInputViewSystemUiVisibility(int defaultFlags)177     public int getInputViewSystemUiVisibility(int defaultFlags) {
178         return mBundle.getInt(INPUT_VIEW_SYSTEM_UI_VISIBILITY, defaultFlags);
179     }
180 
isWatermarkEnabled(boolean defaultValue)181     public boolean isWatermarkEnabled(boolean defaultValue) {
182         return mBundle.getBoolean(WATERMARK_ENABLED, defaultValue);
183     }
184 
getWatermarkGravity(int defaultValue)185     public int getWatermarkGravity(int defaultValue) {
186         return mBundle.getInt(WATERMARK_GRAVITY, defaultValue);
187     }
188 
getHardKeyboardConfigurationBehaviorAllowed(boolean defaultValue)189     public boolean getHardKeyboardConfigurationBehaviorAllowed(boolean defaultValue) {
190         return mBundle.getBoolean(HARD_KEYBOARD_CONFIGURATION_BEHAVIOR_ALLOWED, defaultValue);
191     }
192 
getInlineSuggestionsEnabled()193     public boolean getInlineSuggestionsEnabled() {
194         return mBundle.getBoolean(INLINE_SUGGESTIONS_ENABLED);
195     }
196 
197     @Nullable
getInlineSuggestionViewContentDesc(@ullable String defaultValue)198     public String getInlineSuggestionViewContentDesc(@Nullable String defaultValue) {
199         return mBundle.getString(INLINE_SUGGESTION_VIEW_CONTENT_DESC, defaultValue);
200     }
201 
isStrictModeEnabled()202     public boolean isStrictModeEnabled() {
203         return mBundle.getBoolean(STRICT_MODE_ENABLED, false);
204     }
205 
isVerifyContextApisInOnCreate()206     public boolean isVerifyContextApisInOnCreate() {
207         return mBundle.getBoolean(VERIFY_CONTEXT_APIS_IN_ON_CREATE, false);
208     }
209 
isWindowLayoutInfoCallbackEnabled()210     public boolean isWindowLayoutInfoCallbackEnabled() {
211         return mBundle.getBoolean(WINDOW_LAYOUT_INFO_CALLBACK_ENABLED, false);
212     }
213 
isConnectionlessHandwritingEnabled()214     public boolean isConnectionlessHandwritingEnabled() {
215         return mBundle.getBoolean(CONNECTIONLESS_HANDWRITING_ENABLED, false);
216     }
217 
isOnBackCallbackEnabled()218     public boolean isOnBackCallbackEnabled() {
219         return mBundle.getBoolean(ON_BACK_CALLBACK_ENABLED, false);
220     }
221 
close()222     public void close() {
223         if (mChannel != null) {
224             mChannel.close();
225         }
226     }
227 
228     /** Whether or not custom extract view hierarchy should be used. */
isCustomExtractTextViewEnabled()229     public boolean isCustomExtractTextViewEnabled() {
230         return mBundle.getBoolean(USE_CUSTOM_EXTRACT_TEXT_VIEW, false);
231     }
232 
233     /** Whether the IME should provide zero insets when shown. */
isZeroInsetsEnabled()234     public boolean isZeroInsetsEnabled() {
235         return mBundle.getBoolean(ZERO_INSETS, false);
236     }
237 
serializeToBundle(@onNull String eventCallbackActionName, @Nullable Builder builder, @NonNull RemoteCallback channel)238     static Bundle serializeToBundle(@NonNull String eventCallbackActionName,
239             @Nullable Builder builder, @NonNull RemoteCallback channel) {
240         final Bundle result = new Bundle();
241         result.putString(EVENT_CALLBACK_INTENT_ACTION_KEY, eventCallbackActionName);
242         result.putParcelable(DATA_KEY, builder != null ? builder.mBundle : PersistableBundle.EMPTY);
243         result.putParcelable(CHANNEL_KEY, channel);
244         return result;
245     }
246 
247     /**
248      * The builder class for {@link ImeSettings}.
249      */
250     public static final class Builder {
251         private final PersistableBundle mBundle = new PersistableBundle();
252 
253         @MockImePackageNames
254         @NonNull
255         String mMockImePackageName = MockImePackageNames.MockIme1;
256 
257         /**
258          * Specifies a non-default {@link MockIme} package name, which is by default
259          * {@code com.android.cts.mockime}.
260          *
261          * <p>You can use this to interact with multiple {@link MockIme} sessions at the same time.
262          * </p>
263          *
264          * @param packageName One of {@link MockImePackageNames}.
265          * @return this {@link Builder} object
266          */
setMockImePackageName(@ockImePackageNames String packageName)267         public Builder setMockImePackageName(@MockImePackageNames String packageName) {
268             mMockImePackageName = packageName;
269             return this;
270         }
271 
272         @NonNull
273         UserHandle mTargetUser = Process.myUserHandle();
274 
275         /**
276          * Specifies a different user than the current user.
277          *
278          * @param targetUser The user whose {@link MockIme} will be connected to.
279          * @return this {@link Builder} object
280          */
setTargetUser(@onNull UserHandle targetUser)281         public Builder setTargetUser(@NonNull UserHandle targetUser) {
282             mTargetUser = Objects.requireNonNull(targetUser);
283             return this;
284         }
285 
286         boolean mSuppressResetIme = false;
287         /**
288          * Specifies whether {@code adb shell ime reset} should be suppressed or not on
289          * {@link MockImeSession#create(android.content.Context)} and
290          * {@link MockImeSession#close()}.
291          *
292          * <p>The default value is {@code false}.</p>
293          *
294          * @param suppressResetIme {@code true} to suppress {@code adb shell ime reset} upon
295          *                         initialize and cleanup processes of {@link MockImeSession}.
296          * @return this {@link Builder} object
297          */
setSuppressResetIme(boolean suppressResetIme)298         public Builder setSuppressResetIme(boolean suppressResetIme) {
299             mSuppressResetIme = suppressResetIme;
300             return this;
301         }
302 
303         @Nullable
304         InputMethodSubtype[] mAdditionalSubtypes;
305 
306         /**
307          * Specifies additional {@link InputMethodSubtype}s to be set before launching
308          * {@link MockIme} by using
309          * {@link android.view.inputmethod.InputMethodManager#setAdditionalInputMethodSubtypes(
310          * String, InputMethodSubtype[])}.
311          *
312          * @param subtypes An array of {@link InputMethodSubtype}.
313          * @return this {@link Builder} object
314          */
setAdditionalSubtypes(InputMethodSubtype... subtypes)315         public Builder setAdditionalSubtypes(InputMethodSubtype... subtypes) {
316             mAdditionalSubtypes = subtypes;
317             return this;
318         }
319 
320         /**
321          * Controls how MockIme reacts to
322          * {@link android.inputmethodservice.InputMethodService#onEvaluateFullscreenMode()}.
323          *
324          * @param policy one of {@link FullscreenModePolicy}
325          * @see MockIme#onEvaluateFullscreenMode()
326          */
setFullscreenModePolicy(@ullscreenModePolicy int policy)327         public Builder setFullscreenModePolicy(@FullscreenModePolicy int policy) {
328             mBundle.putInt(FULLSCREEN_MODE_POLICY, policy);
329             return this;
330         }
331 
332         /**
333          * Sets the background color of the {@link MockIme}.
334          * @param color background color to be used
335          */
setBackgroundColor(@olorInt int color)336         public Builder setBackgroundColor(@ColorInt int color) {
337             mBundle.putInt(BACKGROUND_COLOR_KEY, color);
338             return this;
339         }
340 
341         /**
342          * Sets the color to be passed to {@link android.view.Window#setNavigationBarColor(int)}.
343          *
344          * @param color color to be passed to {@link android.view.Window#setNavigationBarColor(int)}
345          * @see android.view.View
346          */
setNavigationBarColor(@olorInt int color)347         public Builder setNavigationBarColor(@ColorInt int color) {
348             mBundle.putInt(NAVIGATION_BAR_COLOR_KEY, color);
349             return this;
350         }
351 
352         /**
353          * Sets the input view height measured from the bottom of the screen.
354          *
355          * @param height height of the soft input view. This includes the system window inset such
356          *               as navigation bar.
357          */
setInputViewHeight(int height)358         public Builder setInputViewHeight(int height) {
359             mBundle.putInt(INPUT_VIEW_HEIGHT, height);
360             return this;
361         }
362 
363         /**
364          * Sets whether IME draws behind navigation bar.
365          */
setDrawsBehindNavBar(boolean drawsBehindNavBar)366         public Builder setDrawsBehindNavBar(boolean drawsBehindNavBar) {
367             mBundle.putBoolean(DRAWS_BEHIND_NAV_BAR, drawsBehindNavBar);
368             return this;
369         }
370 
371         /**
372          * Sets window flags to be specified to {@link android.view.Window#setFlags(int, int)} of
373          * the main {@link MockIme} window.
374          *
375          * <p>When {@link android.view.WindowManager.LayoutParams#FLAG_LAYOUT_IN_OVERSCAN} is set,
376          * {@link MockIme} tries to render the navigation bar by itself.</p>
377          *
378          * @param flags flags to be specified
379          * @param flagsMask mask bits that specify what bits need to be cleared before setting
380          *                  {@code flags}
381          * @see android.view.WindowManager
382          */
setWindowFlags(int flags, int flagsMask)383         public Builder setWindowFlags(int flags, int flagsMask) {
384             mBundle.putInt(WINDOW_FLAGS, flags);
385             mBundle.putInt(WINDOW_FLAGS_MASK, flagsMask);
386             return this;
387         }
388 
389         /**
390          * Sets flags to be specified to {@link android.view.View#setSystemUiVisibility(int)} of
391          * the main soft input view (the returned view from {@link MockIme#onCreateInputView()}).
392          *
393          * @param visibilityFlags flags to be specified
394          * @see android.view.View
395          */
setInputViewSystemUiVisibility(int visibilityFlags)396         public Builder setInputViewSystemUiVisibility(int visibilityFlags) {
397             mBundle.putInt(INPUT_VIEW_SYSTEM_UI_VISIBILITY, visibilityFlags);
398             return this;
399         }
400 
401         /**
402          * Sets whether a unique watermark image needs to be shown on the software keyboard or not.
403          *
404          * <p>This needs to be enabled to use</p>
405          *
406          * @param enabled {@code true} when such a watermark image is requested.
407          */
setWatermarkEnabled(boolean enabled)408         public Builder setWatermarkEnabled(boolean enabled) {
409             mBundle.putBoolean(WATERMARK_ENABLED, enabled);
410             return this;
411         }
412 
413         /**
414          * Sets the {@link android.view.Gravity} flags for the watermark image.
415          *
416          * <p>{@link android.view.Gravity#CENTER} will be used if not set.</p>
417          *
418          * @param gravity {@code true} {@link android.view.Gravity} flags to be set.
419          */
setWatermarkGravity(int gravity)420         public Builder setWatermarkGravity(int gravity) {
421             mBundle.putInt(WATERMARK_GRAVITY, gravity);
422             return this;
423         }
424 
425         /**
426          * Controls whether {@link MockIme} is allowed to change the behavior based on
427          * {@link android.content.res.Configuration#keyboard} and
428          * {@link android.content.res.Configuration#hardKeyboardHidden}.
429          *
430          * <p>Methods in {@link android.inputmethodservice.InputMethodService} such as
431          * {@link android.inputmethodservice.InputMethodService#onEvaluateInputViewShown()} and
432          * {@link android.inputmethodservice.InputMethodService#onShowInputRequested(int, boolean)}
433          * change their behaviors when a hardware keyboard is attached.  This is confusing when
434          * writing tests so by default {@link MockIme} tries to cancel those behaviors.  This
435          * settings re-enables such a behavior.</p>
436          *
437          * @param allowed {@code true} when {@link MockIme} is allowed to change the behavior when
438          *                a hardware keyboard is attached
439          *
440          * @see android.inputmethodservice.InputMethodService#onEvaluateInputViewShown()
441          * @see android.inputmethodservice.InputMethodService#onShowInputRequested(int, boolean)
442          */
setHardKeyboardConfigurationBehaviorAllowed(boolean allowed)443         public Builder setHardKeyboardConfigurationBehaviorAllowed(boolean allowed) {
444             mBundle.putBoolean(HARD_KEYBOARD_CONFIGURATION_BEHAVIOR_ALLOWED, allowed);
445             return this;
446         }
447 
448         /**
449          * Controls whether inline suggestions are enabled for {@link MockIme}. If enabled, a
450          * suggestion strip will be rendered at the top of the keyboard.
451          *
452          * @param enabled {@code true} when {@link MockIme} is enabled to show inline suggestions.
453          */
setInlineSuggestionsEnabled(boolean enabled)454         public Builder setInlineSuggestionsEnabled(boolean enabled) {
455             mBundle.putBoolean(INLINE_SUGGESTIONS_ENABLED, enabled);
456             return this;
457         }
458 
459         /**
460          * Controls whether inline suggestions are enabled for {@link MockIme}. If enabled, a
461          * suggestion strip will be rendered at the top of the keyboard.
462          *
463          * @param contentDesc content description to be set to the inline suggestion View.
464          */
setInlineSuggestionViewContentDesc(@onNull String contentDesc)465         public Builder setInlineSuggestionViewContentDesc(@NonNull String contentDesc) {
466             mBundle.putString(INLINE_SUGGESTION_VIEW_CONTENT_DESC, contentDesc);
467             return this;
468         }
469 
470         /** Sets whether to enable {@link android.os.StrictMode} or not. */
setStrictModeEnabled(boolean enabled)471         public Builder setStrictModeEnabled(boolean enabled) {
472             mBundle.putBoolean(STRICT_MODE_ENABLED, enabled);
473             return this;
474         }
475 
476         /**
477          * Sets whether to verify below {@link android.content.Context} APIs or not:
478          * <ul>
479          *     <li>{@link android.inputmethodservice.InputMethodService#getDisplay}</li>
480          *     <li>{@link android.inputmethodservice.InputMethodService#isUiContext}</li>
481          * </ul>
482          */
setVerifyUiContextApisInOnCreate(boolean enabled)483         public Builder setVerifyUiContextApisInOnCreate(boolean enabled) {
484             mBundle.putBoolean(VERIFY_CONTEXT_APIS_IN_ON_CREATE, enabled);
485             return this;
486         }
487 
488         /**
489          * Sets whether to enable {@link WindowLayoutInfo} callbacks for {@link MockIme}.
490          */
setWindowLayoutInfoCallbackEnabled(boolean enabled)491         public Builder setWindowLayoutInfoCallbackEnabled(boolean enabled) {
492             mBundle.putBoolean(WINDOW_LAYOUT_INFO_CALLBACK_ENABLED, enabled);
493             return this;
494         }
495 
496         /**
497          * Sets whether to enable {@link
498          * android.inputmethodservice.InputMethodService#onStartConnectionlessStylusHandwriting}.
499          */
setConnectionlessHandwritingEnabled(boolean enabled)500         public Builder setConnectionlessHandwritingEnabled(boolean enabled) {
501             mBundle.putBoolean(CONNECTIONLESS_HANDWRITING_ENABLED, enabled);
502             return this;
503         }
504 
505         /**
506          * Sets whether the IME's
507          * {@link android.content.pm.ApplicationInfo#isOnBackInvokedCallbackEnabled()}
508          * should be set to {@code true}.
509          */
setOnBackCallbackEnabled(boolean enabled)510         public Builder setOnBackCallbackEnabled(boolean enabled) {
511             mBundle.putBoolean(ON_BACK_CALLBACK_ENABLED, enabled);
512             return this;
513         }
514 
515         /** Sets whether or not custom extract view hierarchy should be used. */
setCustomExtractTextViewEnabled(boolean enabled)516         public Builder setCustomExtractTextViewEnabled(boolean enabled) {
517             mBundle.putBoolean(USE_CUSTOM_EXTRACT_TEXT_VIEW, enabled);
518             return this;
519         }
520 
521         /**
522          * Sets whether {@link android.inputmethodservice.InputMethodService#onComputeInsets}
523          * should return zero insets.
524          */
setZeroInsets(boolean enabled)525         public Builder setZeroInsets(boolean enabled) {
526             mBundle.putBoolean(ZERO_INSETS, enabled);
527             return this;
528         }
529     }
530 }
531