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