/* * Copyright (C) 2015 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.traceur; import android.annotation.Nullable; import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.PackageManager; import android.icu.text.MessageFormat; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.preference.ListPreference; import androidx.preference.MultiSelectListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragment; import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreference; import com.android.settingslib.HelpUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; public class MainFragment extends PreferenceFragment { static final String TAG = TraceUtils.TAG; public static final String ACTION_REFRESH_TAGS = "com.android.traceur.REFRESH_TAGS"; private static final String BETTERBUG_PACKAGE_NAME = "com.google.android.apps.internal.betterbug"; private static final String ROOT_MIME_TYPE = "vnd.android.document/root"; private static final String STORAGE_URI = "content://com.android.traceur.documents/root"; private SwitchPreference mTracingOn; private SwitchPreference mStackSamplingOn; private SwitchPreference mHeapDumpOn; private AlertDialog mAlertDialog; private SharedPreferences mPrefs; private MultiSelectListPreference mTags; private MultiSelectListPreference mHeapDumpProcesses; private boolean mRefreshing; private BroadcastReceiver mRefreshReceiver; OnSharedPreferenceChangeListener mSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener () { public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) { refreshUi(); } }; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Receiver.updateDeveloperOptionsWatcher(getContext(), /* fromBootIntent */ false); mPrefs = PreferenceManager.getDefaultSharedPreferences( getActivity().getApplicationContext()); mTracingOn = (SwitchPreference) findPreference( getActivity().getString(R.string.pref_key_tracing_on)); mStackSamplingOn = (SwitchPreference) findPreference( getActivity().getString(R.string.pref_key_stack_sampling_on)); mHeapDumpOn = (SwitchPreference) findPreference( getActivity().getString(R.string.pref_key_heap_dump_on)); mTracingOn.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Receiver.updateTracing(getContext()); // Disable the stack sampling and heap dump toggles if the trace toggle is enabled. mStackSamplingOn.setEnabled(!((SwitchPreference) preference).isChecked()); mHeapDumpOn.setEnabled(!((SwitchPreference) preference).isChecked()); return true; } }); mStackSamplingOn.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Receiver.updateTracing(getContext()); // Disable the trace and heap dump toggles if the stack sampling toggle is enabled. mTracingOn.setEnabled(!((SwitchPreference) preference).isChecked()); mHeapDumpOn.setEnabled(!((SwitchPreference) preference).isChecked()); return true; } }); mHeapDumpOn.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Receiver.updateTracing(getContext()); // Disable the trace and stack sampling toggles if the heap dump toggle is enabled. mTracingOn.setEnabled(!((SwitchPreference) preference).isChecked()); mStackSamplingOn.setEnabled(!((SwitchPreference) preference).isChecked()); return true; } }); mHeapDumpProcesses = (MultiSelectListPreference) findPreference( getContext().getString(R.string.pref_key_heap_dump_processes)); mTags = (MultiSelectListPreference) findPreference(getContext().getString(R.string.pref_key_tags)); mTags.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { if (mRefreshing) { return true; } Set set = (Set) newValue; TreeMap available = TraceUtils.listCategories(); ArrayList clean = new ArrayList<>(set.size()); for (String s : set) { if (available.containsKey(s)) { clean.add(s); } } set.clear(); set.addAll(clean); return true; } }); findPreference("restore_default_tags").setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { refreshUi(/* restoreDefaultTags =*/ true, /* clearHeapDumpProcesses =*/ false); Toast.makeText(getContext(), getContext().getString(R.string.default_categories_restored), Toast.LENGTH_SHORT).show(); return true; } }); findPreference("clear_heap_dump_processes").setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { refreshUi(/* restoreDefaultTags =*/ false, /* clearHeapDumpProcesses =*/ true); Toast.makeText(getContext(), getContext().getString(R.string.clear_heap_dump_processes_toast), Toast.LENGTH_SHORT).show(); return true; } }); findPreference(getString(R.string.pref_key_tracing_quick_setting)) .setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Receiver.updateTracingQuickSettings(getContext()); return true; } }); findPreference(getString(R.string.pref_key_stack_sampling_quick_setting)) .setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Receiver.updateStackSamplingQuickSettings(getContext()); return true; } }); findPreference("clear_saved_files").setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { new AlertDialog.Builder(getContext()) .setTitle(R.string.clear_saved_files_question) .setMessage(R.string.all_recordings_will_be_deleted) .setPositiveButton(R.string.clear, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { TraceUtils.clearSavedTraces(); } }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .create() .show(); return true; } }); findPreference("trace_link_button") .setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Intent intent = buildTraceFileViewIntent(); try { startActivity(intent); } catch (ActivityNotFoundException e) { return false; } return true; } }); // This disables "Attach to bugreports" when long traces are enabled. This cannot be done in // main.xml because there are some other settings there that are enabled with long traces. SwitchPreference attachToBugreport = findPreference( getString(R.string.pref_key_attach_to_bugreport)); findPreference(getString(R.string.pref_key_long_traces)) .setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { if (((SwitchPreference) preference).isChecked()) { attachToBugreport.setEnabled(false); } else { attachToBugreport.setEnabled(true); } return true; } }); refreshUi(); mRefreshReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { refreshUi(); } }; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { setHasOptionsMenu(true); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onStart() { super.onStart(); getPreferenceScreen().getSharedPreferences() .registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); getActivity().registerReceiver(mRefreshReceiver, new IntentFilter(ACTION_REFRESH_TAGS), Context.RECEIVER_NOT_EXPORTED); TraceUtils.cleanupOlderFiles(); Receiver.updateTracing(getContext()); } @Override public void onStop() { getPreferenceScreen().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); getActivity().unregisterReceiver(mRefreshReceiver); if (mAlertDialog != null) { mAlertDialog.cancel(); mAlertDialog = null; } super.onStop(); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.main); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_url, this.getClass().getName()); } private Intent buildTraceFileViewIntent() { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse(STORAGE_URI), ROOT_MIME_TYPE); return intent; } private void refreshUi() { refreshUi(/* restoreDefaultTags =*/ false, /* clearHeapDumpProcesses =*/ false); } /* * Refresh the preferences UI to make sure it reflects the current state of the preferences and * system. */ private void refreshUi(boolean restoreDefaultTags, boolean clearHeapDumpProcesses) { Context context = getContext(); // Make sure the Record trace, Record CPU profile, and Record heap dump toggles match their // preference values. mTracingOn.setChecked(mPrefs.getBoolean(mTracingOn.getKey(), false)); mStackSamplingOn.setChecked(mPrefs.getBoolean(mStackSamplingOn.getKey(), false)); mHeapDumpOn.setChecked(mPrefs.getBoolean(mHeapDumpOn.getKey(), false)); SwitchPreference stopOnReport = (SwitchPreference) findPreference(getString(R.string.pref_key_stop_on_bugreport)); stopOnReport.setChecked(mPrefs.getBoolean(stopOnReport.getKey(), false)); SwitchPreference continuousHeapDump = (SwitchPreference) findPreference( getString(R.string.pref_key_continuous_heap_dump)); continuousHeapDump.setChecked(mPrefs.getBoolean(continuousHeapDump.getKey(), false)); // Update category list to match the categories available on the system. Set> availableTags = TraceUtils.listCategories().entrySet(); ArrayList entries = new ArrayList(availableTags.size()); ArrayList values = new ArrayList(availableTags.size()); for (Entry entry : availableTags) { entries.add(entry.getKey() + ": " + entry.getValue()); values.add(entry.getKey()); } // We keep selected processes in the list in case a user is interested in a process that AM // is not yet aware of (e.g. an app that hasn't started up). Set runningProcesses = TraceUtils.getRunningAppProcesses(context); Set selectedProcesses = mHeapDumpProcesses.getValues(); runningProcesses.addAll(selectedProcesses); List sortedProcesses = new ArrayList<>(runningProcesses); Collections.sort(sortedProcesses); mRefreshing = true; try { mTags.setEntries(entries.toArray(new String[0])); mTags.setEntryValues(values.toArray(new String[0])); if (restoreDefaultTags || !mPrefs.contains(context.getString(R.string.pref_key_tags))) { mTags.setValues(PresetTraceConfigs.getDefaultTags()); } mHeapDumpProcesses.setEntries(sortedProcesses.toArray(new String[0])); mHeapDumpProcesses.setEntryValues(sortedProcesses.toArray(new String[0])); if (clearHeapDumpProcesses || !mPrefs.contains(context.getString(R.string.pref_key_heap_dump_processes))) { mHeapDumpProcesses.setValues(new HashSet()); } } finally { mRefreshing = false; } // Enable or disable each toggle based on the state of the others. This path exists in case // the tracing state was updated with the QS tile or the ongoing-trace notification, which // would not call the toggles' OnClickListeners. mTracingOn.setEnabled(!(mStackSamplingOn.isChecked() || mHeapDumpOn.isChecked())); mStackSamplingOn.setEnabled(!(mTracingOn.isChecked() || mHeapDumpOn.isChecked())); // Disallow heap dumps if no process is selected, or if tracing/stack sampling is active. boolean heapDumpProcessSelected = mHeapDumpProcesses.getValues().size() > 0; mHeapDumpOn.setEnabled(heapDumpProcessSelected && !(mTracingOn.isChecked() || mStackSamplingOn.isChecked())); mHeapDumpOn.setSummary(heapDumpProcessSelected ? context.getString(R.string.record_heap_dump_summary_enabled) : context.getString(R.string.record_heap_dump_summary_disabled)); // Update subtitles on this screen. Set categories = mTags.getValues(); MessageFormat msgFormat = new MessageFormat( getResources().getString(R.string.num_categories_selected), Locale.getDefault()); Map arguments = new HashMap<>(); arguments.put("count", categories.size()); mTags.setSummary(PresetTraceConfigs.getDefaultTags().equals(categories) ? context.getString(R.string.default_categories) : msgFormat.format(arguments)); ListPreference bufferSize = (ListPreference)findPreference( context.getString(R.string.pref_key_buffer_size)); bufferSize.setSummary(bufferSize.getEntry()); ListPreference maxLongTraceSize = (ListPreference)findPreference( context.getString(R.string.pref_key_max_long_trace_size)); maxLongTraceSize.setSummary(maxLongTraceSize.getEntry()); ListPreference maxLongTraceDuration = (ListPreference)findPreference( context.getString(R.string.pref_key_max_long_trace_duration)); maxLongTraceDuration.setSummary(maxLongTraceDuration.getEntry()); ListPreference continuousHeapDumpInterval = (ListPreference)findPreference( context.getString(R.string.pref_key_continuous_heap_dump_interval)); continuousHeapDumpInterval.setSummary(continuousHeapDumpInterval.getEntry()); // Check if BetterBug is installed to see if Traceur should display either the toggle for // 'attach_to_bugreport' or 'stop_on_bugreport'. try { context.getPackageManager().getPackageInfo(BETTERBUG_PACKAGE_NAME, PackageManager.MATCH_SYSTEM_ONLY); findPreference(getString(R.string.pref_key_attach_to_bugreport)).setVisible(true); findPreference(getString(R.string.pref_key_stop_on_bugreport)).setVisible(false); // Changes the long traces summary to add that they cannot be attached to bugreports. findPreference(getString(R.string.pref_key_long_traces)) .setSummary(getString(R.string.long_traces_summary_betterbug)); } catch (PackageManager.NameNotFoundException e) { // attach_to_bugreport must be disabled here because it's true by default. mPrefs.edit().putBoolean( getString(R.string.pref_key_attach_to_bugreport), false).commit(); findPreference(getString(R.string.pref_key_attach_to_bugreport)).setVisible(false); findPreference(getString(R.string.pref_key_stop_on_bugreport)).setVisible(true); // Sets long traces summary to the default in case Betterbug was removed. findPreference(getString(R.string.pref_key_long_traces)) .setSummary(getString(R.string.long_traces_summary)); } // Check if an activity exists to handle the trace_link_button intent. If not, hide the UI // element PackageManager packageManager = context.getPackageManager(); Intent intent = buildTraceFileViewIntent(); if (intent.resolveActivity(packageManager) == null) { findPreference("trace_link_button").setVisible(false); } } }