diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index fbabf52d096ec..a66b405844b8e 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -63,6 +63,7 @@ import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.AnimatedVectorDrawable;
@@ -146,6 +147,7 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.function.Supplier;
/**
@@ -281,6 +283,7 @@ public class ChooserActivity extends ResolverActivity implements
private long mQueriedSharingShortcutsTimeMs;
private int mCurrAvailableWidth = 0;
+ private Insets mLastAppliedInsets = null;
private int mLastNumberOfChildren = -1;
private int mMaxTargetsPerRow = 1;
@@ -1619,6 +1622,8 @@ public class ChooserActivity extends ResolverActivity implements
if (mChooserMultiProfilePagerAdapter.getInactiveListAdapter() != null) {
mChooserMultiProfilePagerAdapter.getInactiveListAdapter().destroyAppPredictor();
}
+ mPersonalAppPredictor = null;
+ mWorkAppPredictor = null;
}
@Override // ResolverListCommunicator
@@ -2531,7 +2536,11 @@ public class ChooserActivity extends ResolverActivity implements
|| gridAdapter.calculateChooserTargetWidth(availableWidth)
|| recyclerView.getAdapter() == null
|| availableWidth != mCurrAvailableWidth;
+
+ boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets);
+
if (isLayoutUpdated
+ || insetsChanged
|| mLastNumberOfChildren != recyclerView.getChildCount()) {
mCurrAvailableWidth = availableWidth;
if (isLayoutUpdated) {
@@ -2552,7 +2561,7 @@ public class ChooserActivity extends ResolverActivity implements
return;
}
- if (mLastNumberOfChildren == recyclerView.getChildCount()) {
+ if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
return;
}
@@ -2563,6 +2572,7 @@ public class ChooserActivity extends ResolverActivity implements
int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
mEnterTransitionAnimationDelegate.markOffsetCalculated();
+ mLastAppliedInsets = mSystemWindowInsets;
});
}
}
@@ -3055,7 +3065,12 @@ public class ChooserActivity extends ResolverActivity implements
mChooserMultiProfilePagerAdapter.setupContainerPadding(
getActiveEmptyStateView().findViewById(R.id.resolver_empty_state_container));
}
- return super.onApplyWindowInsets(v, insets);
+
+ WindowInsets result = super.onApplyWindowInsets(v, insets);
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.requestLayout();
+ }
+ return result;
}
private void setHorizontalScrollingEnabled(boolean enabled) {
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 71919ffd8223c..e773a9c706b0e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -79,6 +79,11 @@
+
+
+
+
+
diff --git a/packages/SystemUI/res/drawable/ic_present_to_all.xml b/packages/SystemUI/res/drawable/ic_present_to_all.xml
new file mode 100644
index 0000000000000..d6c9bbe0960ef
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_present_to_all.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/packages/SystemUI/res/layout/media_projection_app_selector.xml b/packages/SystemUI/res/layout/media_projection_app_selector.xml
new file mode 100644
index 0000000000000..4ad6849e9c45d
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_projection_app_selector.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e3be36503c873..e5e369ea79392 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1440,6 +1440,8 @@
12dp
+ 32dp
+
60dp
40dp
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
new file mode 100644
index 0000000000000..2e391c7aacbed
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 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.testing.screenshot
+
+import android.app.Activity
+import android.graphics.Color
+import android.view.View
+import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import platform.test.screenshot.*
+
+/**
+ * A rule that allows to run a screenshot diff test on a view that is hosted in another activity.
+ */
+class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule {
+
+ private val colorsRule = MaterialYouColorsRule()
+ private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
+ private val screenshotRule =
+ ScreenshotTestRule(
+ SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec))
+ )
+ private val delegateRule =
+ RuleChain.outerRule(colorsRule).around(deviceEmulationRule).around(screenshotRule)
+ private val matcher = UnitTestBitmapMatcher
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return delegateRule.apply(base, description)
+ }
+
+ /**
+ * Compare the content of the [view] with the golden image identified by [goldenIdentifier] in
+ * the context of [emulationSpec].
+ */
+ fun screenshotTest(goldenIdentifier: String, view: View) {
+ view.removeElevationRecursively()
+
+ ScreenshotRuleAsserter.Builder(screenshotRule)
+ .setScreenshotProvider { view.toBitmap() }
+ .withMatcher(matcher)
+ .build()
+ .assertGoldenImage(goldenIdentifier)
+ }
+
+ /**
+ * Compare the content of the [activity] with the golden image identified by [goldenIdentifier]
+ * in the context of [emulationSpec].
+ */
+ fun activityScreenshotTest(
+ goldenIdentifier: String,
+ activity: Activity,
+ ) {
+ val rootView = activity.window.decorView
+
+ // Hide system bars, remove insets, focus and make sure device-specific cutouts
+ // don't affect screenshots
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ val window = activity.window
+ window.setDecorFitsSystemWindows(false)
+ WindowInsetsControllerCompat(window, rootView).apply {
+ hide(WindowInsetsCompat.Type.systemBars())
+ systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+
+ window.statusBarColor = Color.TRANSPARENT
+ window.navigationBarColor = Color.TRANSPARENT
+ window.attributes =
+ window.attributes.apply {
+ layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+ }
+
+ rootView.removeInsetsRecursively()
+ activity.currentFocus?.clearFocus()
+ }
+
+ screenshotTest(goldenIdentifier, rootView)
+ }
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/TestAppComponentFactory.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/TestAppComponentFactory.kt
new file mode 100644
index 0000000000000..98e9aaf44ceb2
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/TestAppComponentFactory.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 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.testing.screenshot
+
+import android.app.Activity
+import android.content.Intent
+import androidx.core.app.AppComponentFactory
+
+class TestAppComponentFactory : AppComponentFactory() {
+
+ init {
+ instance = this
+ }
+
+ private val overrides: MutableMap Activity> = hashMapOf()
+
+ fun clearOverrides() {
+ overrides.clear()
+ }
+
+ fun registerActivityOverride(activity: Class, provider: () -> T) {
+ overrides[activity.name] = provider
+ }
+
+ override fun instantiateActivityCompat(
+ cl: ClassLoader,
+ className: String,
+ intent: Intent?
+ ): Activity {
+ return overrides
+ .getOrDefault(className) { super.instantiateActivityCompat(cl, className, intent) }
+ .invoke()
+ }
+
+ companion object {
+
+ private var instance: TestAppComponentFactory? = null
+
+ fun getInstance(): TestAppComponentFactory =
+ instance
+ ?: error(
+ "TestAppComponentFactory is not initialized, " +
+ "did you specify it in the manifest?"
+ )
+ }
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/View.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/View.kt
new file mode 100644
index 0000000000000..b84d26a7034a7
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/View.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 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.testing.screenshot
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.util.children
+import android.view.WindowInsets
+
+/**
+ * Elevation/shadows is not deterministic when doing hardware rendering, this exentsion allows to
+ * disable it for any view in the hierarchy.
+ */
+fun View.removeElevationRecursively() {
+ this.elevation = 0f
+ (this as? ViewGroup)?.children?.forEach(View::removeElevationRecursively)
+}
+
+/**
+ * Different devices could have different insets (e.g. different height of the navigation bar or
+ * taskbar). This method dispatches empty insets to the whole view hierarchy and removes
+ * the original listener, so the views won't receive real insets.
+ */
+fun View.removeInsetsRecursively() {
+ this.dispatchApplyWindowInsets(WindowInsets.CONSUMED)
+ this.setOnApplyWindowInsetsListener(null)
+ (this as? ViewGroup)?.children?.forEach(View::removeInsetsRecursively)
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewCapture.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewCapture.kt
index c609e6f8b4bf9..cdedc64ed0e9c 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewCapture.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewCapture.kt
@@ -1,10 +1,12 @@
package com.android.systemui.testing.screenshot
+import android.annotation.WorkerThread
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.graphics.Bitmap
import android.graphics.Canvas
+import android.graphics.HardwareRenderer
import android.graphics.Rect
import android.os.Build
import android.os.Handler
@@ -19,8 +21,13 @@ import androidx.annotation.RequiresApi
import androidx.concurrent.futures.ResolvableFuture
import androidx.test.annotation.ExperimentalTestApi
import androidx.test.core.internal.os.HandlerExecutor
+import androidx.test.espresso.Espresso
import androidx.test.platform.graphics.HardwareRendererCompat
+import com.google.common.util.concurrent.FutureCallback
+import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.runBlocking
/*
* This file was forked from androidx/test/core/view/ViewCapture.kt to add [Window] parameter to
@@ -61,6 +68,47 @@ fun View.captureToBitmap(window: Window? = null): ListenableFuture {
return bitmapFuture
}
+/**
+ * Synchronously captures an image of the view into a [Bitmap]. Synchronous equivalent of
+ * [captureToBitmap].
+ */
+@WorkerThread
+@ExperimentalTestApi
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+fun View.toBitmap(window: Window? = null): Bitmap {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ error("toBitmap() can't be called from the main thread")
+ }
+
+ if (!HardwareRenderer.isDrawingEnabled()) {
+ error("Hardware rendering is not enabled")
+ }
+
+ // Make sure we are idle.
+ Espresso.onIdle()
+
+ val mainExecutor = context.mainExecutor
+ return runBlocking {
+ suspendCoroutine { continuation ->
+ Futures.addCallback(
+ captureToBitmap(window),
+ object : FutureCallback {
+ override fun onSuccess(result: Bitmap) {
+ continuation.resumeWith(Result.success(result))
+ }
+
+ override fun onFailure(t: Throwable) {
+ continuation.resumeWith(Result.failure(t))
+ }
+ },
+ // We know that we are not on the main thread, so we can block the current
+ // thread and wait for the result in the main thread.
+ mainExecutor,
+ )
+ }
+ }
+}
+
/**
* Trigger a redraw of the given view.
*
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
index 47e2d2ccd13e7..0b0595f4405f3 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -19,21 +19,13 @@ package com.android.systemui.testing.screenshot
import android.app.Activity
import android.app.Dialog
import android.graphics.Bitmap
-import android.graphics.HardwareRenderer
-import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import android.view.Window
import androidx.activity.ComponentActivity
-import androidx.test.espresso.Espresso
import androidx.test.ext.junit.rules.ActivityScenarioRule
-import com.google.common.util.concurrent.FutureCallback
-import com.google.common.util.concurrent.Futures
-import kotlin.coroutines.suspendCoroutine
-import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
@@ -89,6 +81,8 @@ class ViewScreenshotTestRule(
// Elevation/shadows is not deterministic when doing hardware rendering, so we disable
// it for any view in the hierarchy.
window.decorView.removeElevationRecursively()
+
+ activity.currentFocus?.clearFocus()
}
// We call onActivity again because it will make sure that our Activity is done measuring,
@@ -150,53 +144,11 @@ class ViewScreenshotTestRule(
}
}
- private fun View.removeElevationRecursively() {
- this.elevation = 0f
-
- if (this is ViewGroup) {
- repeat(childCount) { i -> getChildAt(i).removeElevationRecursively() }
- }
- }
-
private fun Dialog.toBitmap(): Bitmap {
val window = window
return window.decorView.toBitmap(window)
}
- private fun View.toBitmap(window: Window? = null): Bitmap {
- if (Looper.getMainLooper() == Looper.myLooper()) {
- error("toBitmap() can't be called from the main thread")
- }
-
- if (!HardwareRenderer.isDrawingEnabled()) {
- error("Hardware rendering is not enabled")
- }
-
- // Make sure we are idle.
- Espresso.onIdle()
-
- val mainExecutor = context.mainExecutor
- return runBlocking {
- suspendCoroutine { continuation ->
- Futures.addCallback(
- captureToBitmap(window),
- object : FutureCallback {
- override fun onSuccess(result: Bitmap?) {
- continuation.resumeWith(Result.success(result!!))
- }
-
- override fun onFailure(t: Throwable) {
- continuation.resumeWith(Result.failure(t))
- }
- },
- // We know that we are not on the main thread, so we can block the current
- // thread and wait for the result in the main thread.
- mainExecutor,
- )
- }
- }
- }
-
enum class Mode(val layoutParams: LayoutParams) {
WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)),
MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)),
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index 53abd9948d030..0f1ee31e066ec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -23,35 +23,47 @@ import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.ResultReceiver
-import android.view.View
+import android.os.UserHandle
+import android.widget.ImageView
import com.android.internal.app.ChooserActivity
+import com.android.internal.app.ResolverListController
import com.android.internal.app.chooser.NotSelectableTargetInfo
import com.android.internal.app.chooser.TargetInfo
+import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.util.AsyncActivityLauncher
-import com.android.systemui.R;
-import javax.inject.Inject
+import com.android.systemui.R
+import com.android.internal.R as AndroidR
-class MediaProjectionAppSelectorActivity @Inject constructor(
- private val activityLauncher: AsyncActivityLauncher
+class MediaProjectionAppSelectorActivity constructor(
+ private val activityLauncher: AsyncActivityLauncher,
+ /** This is used to override the dependency in a screenshot test */
+ @VisibleForTesting
+ private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)? = null
) : ChooserActivity() {
+ override fun getLayoutResource() =
+ R.layout.media_projection_app_selector
+
public override fun onCreate(bundle: Bundle?) {
val queryIntent = Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
intent.putExtra(Intent.EXTRA_INTENT, queryIntent)
- // TODO(b/235465652) Use resource lexeme
- intent.putExtra(Intent.EXTRA_TITLE, "Record or cast an app")
+ // TODO(b/240939253): update copies
+ val title = getString(R.string.media_projection_dialog_service_title)
+ intent.putExtra(Intent.EXTRA_TITLE, title)
super.onCreate(bundle)
- // TODO(b/235465652) we should update VisD of the title and add an icon
- findViewById(R.id.title)?.visibility = View.VISIBLE
+ requireViewById(AndroidR.id.icon).setImageResource(R.drawable.ic_present_to_all)
}
override fun appliedThemeResId(): Int =
R.style.Theme_SystemUI_MediaProjectionAppSelector
+ override fun createListController(userHandle: UserHandle): ResolverListController =
+ listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
+
override fun startSelected(which: Int, always: Boolean, filtered: Boolean) {
val currentListAdapter = mChooserMultiProfilePagerAdapter.activeListAdapter
val targetInfo = currentListAdapter.targetInfoForPosition(which, filtered) ?: return
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
index e33a1b909d482..9696998340240 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
@@ -18,8 +18,10 @@ package com.android.systemui.media.dagger
import android.app.Activity
import com.android.systemui.media.MediaProjectionAppSelectorActivity
+import com.android.systemui.util.AsyncActivityLauncher
import dagger.Binds
import dagger.Module
+import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@@ -29,7 +31,17 @@ abstract class MediaProjectionModule {
@Binds
@IntoMap
@ClassKey(MediaProjectionAppSelectorActivity::class)
- abstract fun provideMediaProjectionAppSelectorActivity(
+ abstract fun bindMediaProjectionAppSelectorActivity(
activity: MediaProjectionAppSelectorActivity): Activity
+ companion object {
+ @Provides
+ fun provideMediaProjectionAppSelectorActivity(
+ activityLauncher: AsyncActivityLauncher
+ ): MediaProjectionAppSelectorActivity {
+ return MediaProjectionAppSelectorActivity(
+ activityLauncher
+ )
+ }
+ }
}