Create Custom Trace Settings UI for Record Issue Tile.

This is the dialog that shows when a user creates a custom trace, rather
than a preset trace type, like battery or performance tracing.

Bug: 305049544
Test: https://photos.app.goo.gl/dj3NrCBi7aTum8gj9
Flag: com.android.systemui.record_issue_qs_tile
Change-Id: I2a47a2f72c0e4103291617ceef37a8a94be4a85d
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index db71d72..ce6b133 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -571,6 +571,7 @@
         "lottie",
         "LowLightDreamLib",
         "TraceurCommon",
+        "Traceur-res",
         "//frameworks/libs/systemui:motion_tool_lib",
         "notification_flags_lib",
         "PlatformComposeCore",
@@ -747,6 +748,7 @@
         "androidx.compose.animation_animation-graphics",
         "androidx.lifecycle_lifecycle-viewmodel-compose",
         "TraceurCommon",
+        "Traceur-res",
     ],
 }
 
diff --git a/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml b/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml
new file mode 100644
index 0000000..6180bf5
--- /dev/null
+++ b/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:orientation="vertical" >
+
+    <TextView
+        android:id="@+id/categories"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+    <TextView
+        android:id="@+id/cpu_buffer_size"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+    <!-- Attach to Bugreport Switch -->
+    <LinearLayout
+        android:id="@+id/attach_to_bugreport_switch_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/attach_to_bugreport_switch_label"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_weight="1"
+            android:layout_gravity="fill_vertical"
+            android:gravity="start"
+            android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+        <Switch
+            android:id="@+id/attach_to_bugreport_switch"
+            android:layout_width="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="end"
+            android:layout_gravity="fill_vertical"
+            android:layout_weight="0" />
+    </LinearLayout>
+
+    <!-- Winscope Switch -->
+    <LinearLayout
+        android:id="@+id/winscope_switch_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/winscope_switch_label"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_weight="1"
+            android:layout_gravity="fill_vertical"
+            android:gravity="start"
+            android:textAppearance="@style/TextAppearance.Dialog.Body.Message"/>
+
+        <Switch
+            android:id="@+id/winscope_switch"
+            android:layout_width="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="end"
+            android:layout_gravity="fill_vertical"
+            android:layout_weight="0" />
+    </LinearLayout>
+
+    <!-- Trace Debuggable Apps Switch -->
+    <LinearLayout
+        android:id="@+id/trace_debuggable_apps_switch_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/debuggable_apps_switch_label"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_weight="1"
+            android:layout_gravity="fill_vertical"
+            android:gravity="start"
+            android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+        <Switch
+            android:id="@+id/trace_debuggable_apps_switch"
+            android:layout_width="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="end"
+            android:layout_gravity="fill_vertical"
+            android:layout_weight="0" />
+    </LinearLayout>
+
+    <!-- Long Traces Switch -->
+    <LinearLayout
+        android:id="@+id/long_traces_switch_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/long_traces_switch_label"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_weight="1"
+            android:layout_gravity="fill_vertical"
+            android:gravity="start"
+            android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+        <Switch
+            android:id="@+id/long_traces_switch"
+            android:layout_width="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="end"
+            android:layout_gravity="fill_vertical"
+            android:layout_weight="0" />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/long_trace_size"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+    <TextView
+        android:id="@+id/long_trace_duration"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+</LinearLayout>
+
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9f1f1a0..e3be8eb 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -946,6 +946,10 @@
     <string name="performance">Performance</string>
     <string name="user_interface">User Interface</string>
     <string name="thermal">Thermal</string>
