diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index fa87de2b1353c..ffd6b522e3943 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -110,6 +110,7 @@ android_library {
"androidx.arch.core_core-runtime",
"androidx.lifecycle_lifecycle-common-java8",
"androidx.lifecycle_lifecycle-extensions",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.dynamicanimation_dynamicanimation",
"androidx-constraintlayout_constraintlayout",
"androidx.exifinterface_exifinterface",
@@ -218,6 +219,7 @@ android_library {
"androidx.arch.core_core-runtime",
"androidx.lifecycle_lifecycle-common-java8",
"androidx.lifecycle_lifecycle-extensions",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.dynamicanimation_dynamicanimation",
"androidx-constraintlayout_constraintlayout",
"androidx.exifinterface_exifinterface",
diff --git a/packages/SystemUI/res/layout/people_space_activity.xml b/packages/SystemUI/res/layout/people_space_activity.xml
index 7102375a89bf3..f45cc7c464d51 100644
--- a/packages/SystemUI/res/layout/people_space_activity.xml
+++ b/packages/SystemUI/res/layout/people_space_activity.xml
@@ -13,103 +13,11 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+ android:layout_height="match_parent">
+
+
diff --git a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml b/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
index 2e9ff07caed90..e929169cfe3d9 100644
--- a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
+++ b/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
@@ -16,7 +16,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/people_space_tile_view.xml b/packages/SystemUI/res/layout/people_space_tile_view.xml
index 2a2c35dde8418..b0599caae6df8 100644
--- a/packages/SystemUI/res/layout/people_space_tile_view.xml
+++ b/packages/SystemUI/res/layout/people_space_tile_view.xml
@@ -37,8 +37,8 @@
+ android:layout_width="@dimen/avatar_size_for_medium"
+ android:layout_height="@dimen/avatar_size_for_medium" />
priorityTiles = new ArrayList<>();
- List recentTiles = new ArrayList<>();
- try {
- priorityTiles = mPeopleSpaceWidgetManager.getPriorityTiles();
- recentTiles = mPeopleSpaceWidgetManager.getRecentTiles();
- } catch (Exception e) {
- Log.e(TAG, "Couldn't retrieve conversations", e);
- }
+ // Update the widget ID coming from the intent.
+ int widgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
+ mViewModel.onWidgetIdChanged(widgetId);
- // If no conversations, render activity without conversations
- if (recentTiles.isEmpty() && priorityTiles.isEmpty()) {
- setContentView(R.layout.people_space_activity_no_conversations);
-
- // The Tile preview has colorBackground as its background. Change it so it's different
- // than the activity's background.
- LinearLayout item = findViewById(android.R.id.background);
- 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 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();
+ ViewGroup view = PeopleViewBinder.create(this);
+ PeopleViewBinder.bind(view, mViewModel, /* lifecycleOwner= */ this,
+ () -> {
+ finishActivity();
+ return null;
+ });
+ setContentView(view);
}
/** Finish activity with a successful widget configuration result. */
@@ -169,19 +77,13 @@ public class PeopleSpaceActivity extends Activity {
/** Finish activity without choosing a widget. */
public void dismissActivity(View v) {
if (DEBUG) Log.d(TAG, "Activity dismissed with no widgets added!");
+ setResult(RESULT_CANCELED);
finish();
}
private void setActivityResult(int result) {
Intent resultValue = new Intent();
- resultValue.putExtra(EXTRA_APPWIDGET_ID, mAppWidgetId);
+ resultValue.putExtra(EXTRA_APPWIDGET_ID, mViewModel.getAppWidgetId().getValue());
setResult(result, resultValue);
}
-
- @Override
- protected void onResume() {
- super.onResume();
- // Refresh tile views to sync new conversations.
- buildActivity();
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java b/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java
index 4ee951f3cdb1f..58e700f813882 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java
@@ -28,6 +28,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.IconDrawableFactory;
import android.util.Log;
+import android.view.ContextThemeWrapper;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
@@ -52,16 +53,15 @@ class PeopleStoryIconFactory implements AutoCloseable {
PeopleStoryIconFactory(Context context, PackageManager pm,
IconDrawableFactory iconDrawableFactory, int iconSizeDp) {
- context.setTheme(android.R.style.Theme_DeviceDefault_DayNight);
- mIconBitmapSize = (int) (iconSizeDp * context.getResources().getDisplayMetrics().density);
- mDensity = context.getResources().getDisplayMetrics().density;
+ mContext = new ContextThemeWrapper(context, android.R.style.Theme_DeviceDefault_DayNight);
+ mIconBitmapSize = (int) (iconSizeDp * mContext.getResources().getDisplayMetrics().density);
+ mDensity = mContext.getResources().getDisplayMetrics().density;
mIconSize = mDensity * iconSizeDp;
mPackageManager = pm;
mIconDrawableFactory = iconDrawableFactory;
- mImportantConversationColor = context.getColor(R.color.important_conversation);
- mAccentColor = Utils.getColorAttr(context,
+ mImportantConversationColor = mContext.getColor(R.color.important_conversation);
+ mAccentColor = Utils.getColorAttr(mContext,
com.android.internal.R.attr.colorAccentPrimaryVariant).getDefaultColor();
- mContext = context;
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java
index 00aa1381ace11..be82b1faac8ef 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java
@@ -75,6 +75,7 @@ import androidx.core.math.MathUtils;
import com.android.internal.annotations.VisibleForTesting;
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.PeopleSpaceWidgetProvider;
import com.android.systemui.people.widget.PeopleTileKey;
@@ -299,7 +300,8 @@ public class PeopleTileViewHelper {
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;
int notificationPolicyState = tile.getNotificationPolicyState();
@@ -536,7 +538,8 @@ public class PeopleTileViewHelper {
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(
c -> c.getActivity() == ACTIVITY_NEW_STORY);
}
@@ -1250,16 +1253,24 @@ public class PeopleTileViewHelper {
}
/** 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) {
- boolean hasNewStory = getHasNewStory(tile);
- return getPersonIconBitmap(context, tile, maxAvatarSize, hasNewStory);
+ return getPersonIconBitmap(context, maxAvatarSize, tile.getHasNewStory(),
+ tile.getUserIcon(), tile.getKey().getPackageName(), tile.getKey().getUserId(),
+ tile.isImportant(), tile.isDndBlocking());
}
/** Returns a bitmap with the user icon and package icon. */
private static Bitmap getPersonIconBitmap(
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) {
Drawable placeholder = context.getDrawable(R.drawable.ic_avatar_with_badge).mutate();
placeholder.setColorFilter(getDisabledColorFilter());
@@ -1272,10 +1283,10 @@ public class PeopleTileViewHelper {
RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
context.getResources(), icon.getBitmap());
Drawable personDrawable = storyIcon.getPeopleTileDrawable(roundedDrawable,
- tile.getPackageName(), getUserId(tile), tile.isImportantConversation(),
+ packageName, userId, importantConversation,
hasNewStory);
- if (isDndBlockingTileData(tile)) {
+ if (dndBlockingTileData) {
personDrawable.setColorFilter(getDisabledColorFilter());
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/data/model/PeopleTileModel.kt b/packages/SystemUI/src/com/android/systemui/people/data/model/PeopleTileModel.kt
new file mode 100644
index 0000000000000..5d8539fabc6b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/data/model/PeopleTileModel.kt
@@ -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,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleTileRepository.kt b/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleTileRepository.kt
new file mode 100644
index 0000000000000..01b43d52130b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleTileRepository.kt
@@ -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
+
+ /* The current recent tiles. */
+ fun recentTiles(): List
+}
+
+@SysUISingleton
+class PeopleTileRepositoryImpl
+@Inject
+constructor(
+ private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
+) : PeopleTileRepository {
+ override fun priorityTiles(): List {
+ return peopleSpaceWidgetManager.priorityTiles.map { it.toModel() }
+ }
+
+ override fun recentTiles(): List {
+ 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),
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleWidgetRepository.kt
new file mode 100644
index 0000000000000..f2b6cb1fc3f57
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleWidgetRepository.kt
@@ -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)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
new file mode 100644
index 0000000000000..bc982cccaacdf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
@@ -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(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(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,
+ recentTiles: List,
+ 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(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(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,
+ 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(tilesId)
+ layout.removeAllViews()
+ layout.outlineProvider = ViewOutlineProvider
+
+ val tilesListView = root.requireViewById(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)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleTileViewModel.kt
new file mode 100644
index 0000000000000..40205ce9424a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleTileViewModel.kt
@@ -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?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
new file mode 100644
index 0000000000000..17de991588b8f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
@@ -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> = _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> = _recentTiles
+
+ /** The ID of the widget currently being edited/added. */
+ private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
+ val appWidgetId: StateFlow = _appWidgetId
+
+ /** Whether the user journey is complete. */
+ private val _isFinished = MutableStateFlow(false)
+ val isFinished: StateFlow = _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 {
+ return try {
+ tileRepository.priorityTiles().map { it.toViewModel() }
+ } catch (e: Exception) {
+ Log.e(TAG, "Couldn't retrieve priority conversations", e)
+ emptyList()
+ }
+ }
+
+ private fun recentTiles(): List {
+ 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 create(modelClass: Class): T {
+ check(modelClass == PeopleViewModel::class.java)
+ return PeopleViewModel(context, tileRepository, widgetRepository) as T
+ }
+ }
+
+ companion object {
+ private const val TAG = "PeopleSpaceViewModel"
+ }
+}