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