+    <string name="custom">Custom</string>
+
+    <string name="custom_trace_settings_dialog_title">Custom Trace Settings</string>
+    <string name="restore_default">Restore Default</string>
 
     <!-- QuickSettings: Label for the toggle that controls whether One-handed mode is enabled. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_onehanded_label">One-handed mode</string>
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceSettingsDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceSettingsDialogDelegate.kt
new file mode 100644
index 0000000..3a66639
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceSettingsDialogDelegate.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2024 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.recordissue
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.widget.Switch
+import android.widget.TextView
+import com.android.systemui.recordissue.IssueRecordingState.Companion.TAG_TITLE_DELIMITER
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.traceur.PresetTraceConfigs
+import com.android.traceur.TraceConfig
+import com.android.traceur.res.R as T
+
+class CustomTraceSettingsDialogDelegate(
+    private val factory: SystemUIDialog.Factory,
+    private val state: IssueRecordingState,
+    private val onSave: Runnable,
+) : SystemUIDialog.Delegate {
+
+    private val builder = TraceConfig.Builder(PresetTraceConfigs.getDefaultConfig())
+
+    override fun createDialog(): SystemUIDialog = factory.create(this)
+
+    override fun beforeCreate(dialog: SystemUIDialog?, savedInstanceState: Bundle?) {
+        super.beforeCreate(dialog, savedInstanceState)
+
+        dialog?.apply {
+            setTitle(R.string.custom_trace_settings_dialog_title)
+            setView(
+                LayoutInflater.from(context).inflate(R.layout.custom_trace_settings_dialog, null)
+            )
+            setPositiveButton(R.string.save) { _, _ -> onSave.run() }
+            setNegativeButton(R.string.cancel) { _, _ -> }
+        }
+    }
+
+    @SuppressLint("SetTextI18n")
+    override fun onCreate(dialog: SystemUIDialog?, savedInstanceState: Bundle?) {
+        super.onCreate(dialog, savedInstanceState)
+
+        dialog?.apply {
+            requireViewById<TextView>(R.id.categories).apply {
+                text =
+                    context.getString(T.string.categories) +
+                        "\n" +
+                        context.getString(R.string.notification_alert_title)
+                setOnClickListener { showCategorySelector(this) }
+            }
+            requireViewById<Switch>(R.id.attach_to_bugreport_switch).apply {
+                isChecked = builder.attachToBugreport
+            }
+            requireViewById<TextView>(R.id.cpu_buffer_size)
+                .setupSingleChoiceText(
+                    T.array.buffer_size_values,
+                    T.array.buffer_size_names,
+                    builder.bufferSizeKb,
+                    T.string.buffer_size,
+                )
+            val longTraceSizeText: TextView =
+                requireViewById<TextView>(R.id.long_trace_size)
+                    .setupSingleChoiceText(
+                        T.array.long_trace_size_values,
+                        T.array.long_trace_size_names,
+                        builder.maxLongTraceSizeMb,
+                        T.string.max_long_trace_size,
+                    )
+            val longTraceDurationText: TextView =
+                requireViewById<TextView>(R.id.long_trace_duration)
+                    .setupSingleChoiceText(
+                        T.array.long_trace_duration_values,
+                        T.array.long_trace_duration_names,
+                        builder.maxLongTraceDurationMinutes,
+                        T.string.max_long_trace_duration,
+                    )
+            requireViewById<Switch>(R.id.long_traces_switch).apply {
+                isChecked = builder.longTrace
+                val disabledAlpha by lazy { getDisabledAlpha(context) }
+                val alpha = if (isChecked) 1f else disabledAlpha
+                longTraceDurationText.alpha = alpha
+                longTraceSizeText.alpha = alpha
+
+                setOnCheckedChangeListener { _, isChecked ->
+                    longTraceDurationText.isEnabled = isChecked
+                    longTraceSizeText.isEnabled = isChecked
+
+                    val newAlpha = if (isChecked) 1f else disabledAlpha
+                    longTraceDurationText.alpha = newAlpha
+                    longTraceSizeText.alpha = newAlpha
+                }
+            }
+            requireViewById<Switch>(R.id.winscope_switch).apply { isChecked = builder.winscope }
+            requireViewById<Switch>(R.id.trace_debuggable_apps_switch).apply {
+                isChecked = builder.apps
+            }
+            requireViewById<TextView>(R.id.long_traces_switch_label).text =
+                context.getString(T.string.long_traces)
+            requireViewById<TextView>(R.id.debuggable_apps_switch_label).text =
+                context.getString(T.string.trace_debuggable_applications)
+            requireViewById<TextView>(R.id.winscope_switch_label).text =
+                context.getString(T.string.winscope_tracing)
+            requireViewById<TextView>(R.id.attach_to_bugreport_switch_label).text =
+                context.getString(T.string.attach_to_bug_report)
+        }
+    }
+
+    @SuppressLint("SetTextI18n")
+    private fun showCategorySelector(root: TextView) {
+        showDialog(root.context) {
+            val titlesToCheckmarks =
+                state.tagTitles.associateBy(
+                    { it },
+                    { builder.tags.contains(it.substringBefore(TAG_TITLE_DELIMITER)) }
+                )
+            val titles = titlesToCheckmarks.keys.toTypedArray()
+            val checkmarks = titlesToCheckmarks.values.toBooleanArray()
+            val checkedTitleSuffixes =
+                titlesToCheckmarks.entries
+                    .filter { it.value }
+                    .map { it.key.substringAfter(TAG_TITLE_DELIMITER) }
+                    .toMutableSet()
+
+            val newTags = builder.tags.toMutableSet()
+            setMultiChoiceItems(titles, checkmarks) { _, i, isChecked ->
+                val tag = titles[i].substringBefore(TAG_TITLE_DELIMITER)
+                val titleSuffix = titles[i].substringAfter(TAG_TITLE_DELIMITER)
+                if (isChecked) {
+                    newTags.add(tag)
+                    checkedTitleSuffixes.add(titleSuffix)
+                } else {
+                    newTags.remove(tag)
+                    checkedTitleSuffixes.remove(titleSuffix)
+                }
+            }
+            setPositiveButton(R.string.save) { _, _ ->
+                root.text =
+                    root.context.resources.getString(T.string.categories) +
+                        "\n" +
+                        checkedTitleSuffixes.fold("") { acc, s -> "$acc, $s" }.substringAfter(", ")
+            }
+            setNeutralButton(R.string.restore_default) { _, _ ->
+                root.text =
+                    context.getString(T.string.categories) +
+                        "\n" +
+                        context.getString(R.string.notification_alert_title)
+            }
+            setNegativeButton(R.string.cancel) { _, _ -> }
+        }
+    }
+
+    @SuppressLint("SetTextI18n")
+    private fun TextView.setupSingleChoiceText(
+        resValues: Int,
+        resNames: Int,
+        startingValue: Int,
+        alertTitleRes: Int,
+    ): TextView {
+        val values = resources.getStringArray(resValues).map { Integer.parseInt(it) }
+        val names = resources.getStringArray(resNames)
+        val startingIndex = values.indexOf(startingValue)
+        text = resources.getString(alertTitleRes) + "\n${names[startingIndex]}"
+
+        setOnClickListener {
+            showDialog(context) {
+                setTitle(alertTitleRes)
+                setSingleChoiceItems(names, startingIndex) { d, i ->
+                    text = resources.getString(alertTitleRes) + "\n${names[i]}"
+                    d.dismiss()
+                }
+            }
+        }
+        return this
+    }
+
+    private fun showDialog(context: Context, onBuilder: AlertDialog.Builder.() -> Unit) =
+        AlertDialog.Builder(context, R.style.Theme_SystemUI_Dialog_Alert)
+            .apply { onBuilder() }
+            .create()
+            .also { SystemUIDialog.applyFlags(it) }
+            .show()
+
+    private fun getDisabledAlpha(context: Context): Float {
+        val ta = context.obtainStyledAttributes(intArrayOf(android.R.attr.disabledAlpha))
+        val alpha = ta.getFloat(0, 0f)
+        ta.recycle()
+        return alpha
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
index 7612900..86c3567 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
@@ -57,6 +57,11 @@
     val traceConfig: TraceConfig
         get() = ALL_ISSUE_TYPES[issueTypeRes] ?: PresetTraceConfigs.getDefaultConfig()
 
+    // The 1st part of the title before the ": " is the tag, and the 2nd part is the description
+    var tagTitles: Set<String>
+        get() = prefs.getStringSet(KEY_TAG_TITLES, emptySet()) ?: emptySet()
+        set(value) = prefs.edit().putStringSet(KEY_TAG_TITLES, value).apply()
+
     private val listeners = CopyOnWriteArrayList<Runnable>()
 
     var isRecording = false
@@ -81,8 +86,10 @@
         private const val KEY_TAKE_BUG_REPORT = "key_takeBugReport"
         private const val HAS_APPROVED_SCREEN_RECORDING = "HasApprovedScreenRecord"
         private const val KEY_RECORD_SCREEN = "key_recordScreen"
+        private const val KEY_TAG_TITLES = "key_tagTitles"
         const val KEY_ISSUE_TYPE_RES = "key_issueTypeRes"
         const val ISSUE_TYPE_NOT_SET = -1
+        const val TAG_TITLE_DELIMITER = ": "
 
         val ALL_ISSUE_TYPES: Map<Int, TraceConfig?> =
             hashMapOf(
@@ -90,6 +97,7 @@
                 Pair(R.string.user_interface, PresetTraceConfigs.getUiConfig()),
                 Pair(R.string.battery, PresetTraceConfigs.getBatteryConfig()),
                 Pair(R.string.thermal, PresetTraceConfigs.getThermalConfig()),
+                Pair(R.string.custom, null),
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index 8a51ad4..e13445e 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -88,7 +88,7 @@
             setPositiveButton(R.string.qs_record_issue_start) { _, _ -> onStarted.run() }
         }
         bgExecutor.execute {
-            traceurMessageSender.onBoundToTraceur.add { traceurMessageSender.getTags() }
+            traceurMessageSender.onBoundToTraceur.add { traceurMessageSender.getTags(state) }
             traceurMessageSender.bindToTraceur(dialog.context)
         }
     }
@@ -170,18 +170,8 @@
     @MainThread
     private fun onIssueTypeClicked(context: Context, onIssueTypeSelected: Runnable) {
         val popupMenu = PopupMenu(context, issueTypeButton)
-
-        ALL_ISSUE_TYPES.keys.forEach {
-            popupMenu.menu.add(it).apply {
-                setIcon(R.drawable.arrow_pointing_down)
-                if (it != state.issueTypeRes) {
-                    iconTintList = ColorStateList.valueOf(Color.TRANSPARENT)
-                }
-                intent = Intent().putExtra(KEY_ISSUE_TYPE_RES, it)
-            }
-        }
-        popupMenu.apply {
-            setOnMenuItemClickListener {
+        val onMenuItemClickListener =
+            PopupMenu.OnMenuItemClickListener {
                 issueTypeButton.text = it.title
                 state.issueTypeRes =
                     it.intent?.getIntExtra(KEY_ISSUE_TYPE_RES, ISSUE_TYPE_NOT_SET)
@@ -189,6 +179,28 @@
                 onIssueTypeSelected.run()
                 true
             }
+        ALL_ISSUE_TYPES.keys.forEach {
+            popupMenu.menu.add(it).apply {
+                setIcon(R.drawable.arrow_pointing_down)
+                if (it != state.issueTypeRes) {
+                    iconTintList = ColorStateList.valueOf(Color.TRANSPARENT)
+                }
+                intent = Intent().putExtra(KEY_ISSUE_TYPE_RES, it)
+
+                if (it == R.string.custom) {
+                    setOnMenuItemClickListener {
+                        CustomTraceSettingsDialogDelegate(factory, state) {
+                                onMenuItemClickListener.onMenuItemClick(it)
+                            }
+                            .createDialog()
+                            .show()
+                        true
+                    }
+                }
+            }
+        }
+        popupMenu.apply {
+            setOnMenuItemClickListener(onMenuItemClickListener)
             setForceShowIcon(true)
             show()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
index a31a9ef..8bfd14a 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
@@ -33,6 +33,7 @@
 import androidx.annotation.WorkerThread
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.recordissue.IssueRecordingState.Companion.TAG_TITLE_DELIMITER
 import com.android.traceur.FileSender
 import com.android.traceur.MessageConstants
 import com.android.traceur.TraceConfig
@@ -112,8 +113,8 @@
     }
 
     @WorkerThread
-    fun getTags() {
-        val replyHandler = Messenger(TagsHandler(backgroundLooper))
+    fun getTags(state: IssueRecordingState) {
+        val replyHandler = Messenger(TagsHandler(backgroundLooper, state))
         notifyTraceur(MessageConstants.TAGS_WHAT, replyTo = replyHandler)
     }
 
@@ -165,7 +166,8 @@
         }
     }
 
-    private class TagsHandler(looper: Looper) : Handler(looper) {
+    private class TagsHandler(looper: Looper, private val state: IssueRecordingState) :
+        Handler(looper) {
 
         override fun handleMessage(msg: Message) {
             if (MessageConstants.TAGS_WHAT == msg.what) {
@@ -174,16 +176,11 @@
                     msg.data.getStringArrayList(MessageConstants.BUNDLE_KEY_TAG_DESCRIPTIONS)
                 if (keys == null || values == null) {
                     throw IllegalArgumentException(
-                        "Neither keys: $keys, nor values: $values can " + "be null"
+                        "Neither keys: $keys, nor values: $values can be null"
                     )
                 }
-
-                val tags = keys.zip(values).map { "${it.first}: ${it.second}" }.toSet()
-                Log.e(
-                    TAG,
-                    "These tags: $tags will be saved and used for the Custom Trace" +
-                        " Config dialog in a future CL. This log will be removed."
-                )
+                state.tagTitles =
+                    keys.zip(values).map { it.first + TAG_TITLE_DELIMITER + it.second }.toSet()
             } else {
                 throw IllegalArgumentException("received unknown msg.what: " + msg.what)
             }