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