1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.traceur; 18 19 import android.annotation.Nullable; 20 import android.app.AlertDialog; 21 import android.content.ActivityNotFoundException; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.content.SharedPreferences; 28 import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 29 import android.content.pm.PackageManager; 30 import android.icu.text.MessageFormat; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.view.LayoutInflater; 34 import android.view.Menu; 35 import android.view.MenuInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.Toast; 39 import androidx.preference.ListPreference; 40 import androidx.preference.MultiSelectListPreference; 41 import androidx.preference.Preference; 42 import androidx.preference.PreferenceFragment; 43 import androidx.preference.PreferenceManager; 44 import androidx.preference.SwitchPreference; 45 46 import com.android.settingslib.HelpUtils; 47 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.HashMap; 51 import java.util.HashSet; 52 import java.util.Locale; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Map.Entry; 56 import java.util.Set; 57 import java.util.TreeMap; 58 59 public class MainFragment extends PreferenceFragment { 60 61 static final String TAG = TraceUtils.TAG; 62 63 public static final String ACTION_REFRESH_TAGS = "com.android.traceur.REFRESH_TAGS"; 64 65 private static final String BETTERBUG_PACKAGE_NAME = 66 "com.google.android.apps.internal.betterbug"; 67 68 private static final String ROOT_MIME_TYPE = "vnd.android.document/root"; 69 private static final String STORAGE_URI = "content://com.android.traceur.documents/root"; 70 71 private SwitchPreference mTracingOn; 72 private SwitchPreference mStackSamplingOn; 73 private SwitchPreference mHeapDumpOn; 74 75 private AlertDialog mAlertDialog; 76 private SharedPreferences mPrefs; 77 78 private MultiSelectListPreference mTags; 79 private MultiSelectListPreference mHeapDumpProcesses; 80 81 private boolean mRefreshing; 82 83 private BroadcastReceiver mRefreshReceiver; 84 85 OnSharedPreferenceChangeListener mSharedPreferenceChangeListener = 86 new OnSharedPreferenceChangeListener () { 87 public void onSharedPreferenceChanged( 88 SharedPreferences sharedPreferences, String key) { 89 refreshUi(); 90 } 91 }; 92 93 @Override onCreate(@ullable Bundle savedInstanceState)94 public void onCreate(@Nullable Bundle savedInstanceState) { 95 super.onCreate(savedInstanceState); 96 97 Receiver.updateDeveloperOptionsWatcher(getContext(), /* fromBootIntent */ false); 98 99 mPrefs = PreferenceManager.getDefaultSharedPreferences( 100 getActivity().getApplicationContext()); 101 102 mTracingOn = (SwitchPreference) findPreference( 103 getActivity().getString(R.string.pref_key_tracing_on)); 104 mStackSamplingOn = (SwitchPreference) findPreference( 105 getActivity().getString(R.string.pref_key_stack_sampling_on)); 106 mHeapDumpOn = (SwitchPreference) findPreference( 107 getActivity().getString(R.string.pref_key_heap_dump_on)); 108 109 mTracingOn.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 110 @Override 111 public boolean onPreferenceClick(Preference preference) { 112 Receiver.updateTracing(getContext()); 113 // Disable the stack sampling and heap dump toggles if the trace toggle is enabled. 114 mStackSamplingOn.setEnabled(!((SwitchPreference) preference).isChecked()); 115 mHeapDumpOn.setEnabled(!((SwitchPreference) preference).isChecked()); 116 return true; 117 } 118 }); 119 120 mStackSamplingOn.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 121 @Override 122 public boolean onPreferenceClick(Preference preference) { 123 Receiver.updateTracing(getContext()); 124 // Disable the trace and heap dump toggles if the stack sampling toggle is enabled. 125 mTracingOn.setEnabled(!((SwitchPreference) preference).isChecked()); 126 mHeapDumpOn.setEnabled(!((SwitchPreference) preference).isChecked()); 127 return true; 128 } 129 }); 130 131 mHeapDumpOn.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 132 @Override 133 public boolean onPreferenceClick(Preference preference) { 134 Receiver.updateTracing(getContext()); 135 // Disable the trace and stack sampling toggles if the heap dump toggle is enabled. 136 mTracingOn.setEnabled(!((SwitchPreference) preference).isChecked()); 137 mStackSamplingOn.setEnabled(!((SwitchPreference) preference).isChecked()); 138 return true; 139 } 140 }); 141 142 mHeapDumpProcesses = (MultiSelectListPreference) findPreference( 143 getContext().getString(R.string.pref_key_heap_dump_processes)); 144 145 mTags = (MultiSelectListPreference) findPreference(getContext().getString(R.string.pref_key_tags)); 146 mTags.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { 147 @Override 148 public boolean onPreferenceChange(Preference preference, Object newValue) { 149 if (mRefreshing) { 150 return true; 151 } 152 Set<String> set = (Set<String>) newValue; 153 TreeMap<String, String> available = TraceUtils.listCategories(); 154 ArrayList<String> clean = new ArrayList<>(set.size()); 155 156 for (String s : set) { 157 if (available.containsKey(s)) { 158 clean.add(s); 159 } 160 } 161 set.clear(); 162 set.addAll(clean); 163 return true; 164 } 165 }); 166 167 findPreference("restore_default_tags").setOnPreferenceClickListener( 168 new Preference.OnPreferenceClickListener() { 169 @Override 170 public boolean onPreferenceClick(Preference preference) { 171 refreshUi(/* restoreDefaultTags =*/ true, 172 /* clearHeapDumpProcesses =*/ false); 173 Toast.makeText(getContext(), 174 getContext().getString(R.string.default_categories_restored), 175 Toast.LENGTH_SHORT).show(); 176 return true; 177 } 178 }); 179 180 findPreference("clear_heap_dump_processes").setOnPreferenceClickListener( 181 new Preference.OnPreferenceClickListener() { 182 @Override 183 public boolean onPreferenceClick(Preference preference) { 184 refreshUi(/* restoreDefaultTags =*/ false, 185 /* clearHeapDumpProcesses =*/ true); 186 Toast.makeText(getContext(), 187 getContext().getString(R.string.clear_heap_dump_processes_toast), 188 Toast.LENGTH_SHORT).show(); 189 return true; 190 } 191 }); 192 193 findPreference(getString(R.string.pref_key_tracing_quick_setting)) 194 .setOnPreferenceClickListener( 195 new Preference.OnPreferenceClickListener() { 196 @Override 197 public boolean onPreferenceClick(Preference preference) { 198 Receiver.updateTracingQuickSettings(getContext()); 199 return true; 200 } 201 }); 202 203 findPreference(getString(R.string.pref_key_stack_sampling_quick_setting)) 204 .setOnPreferenceClickListener( 205 new Preference.OnPreferenceClickListener() { 206 @Override 207 public boolean onPreferenceClick(Preference preference) { 208 Receiver.updateStackSamplingQuickSettings(getContext()); 209 return true; 210 } 211 }); 212 213 findPreference("clear_saved_files").setOnPreferenceClickListener( 214 new Preference.OnPreferenceClickListener() { 215 @Override 216 public boolean onPreferenceClick(Preference preference) { 217 new AlertDialog.Builder(getContext()) 218 .setTitle(R.string.clear_saved_files_question) 219 .setMessage(R.string.all_recordings_will_be_deleted) 220 .setPositiveButton(R.string.clear, 221 new DialogInterface.OnClickListener() { 222 public void onClick(DialogInterface dialog, int which) { 223 TraceUtils.clearSavedTraces(); 224 } 225 }) 226 .setNegativeButton(android.R.string.cancel, 227 new DialogInterface.OnClickListener() { 228 public void onClick(DialogInterface dialog, int which) { 229 dialog.dismiss(); 230 } 231 }) 232 .create() 233 .show(); 234 return true; 235 } 236 }); 237 238 findPreference("trace_link_button") 239 .setOnPreferenceClickListener( 240 new Preference.OnPreferenceClickListener() { 241 @Override 242 public boolean onPreferenceClick(Preference preference) { 243 Intent intent = buildTraceFileViewIntent(); 244 try { 245 startActivity(intent); 246 } catch (ActivityNotFoundException e) { 247 return false; 248 } 249 return true; 250 } 251 }); 252 253 // This disables "Attach to bugreports" when long traces are enabled. This cannot be done in 254 // main.xml because there are some other settings there that are enabled with long traces. 255 SwitchPreference attachToBugreport = findPreference( 256 getString(R.string.pref_key_attach_to_bugreport)); 257 findPreference(getString(R.string.pref_key_long_traces)) 258 .setOnPreferenceClickListener( 259 new Preference.OnPreferenceClickListener() { 260 @Override 261 public boolean onPreferenceClick(Preference preference) { 262 if (((SwitchPreference) preference).isChecked()) { 263 attachToBugreport.setEnabled(false); 264 } else { 265 attachToBugreport.setEnabled(true); 266 } 267 return true; 268 } 269 }); 270 271 refreshUi(); 272 273 mRefreshReceiver = new BroadcastReceiver() { 274 @Override 275 public void onReceive(Context context, Intent intent) { 276 refreshUi(); 277 } 278 }; 279 280 } 281 282 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)283 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 284 setHasOptionsMenu(true); 285 return super.onCreateView(inflater, container, savedInstanceState); 286 } 287 288 @Override onStart()289 public void onStart() { 290 super.onStart(); 291 getPreferenceScreen().getSharedPreferences() 292 .registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); 293 getActivity().registerReceiver(mRefreshReceiver, new IntentFilter(ACTION_REFRESH_TAGS), 294 Context.RECEIVER_NOT_EXPORTED); 295 TraceUtils.cleanupOlderFiles(); 296 Receiver.updateTracing(getContext()); 297 } 298 299 @Override onStop()300 public void onStop() { 301 getPreferenceScreen().getSharedPreferences() 302 .unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); 303 getActivity().unregisterReceiver(mRefreshReceiver); 304 305 if (mAlertDialog != null) { 306 mAlertDialog.cancel(); 307 mAlertDialog = null; 308 } 309 310 super.onStop(); 311 } 312 313 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)314 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 315 addPreferencesFromResource(R.xml.main); 316 } 317 318 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)319 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 320 HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_url, 321 this.getClass().getName()); 322 } 323 buildTraceFileViewIntent()324 private Intent buildTraceFileViewIntent() { 325 Intent intent = new Intent(Intent.ACTION_VIEW); 326 intent.setDataAndType(Uri.parse(STORAGE_URI), ROOT_MIME_TYPE); 327 return intent; 328 } 329 refreshUi()330 private void refreshUi() { 331 refreshUi(/* restoreDefaultTags =*/ false, /* clearHeapDumpProcesses =*/ false); 332 } 333 334 /* 335 * Refresh the preferences UI to make sure it reflects the current state of the preferences and 336 * system. 337 */ refreshUi(boolean restoreDefaultTags, boolean clearHeapDumpProcesses)338 private void refreshUi(boolean restoreDefaultTags, boolean clearHeapDumpProcesses) { 339 Context context = getContext(); 340 341 // Make sure the Record trace, Record CPU profile, and Record heap dump toggles match their 342 // preference values. 343 mTracingOn.setChecked(mPrefs.getBoolean(mTracingOn.getKey(), false)); 344 mStackSamplingOn.setChecked(mPrefs.getBoolean(mStackSamplingOn.getKey(), false)); 345 mHeapDumpOn.setChecked(mPrefs.getBoolean(mHeapDumpOn.getKey(), false)); 346 347 SwitchPreference stopOnReport = 348 (SwitchPreference) findPreference(getString(R.string.pref_key_stop_on_bugreport)); 349 stopOnReport.setChecked(mPrefs.getBoolean(stopOnReport.getKey(), false)); 350 351 SwitchPreference continuousHeapDump = (SwitchPreference) findPreference( 352 getString(R.string.pref_key_continuous_heap_dump)); 353 continuousHeapDump.setChecked(mPrefs.getBoolean(continuousHeapDump.getKey(), false)); 354 355 // Update category list to match the categories available on the system. 356 Set<Entry<String, String>> availableTags = TraceUtils.listCategories().entrySet(); 357 ArrayList<String> entries = new ArrayList<String>(availableTags.size()); 358 ArrayList<String> values = new ArrayList<String>(availableTags.size()); 359 for (Entry<String, String> entry : availableTags) { 360 entries.add(entry.getKey() + ": " + entry.getValue()); 361 values.add(entry.getKey()); 362 } 363 364 // We keep selected processes in the list in case a user is interested in a process that AM 365 // is not yet aware of (e.g. an app that hasn't started up). 366 Set<String> runningProcesses = TraceUtils.getRunningAppProcesses(context); 367 Set<String> selectedProcesses = mHeapDumpProcesses.getValues(); 368 runningProcesses.addAll(selectedProcesses); 369 370 List<String> sortedProcesses = new ArrayList<>(runningProcesses); 371 Collections.sort(sortedProcesses); 372 373 mRefreshing = true; 374 try { 375 mTags.setEntries(entries.toArray(new String[0])); 376 mTags.setEntryValues(values.toArray(new String[0])); 377 if (restoreDefaultTags || !mPrefs.contains(context.getString(R.string.pref_key_tags))) { 378 mTags.setValues(PresetTraceConfigs.getDefaultConfig().getTags()); 379 } 380 mHeapDumpProcesses.setEntries(sortedProcesses.toArray(new String[0])); 381 mHeapDumpProcesses.setEntryValues(sortedProcesses.toArray(new String[0])); 382 if (clearHeapDumpProcesses || 383 !mPrefs.contains(context.getString(R.string.pref_key_heap_dump_processes))) { 384 mHeapDumpProcesses.setValues(new HashSet<String>()); 385 } 386 } finally { 387 mRefreshing = false; 388 } 389 390 // Enable or disable each toggle based on the state of the others. This path exists in case 391 // the tracing state was updated with the QS tile or the ongoing-trace notification, which 392 // would not call the toggles' OnClickListeners. 393 mTracingOn.setEnabled(!(mStackSamplingOn.isChecked() || mHeapDumpOn.isChecked())); 394 mStackSamplingOn.setEnabled(!(mTracingOn.isChecked() || mHeapDumpOn.isChecked())); 395 396 // Disallow heap dumps if no process is selected, or if tracing/stack sampling is active. 397 boolean heapDumpProcessSelected = mHeapDumpProcesses.getValues().size() > 0; 398 mHeapDumpOn.setEnabled(heapDumpProcessSelected && 399 !(mTracingOn.isChecked() || mStackSamplingOn.isChecked())); 400 mHeapDumpOn.setSummary(heapDumpProcessSelected 401 ? context.getString(R.string.record_heap_dump_summary_enabled) 402 : context.getString(R.string.record_heap_dump_summary_disabled)); 403 404 // Update subtitles on this screen. 405 Set<String> categories = mTags.getValues(); 406 MessageFormat msgFormat = new MessageFormat( 407 getResources().getString(R.string.num_categories_selected), 408 Locale.getDefault()); 409 Map<String, Object> arguments = new HashMap<>(); 410 arguments.put("count", categories.size()); 411 mTags.setSummary(PresetTraceConfigs.getDefaultConfig().getTags().equals(categories) 412 ? context.getString(R.string.default_categories) 413 : msgFormat.format(arguments)); 414 415 ListPreference bufferSize = (ListPreference)findPreference( 416 context.getString(R.string.pref_key_buffer_size)); 417 bufferSize.setSummary(bufferSize.getEntry()); 418 419 ListPreference maxLongTraceSize = (ListPreference)findPreference( 420 context.getString(R.string.pref_key_max_long_trace_size)); 421 maxLongTraceSize.setSummary(maxLongTraceSize.getEntry()); 422 423 ListPreference maxLongTraceDuration = (ListPreference)findPreference( 424 context.getString(R.string.pref_key_max_long_trace_duration)); 425 maxLongTraceDuration.setSummary(maxLongTraceDuration.getEntry()); 426 427 ListPreference continuousHeapDumpInterval = (ListPreference)findPreference( 428 context.getString(R.string.pref_key_continuous_heap_dump_interval)); 429 continuousHeapDumpInterval.setSummary(continuousHeapDumpInterval.getEntry()); 430 431 // Check if BetterBug is installed to see if Traceur should display either the toggle for 432 // 'attach_to_bugreport' or 'stop_on_bugreport'. 433 try { 434 context.getPackageManager().getPackageInfo(BETTERBUG_PACKAGE_NAME, 435 PackageManager.MATCH_SYSTEM_ONLY); 436 findPreference(getString(R.string.pref_key_attach_to_bugreport)).setVisible(true); 437 findPreference(getString(R.string.pref_key_stop_on_bugreport)).setVisible(false); 438 // Changes the long traces summary to add that they cannot be attached to bugreports. 439 findPreference(getString(R.string.pref_key_long_traces)) 440 .setSummary(getString(R.string.long_traces_summary_betterbug)); 441 } catch (PackageManager.NameNotFoundException e) { 442 // attach_to_bugreport must be disabled here because it's true by default. 443 mPrefs.edit().putBoolean( 444 getString(R.string.pref_key_attach_to_bugreport), false).commit(); 445 findPreference(getString(R.string.pref_key_attach_to_bugreport)).setVisible(false); 446 findPreference(getString(R.string.pref_key_stop_on_bugreport)).setVisible(true); 447 // Sets long traces summary to the default in case Betterbug was removed. 448 findPreference(getString(R.string.pref_key_long_traces)) 449 .setSummary(getString(R.string.long_traces_summary)); 450 } 451 452 // Check if an activity exists to handle the trace_link_button intent. If not, hide the UI 453 // element 454 PackageManager packageManager = context.getPackageManager(); 455 Intent intent = buildTraceFileViewIntent(); 456 if (intent.resolveActivity(packageManager) == null) { 457 findPreference("trace_link_button").setVisible(false); 458 } 459 } 460 } 461