| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.globalactions; |
| |
| import static android.view.WindowInsets.Type.ime; |
| |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import android.app.Activity; |
| import android.os.Bundle; |
| import android.os.PowerManager; |
| import android.os.SystemClock; |
| import android.view.View; |
| import android.view.WindowInsets; |
| import android.view.WindowInsetsController; |
| import android.widget.EditText; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.test.filters.LargeTest; |
| import androidx.test.platform.app.InstrumentationRegistry; |
| import androidx.test.rule.ActivityTestRule; |
| |
| import com.android.systemui.SysuiTestCase; |
| |
| import org.junit.Rule; |
| import org.junit.Test; |
| |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.BooleanSupplier; |
| |
| @LargeTest |
| public class GlobalActionsImeTest extends SysuiTestCase { |
| |
| @Rule |
| public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>( |
| TestActivity.class, false, false); |
| |
| /** |
| * This test verifies that GlobalActions, which is frequently used to capture bugreports, |
| * doesn't interfere with the IME, i.e. soft-keyboard state. |
| */ |
| @Test |
| public void testGlobalActions_doesntStealImeControl() throws Exception { |
| turnScreenOn(); |
| final TestActivity activity = mActivityTestRule.launchActivity(null); |
| |
| waitUntil("Ime is visible", activity::isImeVisible); |
| |
| executeShellCommand("input keyevent --longpress POWER"); |
| |
| waitUntil("activity loses focus", () -> !activity.mHasFocus); |
| // Give the dialog time to animate in, and steal IME focus. Unfortunately, there's currently |
| // no better way to wait for this. |
| SystemClock.sleep(TimeUnit.SECONDS.toMillis(2)); |
| |
| runAssertionOnMainThread(() -> { |
| assertTrue("IME should remain visible behind GlobalActions, but didn't", |
| activity.mControlsIme); |
| assertTrue("App behind GlobalActions should remain in control of IME, but didn't", |
| activity.mImeVisible); |
| }); |
| } |
| |
| private void turnScreenOn() throws Exception { |
| PowerManager powerManager = mContext.getSystemService(PowerManager.class); |
| assertNotNull(powerManager); |
| if (powerManager.isInteractive()) { |
| return; |
| } |
| executeShellCommand("input keyevent KEYCODE_WAKEUP"); |
| waitUntil("Device not interactive", powerManager::isInteractive); |
| executeShellCommand("am wait-for-broadcast-idle"); |
| } |
| |
| private static void waitUntil(String message, BooleanSupplier predicate) |
| throws Exception { |
| int sleep = 125; |
| final long timeout = SystemClock.uptimeMillis() + 10_000; // 10 second timeout |
| while (SystemClock.uptimeMillis() < timeout) { |
| if (predicate.getAsBoolean()) { |
| return; // okay |
| } |
| Thread.sleep(sleep); |
| sleep *= 5; |
| sleep = Math.min(2000, sleep); |
| } |
| fail(message); |
| } |
| |
| private static void executeShellCommand(String cmd) { |
| InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd); |
| } |
| |
| /** |
| * Like Instrumentation.runOnMainThread(), but forwards AssertionErrors to the caller. |
| */ |
| private static void runAssertionOnMainThread(Runnable r) { |
| AssertionError[] t = new AssertionError[1]; |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| try { |
| r.run(); |
| } catch (AssertionError e) { |
| t[0] = e; |
| // Ignore assertion - throwing it here would crash the main thread. |
| } |
| }); |
| if (t[0] != null) { |
| throw t[0]; |
| } |
| } |
| |
| public static class TestActivity extends Activity implements |
| WindowInsetsController.OnControllableInsetsChangedListener, |
| View.OnApplyWindowInsetsListener { |
| |
| boolean mHasFocus; |
| boolean mControlsIme; |
| boolean mImeVisible; |
| |
| @Override |
| protected void onCreate(@Nullable Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| EditText content = new EditText(this); |
| content.setCursorVisible(false); // Otherwise, main thread doesn't go idle. |
| setContentView(content); |
| content.requestFocus(); |
| |
| getWindow().getDecorView().setOnApplyWindowInsetsListener(this); |
| WindowInsetsController wic = content.getWindowInsetsController(); |
| wic.addOnControllableInsetsChangedListener(this); |
| wic.show(ime()); |
| } |
| |
| @Override |
| public void onWindowFocusChanged(boolean hasFocus) { |
| synchronized (this) { |
| mHasFocus = hasFocus; |
| notifyAll(); |
| } |
| } |
| |
| @Override |
| public void onControllableInsetsChanged(@NonNull WindowInsetsController controller, |
| int typeMask) { |
| synchronized (this) { |
| mControlsIme = (typeMask & ime()) != 0; |
| notifyAll(); |
| } |
| } |
| |
| boolean isImeVisible() { |
| return mHasFocus && mControlsIme && mImeVisible; |
| } |
| |
| @Override |
| public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { |
| mImeVisible = insets.isVisible(ime()); |
| return v.onApplyWindowInsets(insets); |
| } |
| } |
| } |