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