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