Make the PeopleSpaceActivity screenshot testable

This CL makes the PeopleSpaceActivity screenshot testable by extracting
a ViewModel and ViewBinder out of it. See ag/19289788 for the associated
screenshot tests.

Note that I tried to change the code inflating and updating the View as
less as possible, to avoid introducing bugs. Once this CL and the
associated screenshots are submitted, I will go ahead and refactor this
code even more. This CL is meant to be an example of the kind of
refactoring required to make a UI screenshot testable, so I tried to not
make it too big.

Bug: 238993727
Test: atest PeopleSpaceScreenshotTest
Change-Id: Ib792bd5da41c9e8bdab6cba7108a249bab10ebd2
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index fa87de2..ffd6b52 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -110,6 +110,7 @@
         "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 @@
         "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 7102375..f45cc7c 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.
   -->
-<LinearLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@+id/top_level"
+    android:id="@+id/container"
     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>
\ No newline at end of file
+    android:layout_height="match_parent">
+    <!-- The content of people_space_activity_(no|with)_conversations.xml will be added here at
+         runtime depending on the number of conversations to show. -->
+</FrameLayout>
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 2e9ff07..e929169 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 @@
 <RelativeLayout
     xmlns:android="http://schemas.android.com/apk/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_height="match_parent"
     android:padding="24dp"
diff --git a/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml b/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml
new file mode 100644
index 0000000..2384963
--- /dev/null
+++ b/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml
@@ -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>
\ 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 2a2c35d..b0599ca 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 @@
 
             <ImageView
                 android:id="@+id/tile_view_person_icon"
-                android:layout_width="52dp"
-                android:layout_height="52dp" />
+                android:layout_width="@dimen/avatar_size_for_medium"
+                android:layout_height="@dimen/avatar_size_for_medium" />
 
             <LinearLayout
                 android:orientation="horizontal"
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 137e288..d5f1f25 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -46,6 +46,7 @@
 import com.android.systemui.media.dagger.MediaProjectionModule;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationBarComponent;
+import com.android.systemui.people.PeopleModule;
 import com.android.systemui.plugins.BcSmartspaceDataPlugin;
 import com.android.systemui.privacy.PrivacyModule;
 import com.android.systemui.recents.Recents;
@@ -122,6 +123,7 @@
             LogModule.class,
             MediaProjectionModule.class,
             PeopleHubModule.class,
+            PeopleModule.class,
             PluginModule.class,
             PrivacyModule.class,
             QsFrameTranslateModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleModule.kt b/packages/SystemUI/src/com/android/systemui/people/PeopleModule.kt
new file mode 100644
index 0000000..dd35445
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleModule.kt
@@ -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
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index 93a3f81..e845aa8 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -19,144 +19,52 @@
 import static android.appwidget.AppWidgetManager.EXTRA_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.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.Outline;
-import android.graphics.drawable.GradientDrawable;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
-import android.widget.LinearLayout;
 
-import com.android.systemui.R;
-import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
-import com.android.systemui.people.widget.PeopleTileKey;
+import androidx.activity.ComponentActivity;
+import androidx.lifecycle.ViewModelProvider;
 
-import java.util.ArrayList;
-import java.util.List;
+import com.android.systemui.people.ui.view.PeopleViewBinder;
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel;
 
 import javax.inject.Inject;
 
 /** 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 boolean DEBUG = PeopleSpaceUtils.DEBUG;
 
-    private PeopleSpaceWidgetManager mPeopleSpaceWidgetManager;
-    private Context mContext;
-    private int mAppWidgetId;
+    private final PeopleViewModel.Factory mViewModelFactory;
+    private PeopleViewModel mViewModel;
 
     @Inject
-    public PeopleSpaceActivity(PeopleSpaceWidgetManager peopleSpaceWidgetManager) {
+    public PeopleSpaceActivity(PeopleViewModel.Factory viewModelFactory) {
         super();
-        mPeopleSpaceWidgetManager = peopleSpaceWidgetManager;
-
+        mViewModelFactory = viewModelFactory;
     }
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mContext = getApplicationContext();
-        mAppWidgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID,
-                INVALID_APPWIDGET_ID);
         setResult(RESULT_CANCELED);
-    }
+        mViewModel = new ViewModelProvider(this, mViewModelFactory).get(PeopleViewModel.class);
 
-    /** Builds the conversation selection activity. */
-    private void buildActivity() {
-        List<PeopleSpaceTile> priorityTiles = new ArrayList<>();
-        List<PeopleSpaceTile> 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<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();
+        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 @@
     /** 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 4ee951f..58e700f 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.drawable.Drawable;
 import android.util.IconDrawableFactory;
 import android.util.Log;
+import android.view.ContextThemeWrapper;
 
 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
 
@@ -52,16 +53,15 @@
 
     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 00aa138..be82b1f 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 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 @@
         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 @@
         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 @@
     }
 
     /** 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 @@
         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 0000000..5d8539f
--- /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 0000000..01b43d5
--- /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<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),
+        )
+    }
+}
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 0000000..f2b6cb1
--- /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 0000000..bc982cc
--- /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<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)
+        }
+    }
+}
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 0000000..40205ce
--- /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 0000000..17de991
--- /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<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"
+    }
+}