Merge changes from topics "people-space-screenshot", "screenshot-hardware" into tm-qpr-dev
* changes: Make the PeopleSpaceActivity screenshot testable Render View into Bitmap using hardware acceleration (1/2)
This commit is contained in:
committed by
Android (Google) Code Review
commit
9fcf0aac75
@@ -110,6 +110,7 @@ android_library {
|
|||||||
"androidx.arch.core_core-runtime",
|
"androidx.arch.core_core-runtime",
|
||||||
"androidx.lifecycle_lifecycle-common-java8",
|
"androidx.lifecycle_lifecycle-common-java8",
|
||||||
"androidx.lifecycle_lifecycle-extensions",
|
"androidx.lifecycle_lifecycle-extensions",
|
||||||
|
"androidx.lifecycle_lifecycle-runtime-ktx",
|
||||||
"androidx.dynamicanimation_dynamicanimation",
|
"androidx.dynamicanimation_dynamicanimation",
|
||||||
"androidx-constraintlayout_constraintlayout",
|
"androidx-constraintlayout_constraintlayout",
|
||||||
"androidx.exifinterface_exifinterface",
|
"androidx.exifinterface_exifinterface",
|
||||||
@@ -218,6 +219,7 @@ android_library {
|
|||||||
"androidx.arch.core_core-runtime",
|
"androidx.arch.core_core-runtime",
|
||||||
"androidx.lifecycle_lifecycle-common-java8",
|
"androidx.lifecycle_lifecycle-common-java8",
|
||||||
"androidx.lifecycle_lifecycle-extensions",
|
"androidx.lifecycle_lifecycle-extensions",
|
||||||
|
"androidx.lifecycle_lifecycle-runtime-ktx",
|
||||||
"androidx.dynamicanimation_dynamicanimation",
|
"androidx.dynamicanimation_dynamicanimation",
|
||||||
"androidx-constraintlayout_constraintlayout",
|
"androidx-constraintlayout_constraintlayout",
|
||||||
"androidx.exifinterface_exifinterface",
|
"androidx.exifinterface_exifinterface",
|
||||||
|
|||||||
@@ -13,103 +13,11 @@
|
|||||||
~ See the License for the specific language governing permissions and
|
~ See the License for the specific language governing permissions and
|
||||||
~ limitations under the License.
|
~ limitations under the License.
|
||||||
-->
|
-->
|
||||||
<LinearLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
android:id="@+id/container"
|
||||||
android:id="@+id/top_level"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:orientation="vertical"
|
<!-- The content of people_space_activity_(no|with)_conversations.xml will be added here at
|
||||||
android:padding="8dp">
|
runtime depending on the number of conversations to show. -->
|
||||||
<TextView
|
</FrameLayout>
|
||||||
android:id="@+id/select_conversation_title"
|
|
||||||
android:text="@string/select_conversation_title"
|
|
||||||
android:gravity="center"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
|
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
|
||||||
android:textSize="24sp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/select_conversation"
|
|
||||||
android:text="@string/select_conversation_text"
|
|
||||||
android:gravity="center"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
|
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:paddingVertical="24dp"
|
|
||||||
android:paddingHorizontal="48dp"/>
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
|
||||||
android:id="@+id/scroll_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/scroll_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/priority"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="35dp">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/priority_header"
|
|
||||||
android:text="@string/priority_conversations"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
|
|
||||||
android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:paddingStart="16dp"
|
|
||||||
android:layout_height="wrap_content"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/priority_tiles"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:background="@drawable/rounded_bg_full_large_radius"
|
|
||||||
android:clipToOutline="true">
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/recent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/recent_header"
|
|
||||||
android:gravity="start"
|
|
||||||
android:text="@string/recent_conversations"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
|
|
||||||
android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:paddingStart="16dp"
|
|
||||||
android:layout_height="wrap_content"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/recent_tiles"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:background="@drawable/rounded_bg_full_large_radius"
|
|
||||||
android:clipToOutline="true">
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||||
android:id="@+id/top_level"
|
android:id="@+id/top_level_no_conversations"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="24dp"
|
android:padding="24dp"
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||||
|
android:id="@+id/top_level_with_conversations"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/select_conversation_title"
|
||||||
|
android:text="@string/select_conversation_title"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="24sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/select_conversation"
|
||||||
|
android:text="@string/select_conversation_text"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:paddingVertical="24dp"
|
||||||
|
android:paddingHorizontal="48dp"/>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:id="@+id/scroll_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/scroll_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/priority"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="35dp">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/priority_header"
|
||||||
|
android:text="@string/priority_conversations"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
|
||||||
|
android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/priority_tiles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@drawable/rounded_bg_full_large_radius"
|
||||||
|
android:clipToOutline="true">
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/recent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/recent_header"
|
||||||
|
android:gravity="start"
|
||||||
|
android:text="@string/recent_conversations"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
|
||||||
|
android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/recent_tiles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@drawable/rounded_bg_full_large_radius"
|
||||||
|
android:clipToOutline="true">
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
</LinearLayout>
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/tile_view_person_icon"
|
android:id="@+id/tile_view_person_icon"
|
||||||
android:layout_width="52dp"
|
android:layout_width="@dimen/avatar_size_for_medium"
|
||||||
android:layout_height="52dp" />
|
android:layout_height="@dimen/avatar_size_for_medium" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ android_library {
|
|||||||
"androidx.test.espresso.core",
|
"androidx.test.espresso.core",
|
||||||
"androidx.appcompat_appcompat",
|
"androidx.appcompat_appcompat",
|
||||||
"platform-screenshot-diff-core",
|
"platform-screenshot-diff-core",
|
||||||
|
"guava",
|
||||||
],
|
],
|
||||||
|
|
||||||
kotlincflags: ["-Xjvm-default=all"],
|
kotlincflags: ["-Xjvm-default=all"],
|
||||||
|
|||||||
@@ -19,6 +19,12 @@
|
|||||||
<item name="android:windowActionBar">false</item>
|
<item name="android:windowActionBar">false</item>
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
|
||||||
|
<!-- We make the status and navigation bars transparent so that the screenshotted content is
|
||||||
|
not clipped by the status bar height when drawn into the Bitmap (which is what happens
|
||||||
|
given that we draw the view into the Bitmap using hardware acceleration). -->
|
||||||
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
|
||||||
<!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests -->
|
<!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests -->
|
||||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import platform.test.screenshot.matchers.MSSIMMatcher
|
|||||||
import platform.test.screenshot.matchers.PixelPerfectMatcher
|
import platform.test.screenshot.matchers.PixelPerfectMatcher
|
||||||
|
|
||||||
/** Draw this [View] into a [Bitmap]. */
|
/** Draw this [View] into a [Bitmap]. */
|
||||||
|
// TODO(b/195673633): Remove this once Compose screenshot tests use hardware rendering for their
|
||||||
|
// tests.
|
||||||
fun View.drawIntoBitmap(): Bitmap {
|
fun View.drawIntoBitmap(): Bitmap {
|
||||||
val bitmap =
|
val bitmap =
|
||||||
Bitmap.createBitmap(
|
Bitmap.createBitmap(
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package com.android.systemui.testing.screenshot
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.PixelCopy
|
||||||
|
import android.view.SurfaceView
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
import android.view.Window
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.concurrent.futures.ResolvableFuture
|
||||||
|
import androidx.test.annotation.ExperimentalTestApi
|
||||||
|
import androidx.test.core.internal.os.HandlerExecutor
|
||||||
|
import androidx.test.platform.graphics.HardwareRendererCompat
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file was forked from androidx/test/core/view/ViewCapture.kt to add [Window] parameter to
|
||||||
|
* [View.captureToBitmap].
|
||||||
|
* TODO(b/195673633): Remove this fork and use the AndroidX version instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously captures an image of the underlying view into a [Bitmap].
|
||||||
|
*
|
||||||
|
* For devices below [Build.VERSION_CODES#O] (or if the view's window cannot be determined), the
|
||||||
|
* image is obtained using [View#draw]. Otherwise, [PixelCopy] is used.
|
||||||
|
*
|
||||||
|
* This method will also enable [HardwareRendererCompat#setDrawingEnabled(boolean)] if required.
|
||||||
|
*
|
||||||
|
* This API is primarily intended for use in lower layer libraries or frameworks. For test authors,
|
||||||
|
* its recommended to use espresso or compose's captureToImage.
|
||||||
|
*
|
||||||
|
* This API is currently experimental and subject to change or removal.
|
||||||
|
*/
|
||||||
|
@ExperimentalTestApi
|
||||||
|
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
fun View.captureToBitmap(window: Window? = null): ListenableFuture<Bitmap> {
|
||||||
|
val bitmapFuture: ResolvableFuture<Bitmap> = ResolvableFuture.create()
|
||||||
|
val mainExecutor = HandlerExecutor(Handler(Looper.getMainLooper()))
|
||||||
|
|
||||||
|
// disable drawing again if necessary once work is complete
|
||||||
|
if (!HardwareRendererCompat.isDrawingEnabled()) {
|
||||||
|
HardwareRendererCompat.setDrawingEnabled(true)
|
||||||
|
bitmapFuture.addListener({ HardwareRendererCompat.setDrawingEnabled(false) }, mainExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainExecutor.execute {
|
||||||
|
val forceRedrawFuture = forceRedraw()
|
||||||
|
forceRedrawFuture.addListener({ generateBitmap(bitmapFuture, window) }, mainExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmapFuture
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a redraw of the given view.
|
||||||
|
*
|
||||||
|
* Should only be called on UI thread.
|
||||||
|
*
|
||||||
|
* @return a [ListenableFuture] that will be complete once ui drawing is complete
|
||||||
|
*/
|
||||||
|
// NoClassDefFoundError occurs on API 15
|
||||||
|
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
// @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
||||||
|
@ExperimentalTestApi
|
||||||
|
fun View.forceRedraw(): ListenableFuture<Void> {
|
||||||
|
val future: ResolvableFuture<Void> = ResolvableFuture.create()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 29 && isHardwareAccelerated) {
|
||||||
|
viewTreeObserver.registerFrameCommitCallback() { future.set(null) }
|
||||||
|
} else {
|
||||||
|
viewTreeObserver.addOnDrawListener(
|
||||||
|
object : ViewTreeObserver.OnDrawListener {
|
||||||
|
var handled = false
|
||||||
|
override fun onDraw() {
|
||||||
|
if (!handled) {
|
||||||
|
handled = true
|
||||||
|
future.set(null)
|
||||||
|
// cannot remove on draw listener inside of onDraw
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
viewTreeObserver.removeOnDrawListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
invalidate()
|
||||||
|
return future
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun View.generateBitmap(
|
||||||
|
bitmapFuture: ResolvableFuture<Bitmap>,
|
||||||
|
window: Window? = null,
|
||||||
|
) {
|
||||||
|
if (bitmapFuture.isCancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val destBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
when {
|
||||||
|
Build.VERSION.SDK_INT < 26 -> generateBitmapFromDraw(destBitmap, bitmapFuture)
|
||||||
|
this is SurfaceView -> generateBitmapFromSurfaceViewPixelCopy(destBitmap, bitmapFuture)
|
||||||
|
else -> {
|
||||||
|
val window = window ?: getActivity()?.window
|
||||||
|
if (window != null) {
|
||||||
|
generateBitmapFromPixelCopy(window, destBitmap, bitmapFuture)
|
||||||
|
} else {
|
||||||
|
Log.i(
|
||||||
|
"View.captureToImage",
|
||||||
|
"Could not find window for view. Falling back to View#draw instead of PixelCopy"
|
||||||
|
)
|
||||||
|
generateBitmapFromDraw(destBitmap, bitmapFuture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("NewApi")
|
||||||
|
private fun SurfaceView.generateBitmapFromSurfaceViewPixelCopy(
|
||||||
|
destBitmap: Bitmap,
|
||||||
|
bitmapFuture: ResolvableFuture<Bitmap>
|
||||||
|
) {
|
||||||
|
val onCopyFinished =
|
||||||
|
PixelCopy.OnPixelCopyFinishedListener { result ->
|
||||||
|
if (result == PixelCopy.SUCCESS) {
|
||||||
|
bitmapFuture.set(destBitmap)
|
||||||
|
} else {
|
||||||
|
bitmapFuture.setException(
|
||||||
|
RuntimeException(String.format("PixelCopy failed: %d", result))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PixelCopy.request(this, null, destBitmap, onCopyFinished, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun View.generateBitmapFromDraw(
|
||||||
|
destBitmap: Bitmap,
|
||||||
|
bitmapFuture: ResolvableFuture<Bitmap>
|
||||||
|
) {
|
||||||
|
destBitmap.density = resources.displayMetrics.densityDpi
|
||||||
|
computeScroll()
|
||||||
|
val canvas = Canvas(destBitmap)
|
||||||
|
canvas.translate((-scrollX).toFloat(), (-scrollY).toFloat())
|
||||||
|
draw(canvas)
|
||||||
|
bitmapFuture.set(destBitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun View.getActivity(): Activity? {
|
||||||
|
fun Context.getActivity(): Activity? {
|
||||||
|
return when (this) {
|
||||||
|
is Activity -> this
|
||||||
|
is ContextWrapper -> this.baseContext.getActivity()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context.getActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun View.generateBitmapFromPixelCopy(
|
||||||
|
window: Window,
|
||||||
|
destBitmap: Bitmap,
|
||||||
|
bitmapFuture: ResolvableFuture<Bitmap>
|
||||||
|
) {
|
||||||
|
val locationInWindow = intArrayOf(0, 0)
|
||||||
|
getLocationInWindow(locationInWindow)
|
||||||
|
val x = locationInWindow[0]
|
||||||
|
val y = locationInWindow[1]
|
||||||
|
val boundsInWindow = Rect(x, y, x + width, y + height)
|
||||||
|
|
||||||
|
return window.generateBitmapFromPixelCopy(boundsInWindow, destBitmap, bitmapFuture)
|
||||||
|
}
|
||||||
@@ -18,10 +18,22 @@ 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.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.WRAP_CONTENT
|
||||||
|
import android.view.Window
|
||||||
|
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
|
||||||
@@ -59,29 +71,39 @@ class ViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule {
|
|||||||
*/
|
*/
|
||||||
fun screenshotTest(
|
fun screenshotTest(
|
||||||
goldenIdentifier: String,
|
goldenIdentifier: String,
|
||||||
layoutParams: LayoutParams =
|
mode: Mode = Mode.WrapContent,
|
||||||
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT),
|
viewProvider: (ComponentActivity) -> View,
|
||||||
viewProvider: (Activity) -> View,
|
|
||||||
) {
|
) {
|
||||||
activityRule.scenario.onActivity { activity ->
|
activityRule.scenario.onActivity { activity ->
|
||||||
// Make sure that the activity draws full screen and fits the whole display instead of
|
// Make sure that the activity draws full screen and fits the whole display instead of
|
||||||
// the system bars.
|
// the system bars.
|
||||||
activity.window.setDecorFitsSystemWindows(false)
|
val window = activity.window
|
||||||
activity.setContentView(viewProvider(activity), layoutParams)
|
window.setDecorFitsSystemWindows(false)
|
||||||
|
|
||||||
|
// Set the content.
|
||||||
|
activity.setContentView(viewProvider(activity), mode.layoutParams)
|
||||||
|
|
||||||
|
// Elevation/shadows is not deterministic when doing hardware rendering, so we disable
|
||||||
|
// it for any view in the hierarchy.
|
||||||
|
window.decorView.removeElevationRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
||||||
// laying out and drawing its content (that we set in the previous onActivity lambda).
|
// laying out and drawing its content (that we set in the previous onActivity lambda).
|
||||||
|
var contentView: View? = null
|
||||||
activityRule.scenario.onActivity { activity ->
|
activityRule.scenario.onActivity { activity ->
|
||||||
// Check that the content is what we expected.
|
// Check that the content is what we expected.
|
||||||
val content = activity.requireViewById<ViewGroup>(android.R.id.content)
|
val content = activity.requireViewById<ViewGroup>(android.R.id.content)
|
||||||
assertEquals(1, content.childCount)
|
assertEquals(1, content.childCount)
|
||||||
screenshotRule.assertBitmapAgainstGolden(
|
contentView = content.getChildAt(0)
|
||||||
content.getChildAt(0).drawIntoBitmap(),
|
|
||||||
goldenIdentifier,
|
|
||||||
matcher
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bitmap = contentView?.toBitmap() ?: error("contentView is null")
|
||||||
|
screenshotRule.assertBitmapAgainstGolden(
|
||||||
|
bitmap,
|
||||||
|
goldenIdentifier,
|
||||||
|
matcher,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,25 +126,78 @@ class ViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule {
|
|||||||
create()
|
create()
|
||||||
window.setWindowAnimations(0)
|
window.setWindowAnimations(0)
|
||||||
|
|
||||||
|
// Elevation/shadows is not deterministic when doing hardware rendering, so we
|
||||||
|
// disable it for any view in the hierarchy.
|
||||||
|
window.decorView.removeElevationRecursively()
|
||||||
|
|
||||||
// Show the dialog.
|
// Show the dialog.
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We call onActivity again because it will make sure that our Dialog is done measuring,
|
try {
|
||||||
// laying out and drawing its content (that we set in the previous onActivity lambda).
|
val bitmap = dialog?.toBitmap() ?: error("dialog is null")
|
||||||
activityRule.scenario.onActivity {
|
screenshotRule.assertBitmapAgainstGolden(
|
||||||
// Check that the content is what we expected.
|
bitmap,
|
||||||
val dialog = dialog ?: error("dialog is null")
|
goldenIdentifier,
|
||||||
try {
|
matcher,
|
||||||
screenshotRule.assertBitmapAgainstGolden(
|
)
|
||||||
dialog.window.decorView.drawIntoBitmap(),
|
} finally {
|
||||||
goldenIdentifier,
|
dialog?.dismiss()
|
||||||
matcher,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<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,
|
||||||
)
|
)
|
||||||
} finally {
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class Mode(val layoutParams: LayoutParams) {
|
||||||
|
WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)),
|
||||||
|
MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)),
|
||||||
|
MatchWidth(LayoutParams(MATCH_PARENT, WRAP_CONTENT)),
|
||||||
|
MatchHeight(LayoutParams(WRAP_CONTENT, MATCH_PARENT)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.android.systemui.testing.screenshot
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.PixelCopy
|
||||||
|
import android.view.Window
|
||||||
|
import androidx.concurrent.futures.ResolvableFuture
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file was forked from androidx/test/core/view/WindowCapture.kt.
|
||||||
|
* TODO(b/195673633): Remove this fork and use the AndroidX version instead.
|
||||||
|
*/
|
||||||
|
fun Window.generateBitmapFromPixelCopy(
|
||||||
|
boundsInWindow: Rect? = null,
|
||||||
|
destBitmap: Bitmap,
|
||||||
|
bitmapFuture: ResolvableFuture<Bitmap>
|
||||||
|
) {
|
||||||
|
val onCopyFinished =
|
||||||
|
PixelCopy.OnPixelCopyFinishedListener { result ->
|
||||||
|
if (result == PixelCopy.SUCCESS) {
|
||||||
|
bitmapFuture.set(destBitmap)
|
||||||
|
} else {
|
||||||
|
bitmapFuture.setException(
|
||||||
|
RuntimeException(String.format("PixelCopy failed: %d", result))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PixelCopy.request(
|
||||||
|
this,
|
||||||
|
boundsInWindow,
|
||||||
|
destBitmap,
|
||||||
|
onCopyFinished,
|
||||||
|
Handler(Looper.getMainLooper())
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ import com.android.systemui.lowlightclock.LowLightClockController;
|
|||||||
import com.android.systemui.media.dagger.MediaProjectionModule;
|
import com.android.systemui.media.dagger.MediaProjectionModule;
|
||||||
import com.android.systemui.model.SysUiState;
|
import com.android.systemui.model.SysUiState;
|
||||||
import com.android.systemui.navigationbar.NavigationBarComponent;
|
import com.android.systemui.navigationbar.NavigationBarComponent;
|
||||||
|
import com.android.systemui.people.PeopleModule;
|
||||||
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
|
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
|
||||||
import com.android.systemui.privacy.PrivacyModule;
|
import com.android.systemui.privacy.PrivacyModule;
|
||||||
import com.android.systemui.recents.Recents;
|
import com.android.systemui.recents.Recents;
|
||||||
@@ -124,6 +125,7 @@ import dagger.Provides;
|
|||||||
LogModule.class,
|
LogModule.class,
|
||||||
MediaProjectionModule.class,
|
MediaProjectionModule.class,
|
||||||
PeopleHubModule.class,
|
PeopleHubModule.class,
|
||||||
|
PeopleModule.class,
|
||||||
PluginModule.class,
|
PluginModule.class,
|
||||||
PrivacyModule.class,
|
PrivacyModule.class,
|
||||||
QsFrameTranslateModule.class,
|
QsFrameTranslateModule.class,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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.people
|
||||||
|
|
||||||
|
import com.android.systemui.people.data.repository.PeopleTileRepository
|
||||||
|
import com.android.systemui.people.data.repository.PeopleTileRepositoryImpl
|
||||||
|
import com.android.systemui.people.data.repository.PeopleWidgetRepository
|
||||||
|
import com.android.systemui.people.data.repository.PeopleWidgetRepositoryImpl
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
|
||||||
|
/** Dagger module to provide/bind people space dependencies. */
|
||||||
|
@Module
|
||||||
|
interface PeopleModule {
|
||||||
|
@Binds fun bindTileRepository(impl: PeopleTileRepositoryImpl): PeopleTileRepository
|
||||||
|
|
||||||
|
@Binds fun bindWidgetRepository(impl: PeopleWidgetRepositoryImpl): PeopleWidgetRepository
|
||||||
|
}
|
||||||
@@ -19,144 +19,52 @@ package com.android.systemui.people;
|
|||||||
import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
|
import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
|
||||||
import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
|
import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
|
||||||
|
|
||||||
import static com.android.systemui.people.PeopleTileViewHelper.getPersonIconBitmap;
|
|
||||||
import static com.android.systemui.people.PeopleTileViewHelper.getSizeInDp;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.people.PeopleSpaceTile;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Outline;
|
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewOutlineProvider;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
|
|
||||||
import com.android.systemui.R;
|
import androidx.activity.ComponentActivity;
|
||||||
import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import com.android.systemui.people.widget.PeopleTileKey;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import com.android.systemui.people.ui.view.PeopleViewBinder;
|
||||||
import java.util.List;
|
import com.android.systemui.people.ui.viewmodel.PeopleViewModel;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
/** People Tile Widget configuration activity that shows the user their conversation tiles. */
|
/** People Tile Widget configuration activity that shows the user their conversation tiles. */
|
||||||
public class PeopleSpaceActivity extends Activity {
|
public class PeopleSpaceActivity extends ComponentActivity {
|
||||||
|
|
||||||
private static final String TAG = "PeopleSpaceActivity";
|
private static final String TAG = "PeopleSpaceActivity";
|
||||||
private static final boolean DEBUG = PeopleSpaceUtils.DEBUG;
|
private static final boolean DEBUG = PeopleSpaceUtils.DEBUG;
|
||||||
|
|
||||||
private PeopleSpaceWidgetManager mPeopleSpaceWidgetManager;
|
private final PeopleViewModel.Factory mViewModelFactory;
|
||||||
private Context mContext;
|
private PeopleViewModel mViewModel;
|
||||||
private int mAppWidgetId;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public PeopleSpaceActivity(PeopleSpaceWidgetManager peopleSpaceWidgetManager) {
|
public PeopleSpaceActivity(PeopleViewModel.Factory viewModelFactory) {
|
||||||
super();
|
super();
|
||||||
mPeopleSpaceWidgetManager = peopleSpaceWidgetManager;
|
mViewModelFactory = viewModelFactory;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
mContext = getApplicationContext();
|
|
||||||
mAppWidgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID,
|
|
||||||
INVALID_APPWIDGET_ID);
|
|
||||||
setResult(RESULT_CANCELED);
|
setResult(RESULT_CANCELED);
|
||||||
}
|
mViewModel = new ViewModelProvider(this, mViewModelFactory).get(PeopleViewModel.class);
|
||||||
|
|
||||||
/** Builds the conversation selection activity. */
|
// Update the widget ID coming from the intent.
|
||||||
private void buildActivity() {
|
int widgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
|
||||||
List<PeopleSpaceTile> priorityTiles = new ArrayList<>();
|
mViewModel.onWidgetIdChanged(widgetId);
|
||||||
List<PeopleSpaceTile> recentTiles = new ArrayList<>();
|
|
||||||
try {
|
|
||||||
priorityTiles = mPeopleSpaceWidgetManager.getPriorityTiles();
|
|
||||||
recentTiles = mPeopleSpaceWidgetManager.getRecentTiles();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Couldn't retrieve conversations", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no conversations, render activity without conversations
|
ViewGroup view = PeopleViewBinder.create(this);
|
||||||
if (recentTiles.isEmpty() && priorityTiles.isEmpty()) {
|
PeopleViewBinder.bind(view, mViewModel, /* lifecycleOwner= */ this,
|
||||||
setContentView(R.layout.people_space_activity_no_conversations);
|
() -> {
|
||||||
|
finishActivity();
|
||||||
// The Tile preview has colorBackground as its background. Change it so it's different
|
return null;
|
||||||
// than the activity's background.
|
});
|
||||||
LinearLayout item = findViewById(android.R.id.background);
|
setContentView(view);
|
||||||
GradientDrawable shape = (GradientDrawable) item.getBackground();
|
|
||||||
final TypedArray ta = mContext.getTheme().obtainStyledAttributes(
|
|
||||||
new int[]{com.android.internal.R.attr.colorSurface});
|
|
||||||
shape.setColor(ta.getColor(0, Color.WHITE));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.people_space_activity);
|
|
||||||
setTileViews(R.id.priority, R.id.priority_tiles, priorityTiles);
|
|
||||||
setTileViews(R.id.recent, R.id.recent_tiles, recentTiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() {
|
|
||||||
@Override
|
|
||||||
public void getOutline(View view, Outline outline) {
|
|
||||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(),
|
|
||||||
mContext.getResources().getDimension(R.dimen.people_space_widget_radius));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Sets a {@link PeopleSpaceTileView}s for each conversation. */
|
|
||||||
private void setTileViews(int viewId, int tilesId, List<PeopleSpaceTile> tiles) {
|
|
||||||
if (tiles.isEmpty()) {
|
|
||||||
LinearLayout view = findViewById(viewId);
|
|
||||||
view.setVisibility(View.GONE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewGroup layout = findViewById(tilesId);
|
|
||||||
layout.setClipToOutline(true);
|
|
||||||
layout.setOutlineProvider(mViewOutlineProvider);
|
|
||||||
for (int i = 0; i < tiles.size(); ++i) {
|
|
||||||
PeopleSpaceTile tile = tiles.get(i);
|
|
||||||
PeopleSpaceTileView tileView = new PeopleSpaceTileView(mContext,
|
|
||||||
layout, tile.getId(), i == (tiles.size() - 1));
|
|
||||||
setTileView(tileView, tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets {@code tileView} with the data in {@code conversation}. */
|
|
||||||
private void setTileView(PeopleSpaceTileView tileView, PeopleSpaceTile tile) {
|
|
||||||
try {
|
|
||||||
if (tile.getUserName() != null) {
|
|
||||||
tileView.setName(tile.getUserName().toString());
|
|
||||||
}
|
|
||||||
tileView.setPersonIcon(getPersonIconBitmap(mContext, tile,
|
|
||||||
getSizeInDp(mContext, R.dimen.avatar_size_for_medium,
|
|
||||||
mContext.getResources().getDisplayMetrics().density)));
|
|
||||||
|
|
||||||
PeopleTileKey key = new PeopleTileKey(tile);
|
|
||||||
tileView.setOnClickListener(v -> storeWidgetConfiguration(tile, key));
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Couldn't retrieve shortcut information", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stores the user selected configuration for {@code mAppWidgetId}. */
|
|
||||||
private void storeWidgetConfiguration(PeopleSpaceTile tile, PeopleTileKey key) {
|
|
||||||
if (PeopleSpaceUtils.DEBUG) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Put " + tile.getUserName() + "'s shortcut ID: "
|
|
||||||
+ tile.getId() + " for widget ID: "
|
|
||||||
+ mAppWidgetId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mPeopleSpaceWidgetManager.addNewWidget(mAppWidgetId, key);
|
|
||||||
finishActivity();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Finish activity with a successful widget configuration result. */
|
/** Finish activity with a successful widget configuration result. */
|
||||||
@@ -169,19 +77,13 @@ public class PeopleSpaceActivity extends Activity {
|
|||||||
/** Finish activity without choosing a widget. */
|
/** Finish activity without choosing a widget. */
|
||||||
public void dismissActivity(View v) {
|
public void dismissActivity(View v) {
|
||||||
if (DEBUG) Log.d(TAG, "Activity dismissed with no widgets added!");
|
if (DEBUG) Log.d(TAG, "Activity dismissed with no widgets added!");
|
||||||
|
setResult(RESULT_CANCELED);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setActivityResult(int result) {
|
private void setActivityResult(int result) {
|
||||||
Intent resultValue = new Intent();
|
Intent resultValue = new Intent();
|
||||||
resultValue.putExtra(EXTRA_APPWIDGET_ID, mAppWidgetId);
|
resultValue.putExtra(EXTRA_APPWIDGET_ID, mViewModel.getAppWidgetId().getValue());
|
||||||
setResult(result, resultValue);
|
setResult(result, resultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
// Refresh tile views to sync new conversations.
|
|
||||||
buildActivity();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import android.graphics.Rect;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.util.IconDrawableFactory;
|
import android.util.IconDrawableFactory;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.ContextThemeWrapper;
|
||||||
|
|
||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||||
|
|
||||||
@@ -52,16 +53,15 @@ class PeopleStoryIconFactory implements AutoCloseable {
|
|||||||
|
|
||||||
PeopleStoryIconFactory(Context context, PackageManager pm,
|
PeopleStoryIconFactory(Context context, PackageManager pm,
|
||||||
IconDrawableFactory iconDrawableFactory, int iconSizeDp) {
|
IconDrawableFactory iconDrawableFactory, int iconSizeDp) {
|
||||||
context.setTheme(android.R.style.Theme_DeviceDefault_DayNight);
|
mContext = new ContextThemeWrapper(context, android.R.style.Theme_DeviceDefault_DayNight);
|
||||||
mIconBitmapSize = (int) (iconSizeDp * context.getResources().getDisplayMetrics().density);
|
mIconBitmapSize = (int) (iconSizeDp * mContext.getResources().getDisplayMetrics().density);
|
||||||
mDensity = context.getResources().getDisplayMetrics().density;
|
mDensity = mContext.getResources().getDisplayMetrics().density;
|
||||||
mIconSize = mDensity * iconSizeDp;
|
mIconSize = mDensity * iconSizeDp;
|
||||||
mPackageManager = pm;
|
mPackageManager = pm;
|
||||||
mIconDrawableFactory = iconDrawableFactory;
|
mIconDrawableFactory = iconDrawableFactory;
|
||||||
mImportantConversationColor = context.getColor(R.color.important_conversation);
|
mImportantConversationColor = mContext.getColor(R.color.important_conversation);
|
||||||
mAccentColor = Utils.getColorAttr(context,
|
mAccentColor = Utils.getColorAttr(mContext,
|
||||||
com.android.internal.R.attr.colorAccentPrimaryVariant).getDefaultColor();
|
com.android.internal.R.attr.colorAccentPrimaryVariant).getDefaultColor();
|
||||||
mContext = context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import androidx.core.math.MathUtils;
|
|||||||
|
|
||||||
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
|
import com.android.systemui.people.data.model.PeopleTileModel;
|
||||||
import com.android.systemui.people.widget.LaunchConversationActivity;
|
import com.android.systemui.people.widget.LaunchConversationActivity;
|
||||||
import com.android.systemui.people.widget.PeopleSpaceWidgetProvider;
|
import com.android.systemui.people.widget.PeopleSpaceWidgetProvider;
|
||||||
import com.android.systemui.people.widget.PeopleTileKey;
|
import com.android.systemui.people.widget.PeopleTileKey;
|
||||||
@@ -299,7 +300,8 @@ public class PeopleTileViewHelper {
|
|||||||
return createLastInteractionRemoteViews();
|
return createLastInteractionRemoteViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isDndBlockingTileData(@Nullable PeopleSpaceTile tile) {
|
/** Whether the conversation associated with {@code tile} can bypass DND. */
|
||||||
|
public static boolean isDndBlockingTileData(@Nullable PeopleSpaceTile tile) {
|
||||||
if (tile == null) return false;
|
if (tile == null) return false;
|
||||||
|
|
||||||
int notificationPolicyState = tile.getNotificationPolicyState();
|
int notificationPolicyState = tile.getNotificationPolicyState();
|
||||||
@@ -536,7 +538,8 @@ public class PeopleTileViewHelper {
|
|||||||
return views;
|
return views;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean getHasNewStory(PeopleSpaceTile tile) {
|
/** Whether {@code tile} has a new story. */
|
||||||
|
public static boolean getHasNewStory(PeopleSpaceTile tile) {
|
||||||
return tile.getStatuses() != null && tile.getStatuses().stream().anyMatch(
|
return tile.getStatuses() != null && tile.getStatuses().stream().anyMatch(
|
||||||
c -> c.getActivity() == ACTIVITY_NEW_STORY);
|
c -> c.getActivity() == ACTIVITY_NEW_STORY);
|
||||||
}
|
}
|
||||||
@@ -1250,16 +1253,24 @@ public class PeopleTileViewHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a bitmap with the user icon and package icon. */
|
/** Returns a bitmap with the user icon and package icon. */
|
||||||
public static Bitmap getPersonIconBitmap(Context context, PeopleSpaceTile tile,
|
public static Bitmap getPersonIconBitmap(Context context, PeopleTileModel tile,
|
||||||
int maxAvatarSize) {
|
int maxAvatarSize) {
|
||||||
boolean hasNewStory = getHasNewStory(tile);
|
return getPersonIconBitmap(context, maxAvatarSize, tile.getHasNewStory(),
|
||||||
return getPersonIconBitmap(context, tile, maxAvatarSize, hasNewStory);
|
tile.getUserIcon(), tile.getKey().getPackageName(), tile.getKey().getUserId(),
|
||||||
|
tile.isImportant(), tile.isDndBlocking());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a bitmap with the user icon and package icon. */
|
/** Returns a bitmap with the user icon and package icon. */
|
||||||
private static Bitmap getPersonIconBitmap(
|
private static Bitmap getPersonIconBitmap(
|
||||||
Context context, PeopleSpaceTile tile, int maxAvatarSize, boolean hasNewStory) {
|
Context context, PeopleSpaceTile tile, int maxAvatarSize, boolean hasNewStory) {
|
||||||
Icon icon = tile.getUserIcon();
|
return getPersonIconBitmap(context, maxAvatarSize, hasNewStory, tile.getUserIcon(),
|
||||||
|
tile.getPackageName(), getUserId(tile),
|
||||||
|
tile.isImportantConversation(), isDndBlockingTileData(tile));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bitmap getPersonIconBitmap(
|
||||||
|
Context context, int maxAvatarSize, boolean hasNewStory, Icon icon, String packageName,
|
||||||
|
int userId, boolean importantConversation, boolean dndBlockingTileData) {
|
||||||
if (icon == null) {
|
if (icon == null) {
|
||||||
Drawable placeholder = context.getDrawable(R.drawable.ic_avatar_with_badge).mutate();
|
Drawable placeholder = context.getDrawable(R.drawable.ic_avatar_with_badge).mutate();
|
||||||
placeholder.setColorFilter(getDisabledColorFilter());
|
placeholder.setColorFilter(getDisabledColorFilter());
|
||||||
@@ -1272,10 +1283,10 @@ public class PeopleTileViewHelper {
|
|||||||
RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
|
RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
|
||||||
context.getResources(), icon.getBitmap());
|
context.getResources(), icon.getBitmap());
|
||||||
Drawable personDrawable = storyIcon.getPeopleTileDrawable(roundedDrawable,
|
Drawable personDrawable = storyIcon.getPeopleTileDrawable(roundedDrawable,
|
||||||
tile.getPackageName(), getUserId(tile), tile.isImportantConversation(),
|
packageName, userId, importantConversation,
|
||||||
hasNewStory);
|
hasNewStory);
|
||||||
|
|
||||||
if (isDndBlockingTileData(tile)) {
|
if (dndBlockingTileData) {
|
||||||
personDrawable.setColorFilter(getDisabledColorFilter());
|
personDrawable.setColorFilter(getDisabledColorFilter());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.people.data.model
|
||||||
|
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import com.android.systemui.people.widget.PeopleTileKey
|
||||||
|
|
||||||
|
/** Models a tile/conversation. */
|
||||||
|
data class PeopleTileModel(
|
||||||
|
val key: PeopleTileKey,
|
||||||
|
val username: String,
|
||||||
|
val userIcon: Icon,
|
||||||
|
val hasNewStory: Boolean,
|
||||||
|
val isImportant: Boolean,
|
||||||
|
val isDndBlocking: Boolean,
|
||||||
|
)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.people.data.repository
|
||||||
|
|
||||||
|
import android.app.people.PeopleSpaceTile
|
||||||
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
|
import com.android.systemui.people.PeopleTileViewHelper
|
||||||
|
import com.android.systemui.people.data.model.PeopleTileModel
|
||||||
|
import com.android.systemui.people.widget.PeopleSpaceWidgetManager
|
||||||
|
import com.android.systemui.people.widget.PeopleTileKey
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/** A Repository to fetch the current tiles/conversations. */
|
||||||
|
// TODO(b/238993727): Make the tiles API reactive.
|
||||||
|
interface PeopleTileRepository {
|
||||||
|
/* The current priority tiles. */
|
||||||
|
fun priorityTiles(): List<PeopleTileModel>
|
||||||
|
|
||||||
|
/* The current recent tiles. */
|
||||||
|
fun recentTiles(): List<PeopleTileModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
@SysUISingleton
|
||||||
|
class PeopleTileRepositoryImpl
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
|
||||||
|
) : PeopleTileRepository {
|
||||||
|
override fun priorityTiles(): List<PeopleTileModel> {
|
||||||
|
return peopleSpaceWidgetManager.priorityTiles.map { it.toModel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recentTiles(): List<PeopleTileModel> {
|
||||||
|
return peopleSpaceWidgetManager.recentTiles.map { it.toModel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PeopleSpaceTile.toModel(): PeopleTileModel {
|
||||||
|
return PeopleTileModel(
|
||||||
|
PeopleTileKey(this),
|
||||||
|
userName.toString(),
|
||||||
|
userIcon,
|
||||||
|
PeopleTileViewHelper.getHasNewStory(this),
|
||||||
|
isImportantConversation,
|
||||||
|
PeopleTileViewHelper.isDndBlockingTileData(this),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.people.data.repository
|
||||||
|
|
||||||
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
|
import com.android.systemui.people.widget.PeopleSpaceWidgetManager
|
||||||
|
import com.android.systemui.people.widget.PeopleTileKey
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface PeopleWidgetRepository {
|
||||||
|
/**
|
||||||
|
* Bind the widget with ID [widgetId] to the tile keyed by [tileKey].
|
||||||
|
*
|
||||||
|
* If there is already a widget with [widgetId], this existing widget will be reconfigured and
|
||||||
|
* associated to this tile. If there is no widget with [widgetId], a new one will be created.
|
||||||
|
*/
|
||||||
|
fun setWidgetTile(widgetId: Int, tileKey: PeopleTileKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SysUISingleton
|
||||||
|
class PeopleWidgetRepositoryImpl
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
|
||||||
|
) : PeopleWidgetRepository {
|
||||||
|
override fun setWidgetTile(widgetId: Int, tileKey: PeopleTileKey) {
|
||||||
|
peopleSpaceWidgetManager.addNewWidget(widgetId, tileKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
* 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.people.ui.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewOutlineProvider
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.Lifecycle.State.CREATED
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.people.PeopleSpaceTileView
|
||||||
|
import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
|
||||||
|
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/** A ViewBinder for [PeopleViewModel]. */
|
||||||
|
object PeopleViewBinder {
|
||||||
|
private const val TAG = "PeopleSpaceViewBinder"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [ViewOutlineProvider] used to clip the corner radius of the recent and priority lists.
|
||||||
|
*/
|
||||||
|
private val ViewOutlineProvider =
|
||||||
|
object : ViewOutlineProvider() {
|
||||||
|
override fun getOutline(view: View, outline: Outline) {
|
||||||
|
outline.setRoundRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
view.width,
|
||||||
|
view.height,
|
||||||
|
view.context.resources.getDimension(R.dimen.people_space_widget_radius),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a [View] that can later be [bound][bind] to a [PeopleViewModel]. */
|
||||||
|
@JvmStatic
|
||||||
|
fun create(context: Context): ViewGroup {
|
||||||
|
return LayoutInflater.from(context)
|
||||||
|
.inflate(R.layout.people_space_activity, /* root= */ null) as ViewGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bind [view] to [viewModel]. */
|
||||||
|
@JvmStatic
|
||||||
|
fun bind(
|
||||||
|
view: ViewGroup,
|
||||||
|
viewModel: PeopleViewModel,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
onFinish: () -> Unit,
|
||||||
|
) {
|
||||||
|
// Call [onFinish] this activity when the ViewModel tells us so.
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
lifecycleOwner.repeatOnLifecycle(CREATED) {
|
||||||
|
viewModel.isFinished.collect { isFinished ->
|
||||||
|
if (isFinished) {
|
||||||
|
viewModel.clearIsFinished()
|
||||||
|
onFinish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start collecting the UI data once the Activity is STARTED.
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
combine(
|
||||||
|
viewModel.priorityTiles,
|
||||||
|
viewModel.recentTiles,
|
||||||
|
) { priority, recent ->
|
||||||
|
priority to recent
|
||||||
|
}
|
||||||
|
.collect { (priorityTiles, recentTiles) ->
|
||||||
|
if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
|
||||||
|
setConversationsContent(
|
||||||
|
view,
|
||||||
|
priorityTiles,
|
||||||
|
recentTiles,
|
||||||
|
viewModel::onTileClicked,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setNoConversationsContent(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure to refresh the tiles/conversations when the Activity is resumed, so that it
|
||||||
|
// updates them when going back to the Activity after leaving it.
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
viewModel.onTileRefreshRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setNoConversationsContent(view: ViewGroup) {
|
||||||
|
// This should never happen.
|
||||||
|
if (view.childCount > 1) {
|
||||||
|
error("view has ${view.childCount} children, it should have maximum 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The static content for no conversations is already shown.
|
||||||
|
if (view.findViewById<View>(R.id.top_level_no_conversations) != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were showing the content with conversations earlier, remove it.
|
||||||
|
if (view.childCount == 1) {
|
||||||
|
view.removeViewAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = view.context
|
||||||
|
val noConversationsView =
|
||||||
|
LayoutInflater.from(context)
|
||||||
|
.inflate(R.layout.people_space_activity_no_conversations, /* root= */ view)
|
||||||
|
|
||||||
|
// The Tile preview has colorBackground as its background. Change it so it's different than
|
||||||
|
// the activity's background.
|
||||||
|
val item = noConversationsView.findViewById<LinearLayout>(android.R.id.background)
|
||||||
|
val shape = item.background as GradientDrawable
|
||||||
|
val ta =
|
||||||
|
context.theme.obtainStyledAttributes(
|
||||||
|
intArrayOf(com.android.internal.R.attr.colorSurface)
|
||||||
|
)
|
||||||
|
shape.setColor(ta.getColor(0, Color.WHITE))
|
||||||
|
ta.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setConversationsContent(
|
||||||
|
view: ViewGroup,
|
||||||
|
priorityTiles: List<PeopleTileViewModel>,
|
||||||
|
recentTiles: List<PeopleTileViewModel>,
|
||||||
|
onTileClicked: (PeopleTileViewModel) -> Unit,
|
||||||
|
) {
|
||||||
|
// This should never happen.
|
||||||
|
if (view.childCount > 1) {
|
||||||
|
error("view has ${view.childCount} children, it should have maximum 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inflate the content with conversations, if it's not already.
|
||||||
|
if (view.findViewById<View>(R.id.top_level_with_conversations) == null) {
|
||||||
|
// If we were showing the content without conversations earlier, remove it.
|
||||||
|
if (view.childCount == 1) {
|
||||||
|
view.removeViewAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutInflater.from(view.context)
|
||||||
|
.inflate(R.layout.people_space_activity_with_conversations, /* root= */ view)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(b/193782241): Replace the NestedScrollView + 2x LinearLayout from this layout into a
|
||||||
|
// single RecyclerView once this screen is tested by screenshot tests. Introduce a
|
||||||
|
// PeopleSpaceTileViewBinder that will properly create and bind the View associated to a
|
||||||
|
// PeopleSpaceTileViewModel (and remove the PeopleSpaceTileView class).
|
||||||
|
val conversationsView = view.requireViewById<View>(R.id.top_level_with_conversations)
|
||||||
|
setTileViews(
|
||||||
|
conversationsView,
|
||||||
|
R.id.priority,
|
||||||
|
R.id.priority_tiles,
|
||||||
|
priorityTiles,
|
||||||
|
onTileClicked,
|
||||||
|
)
|
||||||
|
|
||||||
|
setTileViews(
|
||||||
|
conversationsView,
|
||||||
|
R.id.recent,
|
||||||
|
R.id.recent_tiles,
|
||||||
|
recentTiles,
|
||||||
|
onTileClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets a [PeopleSpaceTileView]s for each conversation. */
|
||||||
|
private fun setTileViews(
|
||||||
|
root: View,
|
||||||
|
tilesListId: Int,
|
||||||
|
tilesId: Int,
|
||||||
|
tiles: List<PeopleTileViewModel>,
|
||||||
|
onTileClicked: (PeopleTileViewModel) -> Unit,
|
||||||
|
) {
|
||||||
|
// Remove any previously added tile.
|
||||||
|
// TODO(b/193782241): Once this list is a big RecyclerView, set the current list and use
|
||||||
|
// DiffUtil to do as less addView/removeView as possible.
|
||||||
|
val layout = root.requireViewById<ViewGroup>(tilesId)
|
||||||
|
layout.removeAllViews()
|
||||||
|
layout.outlineProvider = ViewOutlineProvider
|
||||||
|
|
||||||
|
val tilesListView = root.requireViewById<LinearLayout>(tilesListId)
|
||||||
|
if (tiles.isEmpty()) {
|
||||||
|
tilesListView.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tilesListView.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
// Add each tile.
|
||||||
|
tiles.forEachIndexed { i, tile ->
|
||||||
|
val tileView =
|
||||||
|
PeopleSpaceTileView(root.context, layout, tile.key.shortcutId, i == tiles.size - 1)
|
||||||
|
bindTileView(tileView, tile, onTileClicked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets [tileView] with the data in [conversation]. */
|
||||||
|
private fun bindTileView(
|
||||||
|
tileView: PeopleSpaceTileView,
|
||||||
|
tile: PeopleTileViewModel,
|
||||||
|
onTileClicked: (PeopleTileViewModel) -> Unit,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
tileView.setName(tile.username)
|
||||||
|
tileView.setPersonIcon(tile.icon)
|
||||||
|
tileView.setOnClickListener { onTileClicked(tile) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Couldn't retrieve shortcut information", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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.people.ui.viewmodel
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import com.android.systemui.people.widget.PeopleTileKey
|
||||||
|
|
||||||
|
/** Models UI state for a single tile/conversation. */
|
||||||
|
data class PeopleTileViewModel(
|
||||||
|
val key: PeopleTileKey,
|
||||||
|
val icon: Bitmap,
|
||||||
|
val username: String?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* 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.people.ui.viewmodel
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.dagger.qualifiers.Application
|
||||||
|
import com.android.systemui.people.PeopleSpaceUtils
|
||||||
|
import com.android.systemui.people.PeopleTileViewHelper
|
||||||
|
import com.android.systemui.people.data.model.PeopleTileModel
|
||||||
|
import com.android.systemui.people.data.repository.PeopleTileRepository
|
||||||
|
import com.android.systemui.people.data.repository.PeopleWidgetRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models UI state for the people space, allowing the user to select which conversation should be
|
||||||
|
* associated to a new or existing Conversation widget.
|
||||||
|
*/
|
||||||
|
class PeopleViewModel(
|
||||||
|
@Application private val context: Context,
|
||||||
|
private val tileRepository: PeopleTileRepository,
|
||||||
|
private val widgetRepository: PeopleWidgetRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
/**
|
||||||
|
* The list of the priority tiles/conversations.
|
||||||
|
*
|
||||||
|
* Important: Even though this is a Flow, the underlying API used to populate this Flow is not
|
||||||
|
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
|
||||||
|
*/
|
||||||
|
private val _priorityTiles = MutableStateFlow(priorityTiles())
|
||||||
|
val priorityTiles: Flow<List<PeopleTileViewModel>> = _priorityTiles
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of the priority tiles/conversations.
|
||||||
|
*
|
||||||
|
* Important: Even though this is a Flow, the underlying API used to populate this Flow is not
|
||||||
|
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
|
||||||
|
*/
|
||||||
|
private val _recentTiles = MutableStateFlow(recentTiles())
|
||||||
|
val recentTiles: Flow<List<PeopleTileViewModel>> = _recentTiles
|
||||||
|
|
||||||
|
/** The ID of the widget currently being edited/added. */
|
||||||
|
private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
|
||||||
|
val appWidgetId: StateFlow<Int> = _appWidgetId
|
||||||
|
|
||||||
|
/** Whether the user journey is complete. */
|
||||||
|
private val _isFinished = MutableStateFlow(false)
|
||||||
|
val isFinished: StateFlow<Boolean> = _isFinished
|
||||||
|
|
||||||
|
/** Refresh the [priorityTiles] and [recentTiles]. */
|
||||||
|
fun onTileRefreshRequested() {
|
||||||
|
_priorityTiles.value = priorityTiles()
|
||||||
|
_recentTiles.value = recentTiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the [appWidgetId] should be changed to [widgetId]. */
|
||||||
|
fun onWidgetIdChanged(widgetId: Int) {
|
||||||
|
_appWidgetId.value = widgetId
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear [isFinished], setting it to false. */
|
||||||
|
fun clearIsFinished() {
|
||||||
|
_isFinished.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when a tile is clicked. */
|
||||||
|
fun onTileClicked(tile: PeopleTileViewModel) {
|
||||||
|
if (PeopleSpaceUtils.DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Put ${tile.username}'s shortcut ID: ${tile.key.shortcutId} for widget ID: " +
|
||||||
|
_appWidgetId.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
widgetRepository.setWidgetTile(_appWidgetId.value, tile.key)
|
||||||
|
_isFinished.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun priorityTiles(): List<PeopleTileViewModel> {
|
||||||
|
return try {
|
||||||
|
tileRepository.priorityTiles().map { it.toViewModel() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Couldn't retrieve priority conversations", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recentTiles(): List<PeopleTileViewModel> {
|
||||||
|
return try {
|
||||||
|
tileRepository.recentTiles().map { it.toViewModel() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Couldn't retrieve recent conversations", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PeopleTileModel.toViewModel(): PeopleTileViewModel {
|
||||||
|
val icon =
|
||||||
|
PeopleTileViewHelper.getPersonIconBitmap(
|
||||||
|
context,
|
||||||
|
this,
|
||||||
|
PeopleTileViewHelper.getSizeInDp(
|
||||||
|
context,
|
||||||
|
R.dimen.avatar_size_for_medium,
|
||||||
|
context.resources.displayMetrics.density,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return PeopleTileViewModel(key, icon, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Factory that should be used to create a [PeopleViewModel]. */
|
||||||
|
class Factory
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@Application private val context: Context,
|
||||||
|
private val tileRepository: PeopleTileRepository,
|
||||||
|
private val widgetRepository: PeopleWidgetRepository,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
check(modelClass == PeopleViewModel::class.java)
|
||||||
|
return PeopleViewModel(context, tileRepository, widgetRepository) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PeopleSpaceViewModel"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user