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)
}