• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.systemui.globalactions;
18 
19 import static android.provider.Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD;
20 import static android.view.WindowInsets.Type.ime;
21 
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertTrue;
25 import static org.junit.Assert.fail;
26 
27 import android.app.Activity;
28 import android.content.ContentResolver;
29 import android.os.Bundle;
30 import android.os.PowerManager;
31 import android.os.SystemClock;
32 import android.provider.Settings;
33 import android.view.View;
34 import android.view.WindowInsets;
35 import android.view.WindowInsetsController;
36 import android.widget.EditText;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 import androidx.test.filters.FlakyTest;
41 import androidx.test.filters.LargeTest;
42 import androidx.test.platform.app.InstrumentationRegistry;
43 import androidx.test.rule.ActivityTestRule;
44 
45 import com.android.systemui.SysuiTestCase;
46 
47 import org.junit.After;
48 import org.junit.Before;
49 import org.junit.Rule;
50 import org.junit.Test;
51 
52 import java.util.concurrent.TimeUnit;
53 import java.util.function.BooleanSupplier;
54 
55 @LargeTest
56 @FlakyTest(bugId = 176891566)
57 public class GlobalActionsImeTest extends SysuiTestCase {
58 
59     @Rule
60     public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>(
61             TestActivity.class, false, false);
62 
63     private int mOriginalShowImeWithHardKeyboard;
64 
65     @Before
setUp()66     public void setUp() {
67         final ContentResolver contentResolver = mContext.getContentResolver();
68         mOriginalShowImeWithHardKeyboard = Settings.Secure.getInt(
69                 contentResolver, SHOW_IME_WITH_HARD_KEYBOARD, 0);
70         // Forcibly shows IME even when hardware keyboard is connected.
71         // To change USER_SYSTEM settings, we have to use settings shell command.
72         executeShellCommand("settings put secure " + SHOW_IME_WITH_HARD_KEYBOARD + " 1");
73     }
74 
75     @After
tearDown()76     public void tearDown() {
77         // To restore USER_SYSTEM settings, we have to use settings shell command.
78         executeShellCommand("settings put secure "
79                 + SHOW_IME_WITH_HARD_KEYBOARD + " " + mOriginalShowImeWithHardKeyboard);
80         // Hide power menu and return to home screen
81         executeShellCommand("input keyevent --longpress POWER");
82         executeShellCommand("input keyevent HOME");
83     }
84 
85     /**
86      * This test verifies that GlobalActions, which is frequently used to capture bugreports,
87      * doesn't interfere with the IME, i.e. soft-keyboard state.
88      */
89     @Test
testGlobalActions_doesntStealImeControl()90     public void testGlobalActions_doesntStealImeControl() throws Exception {
91         turnScreenOn();
92         final TestActivity activity = mActivityTestRule.launchActivity(null);
93         boolean isImeVisible = waitUntil(activity::isImeVisible);
94         if (!isImeVisible) {
95             // Sometimes the keyboard is dismissed when run with other tests. Bringing it up again
96             // should improve test reliability
97             activity.showIme();
98             waitUntil("Ime is not visible", activity::isImeVisible);
99         }
100 
101         // In some cases, IME is not controllable. e.g., floating IME or fullscreen IME.
102         final boolean activityControlledIme = activity.mControlsIme;
103 
104         executeShellCommand("input keyevent --longpress POWER");
105 
106         waitUntil("activity loses focus", () -> !activity.mHasFocus);
107         // Give the dialog time to animate in, and steal IME focus. Unfortunately, there's currently
108         // no better way to wait for this.
109         SystemClock.sleep(TimeUnit.SECONDS.toMillis(2));
110 
111         runAssertionOnMainThread(() -> {
112             assertTrue("IME should remain visible behind GlobalActions, but didn't",
113                     activity.mImeVisible);
114             assertEquals("App behind GlobalActions should remain in control of IME, but didn't",
115                     activityControlledIme, activity.mControlsIme);
116         });
117     }
118 
turnScreenOn()119     private void turnScreenOn() throws Exception {
120         PowerManager powerManager = mContext.getSystemService(PowerManager.class);
121         assertNotNull(powerManager);
122         if (powerManager.isInteractive()) {
123             return;
124         }
125         executeShellCommand("input keyevent KEYCODE_WAKEUP");
126         waitUntil("Device not interactive", powerManager::isInteractive);
127         executeShellCommand("am wait-for-broadcast-idle");
128     }
129 
waitUntil(String message, BooleanSupplier predicate)130     private static void waitUntil(String message, BooleanSupplier predicate)
131             throws Exception {
132         if (!waitUntil(predicate)) {
133             fail(message);
134         }
135     }
136 
waitUntil(BooleanSupplier predicate)137     private static boolean waitUntil(BooleanSupplier predicate) throws Exception {
138         int sleep = 125;
139         final long timeout = SystemClock.uptimeMillis() + 10_000;  // 10 second timeout
140         while (SystemClock.uptimeMillis() < timeout) {
141             if (predicate.getAsBoolean()) {
142                 return true;
143             }
144             Thread.sleep(sleep);
145             sleep *= 5;
146             sleep = Math.min(2000, sleep);
147         }
148         return false;
149     }
150 
executeShellCommand(String cmd)151     private static void executeShellCommand(String cmd) {
152         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd);
153     }
154 
155     /**
156      * Like Instrumentation.runOnMainThread(), but forwards AssertionErrors to the caller.
157      */
runAssertionOnMainThread(Runnable r)158     private static void runAssertionOnMainThread(Runnable r) {
159         AssertionError[] t = new AssertionError[1];
160         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
161             try {
162                 r.run();
163             } catch (AssertionError e) {
164                 t[0] = e;
165                 // Ignore assertion - throwing it here would crash the main thread.
166             }
167         });
168         if (t[0] != null) {
169             throw t[0];
170         }
171     }
172 
173     public static class TestActivity extends Activity implements
174             WindowInsetsController.OnControllableInsetsChangedListener,
175             View.OnApplyWindowInsetsListener {
176 
177         private EditText mEditText;
178         boolean mHasFocus;
179         boolean mControlsIme;
180         boolean mImeVisible;
181 
182         @Override
onCreate(@ullable Bundle savedInstanceState)183         protected void onCreate(@Nullable Bundle savedInstanceState) {
184             super.onCreate(savedInstanceState);
185             setShowWhenLocked(true); // Allow this test to work even if device got stuck on keyguard
186             mEditText = new EditText(this);
187             mEditText.setCursorVisible(false);  // Otherwise, main thread doesn't go idle.
188             setContentView(mEditText);
189             showIme();
190         }
191 
showIme()192         private void showIme() {
193             mEditText.requestFocus();
194             getWindow().getDecorView().setOnApplyWindowInsetsListener(this);
195             WindowInsetsController wic = mEditText.getWindowInsetsController();
196             wic.addOnControllableInsetsChangedListener(this);
197             wic.show(ime());
198         }
199 
200         @Override
onWindowFocusChanged(boolean hasFocus)201         public void onWindowFocusChanged(boolean hasFocus) {
202             synchronized (this) {
203                 mHasFocus = hasFocus;
204                 notifyAll();
205             }
206         }
207 
208         @Override
onControllableInsetsChanged(@onNull WindowInsetsController controller, int typeMask)209         public void onControllableInsetsChanged(@NonNull WindowInsetsController controller,
210                 int typeMask) {
211             synchronized (this) {
212                 mControlsIme = (typeMask & ime()) != 0;
213                 notifyAll();
214             }
215         }
216 
isImeVisible()217         boolean isImeVisible() {
218             return mHasFocus && mImeVisible;
219         }
220 
221         @Override
onApplyWindowInsets(View v, WindowInsets insets)222         public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
223             mImeVisible = insets.isVisible(ime());
224             return v.onApplyWindowInsets(insets);
225         }
226     }
227 }
228