Merge changes from topic "pss-app-selector-screenshot" into tm-qpr-dev
* changes: [Partial Screensharing] Add app selector activity screenshot test [Partial Screensharing] Update app selector (ChooserActivity) header
This commit is contained in:
committed by
Android (Google) Code Review
commit
47c21d1216
@@ -63,6 +63,7 @@ import android.database.DataSetObserver;
|
|||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Insets;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||||
@@ -146,6 +147,7 @@ import java.util.Comparator;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -281,6 +283,7 @@ public class ChooserActivity extends ResolverActivity implements
|
|||||||
private long mQueriedSharingShortcutsTimeMs;
|
private long mQueriedSharingShortcutsTimeMs;
|
||||||
|
|
||||||
private int mCurrAvailableWidth = 0;
|
private int mCurrAvailableWidth = 0;
|
||||||
|
private Insets mLastAppliedInsets = null;
|
||||||
private int mLastNumberOfChildren = -1;
|
private int mLastNumberOfChildren = -1;
|
||||||
private int mMaxTargetsPerRow = 1;
|
private int mMaxTargetsPerRow = 1;
|
||||||
|
|
||||||
@@ -1619,6 +1622,8 @@ public class ChooserActivity extends ResolverActivity implements
|
|||||||
if (mChooserMultiProfilePagerAdapter.getInactiveListAdapter() != null) {
|
if (mChooserMultiProfilePagerAdapter.getInactiveListAdapter() != null) {
|
||||||
mChooserMultiProfilePagerAdapter.getInactiveListAdapter().destroyAppPredictor();
|
mChooserMultiProfilePagerAdapter.getInactiveListAdapter().destroyAppPredictor();
|
||||||
}
|
}
|
||||||
|
mPersonalAppPredictor = null;
|
||||||
|
mWorkAppPredictor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override // ResolverListCommunicator
|
@Override // ResolverListCommunicator
|
||||||
@@ -2531,7 +2536,11 @@ public class ChooserActivity extends ResolverActivity implements
|
|||||||
|| gridAdapter.calculateChooserTargetWidth(availableWidth)
|
|| gridAdapter.calculateChooserTargetWidth(availableWidth)
|
||||||
|| recyclerView.getAdapter() == null
|
|| recyclerView.getAdapter() == null
|
||||||
|| availableWidth != mCurrAvailableWidth;
|
|| availableWidth != mCurrAvailableWidth;
|
||||||
|
|
||||||
|
boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets);
|
||||||
|
|
||||||
if (isLayoutUpdated
|
if (isLayoutUpdated
|
||||||
|
|| insetsChanged
|
||||||
|| mLastNumberOfChildren != recyclerView.getChildCount()) {
|
|| mLastNumberOfChildren != recyclerView.getChildCount()) {
|
||||||
mCurrAvailableWidth = availableWidth;
|
mCurrAvailableWidth = availableWidth;
|
||||||
if (isLayoutUpdated) {
|
if (isLayoutUpdated) {
|
||||||
@@ -2552,7 +2561,7 @@ public class ChooserActivity extends ResolverActivity implements
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mLastNumberOfChildren == recyclerView.getChildCount()) {
|
if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2563,6 +2572,7 @@ public class ChooserActivity extends ResolverActivity implements
|
|||||||
int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
|
int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
|
||||||
mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
|
mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
|
||||||
mEnterTransitionAnimationDelegate.markOffsetCalculated();
|
mEnterTransitionAnimationDelegate.markOffsetCalculated();
|
||||||
|
mLastAppliedInsets = mSystemWindowInsets;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3055,7 +3065,12 @@ public class ChooserActivity extends ResolverActivity implements
|
|||||||
mChooserMultiProfilePagerAdapter.setupContainerPadding(
|
mChooserMultiProfilePagerAdapter.setupContainerPadding(
|
||||||
getActiveEmptyStateView().findViewById(R.id.resolver_empty_state_container));
|
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) {
|
private void setHorizontalScrollingEnabled(boolean enabled) {
|
||||||
|
|||||||
@@ -79,6 +79,11 @@
|
|||||||
<java-symbol type="id" name="deny_button" />
|
<java-symbol type="id" name="deny_button" />
|
||||||
<java-symbol type="id" name="description" />
|
<java-symbol type="id" name="description" />
|
||||||
<java-symbol type="id" name="divider" />
|
<java-symbol type="id" name="divider" />
|
||||||
|
<java-symbol type="id" name="drag" />
|
||||||
|
<java-symbol type="id" name="profile_pager" />
|
||||||
|
<java-symbol type="id" name="chooser_header" />
|
||||||
|
<java-symbol type="id" name="content_preview_container" />
|
||||||
|
<java-symbol type="id" name="profile_tabhost" />
|
||||||
<java-symbol type="id" name="edit_query" />
|
<java-symbol type="id" name="edit_query" />
|
||||||
<java-symbol type="id" name="edittext_container" />
|
<java-symbol type="id" name="edittext_container" />
|
||||||
<java-symbol type="id" name="expand_activities_button" />
|
<java-symbol type="id" name="expand_activities_button" />
|
||||||
|
|||||||
25
packages/SystemUI/res/drawable/ic_present_to_all.xml
Normal file
25
packages/SystemUI/res/drawable/ic_present_to_all.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M21,3L3,3c-1.11,0 -2,0.89 -2,2v14c0,1.11 0.89,2 2,2h18c1.11,0 2,-0.89 2,-2L23,5c0,-1.11 -0.89,-2 -2,-2zM21,19.02L3,19.02L3,4.98h18v14.04zM8,12l4,-4 4,4 -1.41,1.41L13,11.83L13,16h-2v-4.17l-1.59,1.59L8,12z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
<com.android.internal.widget.ResolverDrawerLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
androidprv:maxCollapsedHeight="0dp"
|
||||||
|
androidprv:maxCollapsedHeightSmall="56dp"
|
||||||
|
androidprv:maxWidth="@*android:dimen/chooser_width"
|
||||||
|
android:id="@*android:id/contentPanel">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@*android:id/chooser_header"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
androidprv:layout_alwaysShow="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:elevation="0dp"
|
||||||
|
android:background="@*android:drawable/bottomsheet_background">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@*android:id/icon"
|
||||||
|
android:layout_width="@dimen/media_projection_app_selector_icon_size"
|
||||||
|
android:layout_height="@dimen/media_projection_app_selector_icon_size"
|
||||||
|
android:layout_marginTop="@*android:dimen/chooser_edge_margin_normal"
|
||||||
|
android:layout_marginBottom="@*android:dimen/chooser_edge_margin_normal"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:tint="?android:attr/textColorPrimary"/>
|
||||||
|
|
||||||
|
<TextView android:id="@*android:id/title"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingBottom="@*android:dimen/chooser_view_spacing"
|
||||||
|
android:paddingLeft="24dp"
|
||||||
|
android:paddingRight="24dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@*android:id/content_preview_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TabHost
|
||||||
|
android:id="@*android:id/profile_tabhost"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:background="?android:attr/colorBackground">
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<TabWidget
|
||||||
|
android:id="@*android:id/tabs"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone">
|
||||||
|
</TabWidget>
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@*android:id/tabcontent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<com.android.internal.app.ResolverViewPager
|
||||||
|
android:id="@*android:id/profile_pager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</TabHost>
|
||||||
|
|
||||||
|
</com.android.internal.widget.ResolverDrawerLayout>
|
||||||
@@ -1440,6 +1440,8 @@
|
|||||||
|
|
||||||
<dimen name="fgs_manager_list_top_spacing">12dp</dimen>
|
<dimen name="fgs_manager_list_top_spacing">12dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="media_projection_app_selector_icon_size">32dp</dimen>
|
||||||
|
|
||||||
<!-- Dream overlay related dimensions -->
|
<!-- Dream overlay related dimensions -->
|
||||||
<dimen name="dream_overlay_status_bar_height">60dp</dimen>
|
<dimen name="dream_overlay_status_bar_height">60dp</dimen>
|
||||||
<dimen name="dream_overlay_status_bar_margin">40dp</dimen>
|
<dimen name="dream_overlay_status_bar_margin">40dp</dimen>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, () -> Activity> = hashMapOf()
|
||||||
|
|
||||||
|
fun clearOverrides() {
|
||||||
|
overrides.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Activity> registerActivityOverride(activity: Class<T>, 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?"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.android.systemui.testing.screenshot
|
package com.android.systemui.testing.screenshot
|
||||||
|
|
||||||
|
import android.annotation.WorkerThread
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.HardwareRenderer
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
@@ -19,8 +21,13 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.concurrent.futures.ResolvableFuture
|
import androidx.concurrent.futures.ResolvableFuture
|
||||||
import androidx.test.annotation.ExperimentalTestApi
|
import androidx.test.annotation.ExperimentalTestApi
|
||||||
import androidx.test.core.internal.os.HandlerExecutor
|
import androidx.test.core.internal.os.HandlerExecutor
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
import androidx.test.platform.graphics.HardwareRendererCompat
|
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 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
|
* 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<Bitmap> {
|
|||||||
return bitmapFuture
|
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<Bitmap> {
|
||||||
|
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.
|
* Trigger a redraw of the given view.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -19,21 +19,13 @@ package com.android.systemui.testing.screenshot
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.HardwareRenderer
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroup.LayoutParams
|
import android.view.ViewGroup.LayoutParams
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.Window
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.test.espresso.Espresso
|
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
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.Assert.assertEquals
|
||||||
import org.junit.rules.RuleChain
|
import org.junit.rules.RuleChain
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
@@ -89,6 +81,8 @@ class ViewScreenshotTestRule(
|
|||||||
// Elevation/shadows is not deterministic when doing hardware rendering, so we disable
|
// Elevation/shadows is not deterministic when doing hardware rendering, so we disable
|
||||||
// it for any view in the hierarchy.
|
// it for any view in the hierarchy.
|
||||||
window.decorView.removeElevationRecursively()
|
window.decorView.removeElevationRecursively()
|
||||||
|
|
||||||
|
activity.currentFocus?.clearFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// We call onActivity again because it will make sure that our Activity is done measuring,
|
// 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 {
|
private fun Dialog.toBitmap(): Bitmap {
|
||||||
val window = window
|
val window = window
|
||||||
return window.decorView.toBitmap(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<Bitmap> {
|
|
||||||
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) {
|
enum class Mode(val layoutParams: LayoutParams) {
|
||||||
WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)),
|
WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)),
|
||||||
MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)),
|
MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)),
|
||||||
|
|||||||
@@ -23,35 +23,47 @@ import android.os.Binder
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ResultReceiver
|
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.ChooserActivity
|
||||||
|
import com.android.internal.app.ResolverListController
|
||||||
import com.android.internal.app.chooser.NotSelectableTargetInfo
|
import com.android.internal.app.chooser.NotSelectableTargetInfo
|
||||||
import com.android.internal.app.chooser.TargetInfo
|
import com.android.internal.app.chooser.TargetInfo
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
import com.android.systemui.util.AsyncActivityLauncher
|
import com.android.systemui.util.AsyncActivityLauncher
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R
|
||||||
import javax.inject.Inject
|
import com.android.internal.R as AndroidR
|
||||||
|
|
||||||
class MediaProjectionAppSelectorActivity @Inject constructor(
|
class MediaProjectionAppSelectorActivity constructor(
|
||||||
private val activityLauncher: AsyncActivityLauncher
|
private val activityLauncher: AsyncActivityLauncher,
|
||||||
|
/** This is used to override the dependency in a screenshot test */
|
||||||
|
@VisibleForTesting
|
||||||
|
private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)? = null
|
||||||
) : ChooserActivity() {
|
) : ChooserActivity() {
|
||||||
|
|
||||||
|
override fun getLayoutResource() =
|
||||||
|
R.layout.media_projection_app_selector
|
||||||
|
|
||||||
public override fun onCreate(bundle: Bundle?) {
|
public override fun onCreate(bundle: Bundle?) {
|
||||||
val queryIntent = Intent(Intent.ACTION_MAIN)
|
val queryIntent = Intent(Intent.ACTION_MAIN)
|
||||||
.addCategory(Intent.CATEGORY_LAUNCHER)
|
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
intent.putExtra(Intent.EXTRA_INTENT, queryIntent)
|
intent.putExtra(Intent.EXTRA_INTENT, queryIntent)
|
||||||
|
|
||||||
// TODO(b/235465652) Use resource lexeme
|
// TODO(b/240939253): update copies
|
||||||
intent.putExtra(Intent.EXTRA_TITLE, "Record or cast an app")
|
val title = getString(R.string.media_projection_dialog_service_title)
|
||||||
|
intent.putExtra(Intent.EXTRA_TITLE, title)
|
||||||
|
|
||||||
super.onCreate(bundle)
|
super.onCreate(bundle)
|
||||||
|
|
||||||
// TODO(b/235465652) we should update VisD of the title and add an icon
|
requireViewById<ImageView>(AndroidR.id.icon).setImageResource(R.drawable.ic_present_to_all)
|
||||||
findViewById<View>(R.id.title)?.visibility = View.VISIBLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun appliedThemeResId(): Int =
|
override fun appliedThemeResId(): Int =
|
||||||
R.style.Theme_SystemUI_MediaProjectionAppSelector
|
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) {
|
override fun startSelected(which: Int, always: Boolean, filtered: Boolean) {
|
||||||
val currentListAdapter = mChooserMultiProfilePagerAdapter.activeListAdapter
|
val currentListAdapter = mChooserMultiProfilePagerAdapter.activeListAdapter
|
||||||
val targetInfo = currentListAdapter.targetInfoForPosition(which, filtered) ?: return
|
val targetInfo = currentListAdapter.targetInfoForPosition(which, filtered) ?: return
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ package com.android.systemui.media.dagger
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import com.android.systemui.media.MediaProjectionAppSelectorActivity
|
import com.android.systemui.media.MediaProjectionAppSelectorActivity
|
||||||
|
import com.android.systemui.util.AsyncActivityLauncher
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
import dagger.multibindings.ClassKey
|
import dagger.multibindings.ClassKey
|
||||||
import dagger.multibindings.IntoMap
|
import dagger.multibindings.IntoMap
|
||||||
|
|
||||||
@@ -29,7 +31,17 @@ abstract class MediaProjectionModule {
|
|||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ClassKey(MediaProjectionAppSelectorActivity::class)
|
@ClassKey(MediaProjectionAppSelectorActivity::class)
|
||||||
abstract fun provideMediaProjectionAppSelectorActivity(
|
abstract fun bindMediaProjectionAppSelectorActivity(
|
||||||
activity: MediaProjectionAppSelectorActivity): Activity
|
activity: MediaProjectionAppSelectorActivity): Activity
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
fun provideMediaProjectionAppSelectorActivity(
|
||||||
|
activityLauncher: AsyncActivityLauncher
|
||||||
|
): MediaProjectionAppSelectorActivity {
|
||||||
|
return MediaProjectionAppSelectorActivity(
|
||||||
|
activityLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user