Add error logging and better crash safety for app pairs

This causes app pairs to log an error message if creation or icon inflation is attempted with contents != 2. App pairs with wrong numbers of members should not be created anymore.

Fixes: 317283244
Fixes: 322892793
Test: Manual
Flag: ACONFIG com.android.wm.shell.enable_app_pairs TEAMFOOD
Change-Id: I5e454042719bb67187f900c546380bb44cfc34ac
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 0f3c029..839320e 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -27,6 +27,7 @@
 import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.content.Intent;
+import android.util.Log;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -58,6 +59,8 @@
  * ratio.
  */
 public class AppPairsController {
+    private static final String TAG = "AppPairsController";
+
     // Used for encoding and decoding the "rank" attribute
     private static final int BITMASK_SIZE = 16;
     private static final int BITMASK_FOR_SNAP_POSITION = (1 << BITMASK_SIZE) - 1;
@@ -89,13 +92,23 @@
 
         @PersistentSnapPosition int snapPosition = gtv.getSnapPosition();
         if (!isPersistentSnapPosition(snapPosition)) {
-            throw new RuntimeException("tried to save an app pair with illegal snapPosition");
+            // if we received an illegal snap position, log an error and do not create the app pair.
+            Log.wtf(TAG, "tried to save an app pair with illegal snapPosition " + snapPosition);
+            return;
         }
 
         app1.rank = encodeRank(SPLIT_POSITION_TOP_OR_LEFT, snapPosition);
         app2.rank = encodeRank(SPLIT_POSITION_BOTTOM_OR_RIGHT, snapPosition);
         FolderInfo newAppPair = FolderInfo.createAppPair(app1, app2);
 
+        if (newAppPair.contents.size() != 2) {
+            // if app pair doesn't have exactly 2 members, log an error and do not create the app
+            // pair.
+            Log.wtf(TAG,
+                    "tried to save an app pair with " + newAppPair.contents.size() + " members");
+            return;
+        }
+
         IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
         MODEL_EXECUTOR.execute(() -> {
             newAppPair.contents.forEach(member -> {
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
index 1d73441..9b85a65 100644
--- a/src/com/android/launcher3/apppairs/AppPairIcon.java
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -45,6 +46,8 @@
  * member apps are set into these rectangles.
  */
 public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable {
+    private static final String TAG = "AppPairIcon";
+
     // A view that holds the app pair icon graphic.
     private AppPairIconGraphic mIconGraphic;
     // A view that holds the app pair's title.
@@ -96,8 +99,7 @@
         icon.mAppPairName.setText(appPairInfo.title);
 
         // Set up accessibility
-        icon.setContentDescription(icon.getAccessibilityTitle(
-                appPairInfo.contents.get(0).title, appPairInfo.contents.get(1).title));
+        icon.setContentDescription(icon.getAccessibilityTitle(appPairInfo));
         icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
 
         return icon;
@@ -106,7 +108,14 @@
     /**
      * Returns a formatted accessibility title for app pairs.
      */
-    public String getAccessibilityTitle(CharSequence app1, CharSequence app2) {
+    public String getAccessibilityTitle(FolderInfo appPairInfo) {
+        if (appPairInfo.contents.size() != 2) {
+            Log.wtf(TAG, "AppPair contents not 2, size: " + appPairInfo.contents.size());
+            return "";
+        }
+
+        CharSequence app1 = appPairInfo.contents.get(0).title;
+        CharSequence app2 = appPairInfo.contents.get(1).title;
         return getContext().getString(R.string.app_pair_name_format, app1, app2);
     }
 
diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
index b2497a3..6458eb0 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -93,7 +93,7 @@
     private fun applyIcons(contents: ArrayList<WorkspaceItemInfo>) {
         // App pair should always contain 2 members; if not 2, return to avoid a crash loop
         if (contents.size != 2) {
-            Log.w(TAG, "AppPair contents not 2, size: " + contents.size, Throwable())
+            Log.wtf(TAG, "AppPair contents not 2, size: " + contents.size, Throwable())
             return
         }
 
@@ -112,7 +112,6 @@
         appIcon2?.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt())
     }
 
-
     /** Gets this icon graphic's bounds, with respect to the parent icon's coordinate system. */
     fun getIconBounds(outBounds: Rect) {
         outBounds.set(0, 0, backgroundSize.toInt(), backgroundSize.toInt())