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