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.app.Notification;
20 import android.app.NotificationChannel;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.content.BroadcastReceiver;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.SharedPreferences;
28 import android.content.pm.PackageManager;
29 import android.database.ContentObserver;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.os.Handler;
33 import android.os.RemoteException;
34 import android.os.ServiceManager;
35 import android.os.UserManager;
36 import android.preference.PreferenceManager;
37 import android.provider.Settings;
38 import android.text.TextUtils;
39 import android.util.ArraySet;
40 import android.util.Log;
41 
42 import com.android.internal.statusbar.IStatusBarService;
43 
44 import java.util.Arrays;
45 import java.util.List;
46 import java.util.Set;
47 
48 public class Receiver extends BroadcastReceiver {
49 
50     public static final String STOP_ACTION = "com.android.traceur.STOP";
51     public static final String OPEN_ACTION = "com.android.traceur.OPEN";
52     public static final String BUGREPORT_STARTED =
53             "com.android.internal.intent.action.BUGREPORT_STARTED";
54 
55     public static final String NOTIFICATION_CHANNEL_TRACING = "trace-is-being-recorded";
56     public static final String NOTIFICATION_CHANNEL_OTHER = "system-tracing";
57 
58     private static final String TAG = "Traceur";
59 
60     private static final String BETTERBUG_PACKAGE_NAME =
61             "com.google.android.apps.internal.betterbug";
62 
63     private static ContentObserver mDeveloperOptionsObserver;
64 
65     @Override
onReceive(Context context, Intent intent)66     public void onReceive(Context context, Intent intent) {
67         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
68 
69         if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
70             Log.i(TAG, "Received BOOT_COMPLETE");
71             createNotificationChannels(context);
72             updateDeveloperOptionsWatcher(context, /* fromBootIntent */ true);
73             // We know that Perfetto won't be tracing already at boot, so pass the
74             // tracingIsOff argument to avoid the Perfetto check.
75             updateTracing(context, /* assumeTracingIsOff= */ true);
76             TraceUtils.cleanupOlderFiles();
77         } else if (Intent.ACTION_USER_FOREGROUND.equals(intent.getAction())) {
78             updateStorageProvider(context, isTraceurAllowed(context));
79         } else if (STOP_ACTION.equals(intent.getAction())) {
80             // Only one of these should be enabled, but they all use the same path for stopping and
81             // saving, so set them all to false.
82             prefs.edit().putBoolean(
83                     context.getString(R.string.pref_key_tracing_on), false).commit();
84             prefs.edit().putBoolean(
85                     context.getString(R.string.pref_key_stack_sampling_on), false).commit();
86             prefs.edit().putBoolean(
87                     context.getString(R.string.pref_key_heap_dump_on), false).commit();
88             updateTracing(context);
89         } else if (OPEN_ACTION.equals(intent.getAction())) {
90             context.closeSystemDialogs();
91             context.startActivity(new Intent(context, MainActivity.class)
92                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
93         } else if (BUGREPORT_STARTED.equals(intent.getAction())) {
94             // If stop_on_bugreport is set and attach_to_bugreport is not, stop tracing.
95             // Otherwise, if attach_to_bugreport is set perfetto will end the session,
96             // and we should not take action on the Traceur side.
97             if (prefs.getBoolean(context.getString(R.string.pref_key_stop_on_bugreport), false) &&
98                 !prefs.getBoolean(context.getString(
99                         R.string.pref_key_attach_to_bugreport), true)) {
100                 Log.d(TAG, "Bugreport started, ending trace.");
101                 prefs.edit().putBoolean(context.getString(R.string.pref_key_tracing_on), false).commit();
102                 updateTracing(context);
103             }
104         }
105     }
106 
107     /*
108      * Updates the current tracing state based on the current state of preferences.
109      */
updateTracing(Context context)110     public static void updateTracing(Context context) {
111         updateTracing(context, false);
112     }
113 
updateTracing(Context context, boolean assumeTracingIsOff)114     public static void updateTracing(Context context, boolean assumeTracingIsOff) {
115         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
116         boolean prefsTracingOn =
117                 prefs.getBoolean(context.getString(R.string.pref_key_tracing_on), false);
118         boolean prefsStackSamplingOn =
119                 prefs.getBoolean(context.getString(R.string.pref_key_stack_sampling_on), false);
120         boolean prefsHeapDumpOn =
121                 prefs.getBoolean(context.getString(R.string.pref_key_heap_dump_on), false);
122 
123         // This checks that at most one of the three tracing types are enabled. This shouldn't
124         // happen because enabling one toggle should disable the others. Just in case, set all
125         // preferences to false and stop any ongoing trace.
126         if ((prefsTracingOn ^ prefsStackSamplingOn) ? prefsHeapDumpOn : prefsTracingOn) {
127             Log.e(TAG, "Preference state thinks that multiple trace configs should be active; " +
128                     "disabling all of them and stopping the ongoing trace if one exists.");
129             prefs.edit().putBoolean(
130                     context.getString(R.string.pref_key_tracing_on), false).commit();
131             prefs.edit().putBoolean(
132                     context.getString(R.string.pref_key_stack_sampling_on), false).commit();
133             prefs.edit().putBoolean(
134                     context.getString(R.string.pref_key_heap_dump_on), false).commit();
135             if (TraceUtils.isTracingOn()) {
136                 TraceService.stopTracing(context);
137             }
138             context.sendBroadcast(new Intent(MainFragment.ACTION_REFRESH_TAGS));
139             TraceService.updateAllQuickSettingsTiles();
140             return;
141         }
142 
143         boolean traceUtilsTracingOn = assumeTracingIsOff ? false : TraceUtils.isTracingOn();
144 
145         if ((prefsTracingOn || prefsStackSamplingOn || prefsHeapDumpOn) != traceUtilsTracingOn) {
146             if (prefsStackSamplingOn) {
147                 TraceService.startStackSampling(context);
148             } else if (prefsHeapDumpOn) {
149                 TraceService.startHeapDump(context);
150             } else if (prefsTracingOn) {
151                 // Show notification if the tags in preferences are not all actually available.
152                 Set<String> activeAvailableTags = getActiveTags(context, prefs, true);
153                 Set<String> activeTags = getActiveTags(context, prefs, false);
154 
155                 if (!activeAvailableTags.equals(activeTags)) {
156                     postCategoryNotification(context, prefs);
157                 }
158 
159                 int bufferSize = Integer.parseInt(
160                     prefs.getString(context.getString(R.string.pref_key_buffer_size),
161                         context.getString(R.string.default_buffer_size)));
162 
163                 boolean winscopeTracing = prefs.getBoolean(
164                     context.getString(R.string.pref_key_winscope),
165                         false);
166                 boolean appTracing = prefs.getBoolean(context.getString(R.string.pref_key_apps), true);
167                 boolean longTrace = prefs.getBoolean(context.getString(R.string.pref_key_long_traces), true);
168 
169                 int maxLongTraceSize = Integer.parseInt(
170                     prefs.getString(context.getString(R.string.pref_key_max_long_trace_size),
171                         context.getString(R.string.default_long_trace_size)));
172 
173                 int maxLongTraceDuration = Integer.parseInt(
174                     prefs.getString(context.getString(R.string.pref_key_max_long_trace_duration),
175                         context.getString(R.string.default_long_trace_duration)));
176 
177                 TraceService.startTracing(context, activeAvailableTags, bufferSize, winscopeTracing,
178                     appTracing, longTrace, maxLongTraceSize, maxLongTraceDuration);
179             } else {
180                 TraceService.stopTracing(context);
181             }
182         }
183 
184         // Update the main UI and the QS tile.
185         context.sendBroadcast(new Intent(MainFragment.ACTION_REFRESH_TAGS));
186         TraceService.updateAllQuickSettingsTiles();
187     }
188 
189     /*
190      * Updates the input Quick Settings tile state based on the current state of preferences.
191      */
updateQuickSettingsPanel(Context context, boolean enabled, Class serviceClass)192     private static void updateQuickSettingsPanel(Context context, boolean enabled,
193             Class serviceClass) {
194         ComponentName name = new ComponentName(context, serviceClass);
195         context.getPackageManager().setComponentEnabledSetting(name,
196             enabled
197                 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
198                 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
199             PackageManager.DONT_KILL_APP);
200 
201         IStatusBarService statusBarService = IStatusBarService.Stub.asInterface(
202             ServiceManager.checkService(Context.STATUS_BAR_SERVICE));
203 
204         try {
205             if (statusBarService != null) {
206                 if (enabled) {
207                     statusBarService.addTile(name);
208                 } else {
209                     statusBarService.remTile(name);
210                 }
211             }
212         } catch (RemoteException e) {
213             Log.e(TAG, "Failed to modify QS tile for Traceur.", e);
214         }
215         TraceService.updateAllQuickSettingsTiles();
216     }
217 
updateTracingQuickSettings(Context context)218     public static void updateTracingQuickSettings(Context context) {
219         boolean tracingQsEnabled =
220             PreferenceManager.getDefaultSharedPreferences(context)
221               .getBoolean(context.getString(R.string.pref_key_tracing_quick_setting), false);
222         updateQuickSettingsPanel(context, tracingQsEnabled, TracingQsService.class);
223     }
224 
updateStackSamplingQuickSettings(Context context)225     public static void updateStackSamplingQuickSettings(Context context) {
226         boolean stackSamplingQsEnabled =
227             PreferenceManager.getDefaultSharedPreferences(context)
228               .getBoolean(context.getString(R.string.pref_key_stack_sampling_quick_setting), false);
229         updateQuickSettingsPanel(context, stackSamplingQsEnabled, StackSamplingQsService.class);
230     }
231 
232     /*
233      * When Developer Options are toggled, also toggle the Storage Provider that
234      * shows "System traces" in Files.
235      * When Developer Options are turned off, reset the Show Quick Settings Tile
236      * preference to false to hide the tile. The user will need to re-enable the
237      * preference if they decide to turn Developer Options back on again.
238      */
updateDeveloperOptionsWatcher(Context context, boolean fromBootIntent)239     static void updateDeveloperOptionsWatcher(Context context, boolean fromBootIntent) {
240         if (mDeveloperOptionsObserver == null) {
241             Uri settingUri = Settings.Global.getUriFor(
242                 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED);
243 
244             mDeveloperOptionsObserver =
245                 new ContentObserver(new Handler()) {
246                     @Override
247                     public void onChange(boolean selfChange) {
248                         super.onChange(selfChange);
249                         boolean traceurAllowed = isTraceurAllowed(context);
250                         updateStorageProvider(context, traceurAllowed);
251                         if (!traceurAllowed) {
252                             SharedPreferences prefs =
253                                 PreferenceManager.getDefaultSharedPreferences(context);
254                             prefs.edit().putBoolean(
255                                 context.getString(R.string.pref_key_tracing_quick_setting), false)
256                                 .commit();
257                             prefs.edit().putBoolean(
258                                 context.getString(
259                                     R.string.pref_key_stack_sampling_quick_setting), false)
260                                 .commit();
261                             updateTracingQuickSettings(context);
262                             updateStackSamplingQuickSettings(context);
263                             // Stop an ongoing trace if one exists.
264                             if (TraceUtils.isTracingOn()) {
265                                 TraceService.stopTracingWithoutSaving(context);
266                             }
267                         }
268                     }
269                 };
270 
271             context.getContentResolver().registerContentObserver(settingUri,
272                 false, mDeveloperOptionsObserver);
273             // If this observer is being created and registered on boot, it can be assumed that
274             // developer options did not change in the meantime.
275             if (!fromBootIntent) {
276                 mDeveloperOptionsObserver.onChange(true);
277             }
278         }
279     }
280 
281     // Enables/disables the System Traces storage component.
updateStorageProvider(Context context, boolean enableProvider)282     static void updateStorageProvider(Context context, boolean enableProvider) {
283         ComponentName name = new ComponentName(context, StorageProvider.class);
284         context.getPackageManager().setComponentEnabledSetting(name,
285                 enableProvider
286                         ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
287                         : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
288                 PackageManager.DONT_KILL_APP);
289     }
290 
postCategoryNotification(Context context, SharedPreferences prefs)291     private static void postCategoryNotification(Context context, SharedPreferences prefs) {
292         Intent sendIntent = new Intent(context, MainActivity.class);
293 
294         String title = context.getString(R.string.tracing_categories_unavailable);
295         String msg = TextUtils.join(", ", getActiveUnavailableTags(context, prefs));
296         final Notification.Builder builder =
297             new Notification.Builder(context, NOTIFICATION_CHANNEL_OTHER)
298                 .setSmallIcon(R.drawable.bugfood_icon)
299                 .setContentTitle(title)
300                 .setTicker(title)
301                 .setContentText(msg)
302                 .setContentIntent(PendingIntent.getActivity(
303                         context, 0, sendIntent, PendingIntent.FLAG_ONE_SHOT
304                                 | PendingIntent.FLAG_CANCEL_CURRENT
305                                 | PendingIntent.FLAG_IMMUTABLE))
306                 .setAutoCancel(true)
307                 .setLocalOnly(true)
308                 .setColor(context.getColor(
309                         com.android.internal.R.color.system_notification_accent_color));
310 
311         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
312             builder.extend(new Notification.TvExtender());
313         }
314 
315         context.getSystemService(NotificationManager.class)
316             .notify(Receiver.class.getName(), 0, builder.build());
317     }
318 
createNotificationChannels(Context context)319     private static void createNotificationChannels(Context context) {
320         NotificationChannel tracingChannel = new NotificationChannel(
321             NOTIFICATION_CHANNEL_TRACING,
322             context.getString(R.string.trace_is_being_recorded),
323             NotificationManager.IMPORTANCE_HIGH);
324         tracingChannel.setBypassDnd(true);
325         tracingChannel.enableVibration(true);
326         tracingChannel.setSound(null, null);
327         tracingChannel.setBlockable(true);
328 
329         NotificationChannel saveTraceChannel = new NotificationChannel(
330             NOTIFICATION_CHANNEL_OTHER,
331             context.getString(R.string.saving_trace),
332             NotificationManager.IMPORTANCE_HIGH);
333         saveTraceChannel.setBypassDnd(true);
334         saveTraceChannel.enableVibration(true);
335         saveTraceChannel.setSound(null, null);
336         saveTraceChannel.setBlockable(true);
337 
338         NotificationManager notificationManager =
339             context.getSystemService(NotificationManager.class);
340         notificationManager.createNotificationChannel(tracingChannel);
341         notificationManager.createNotificationChannel(saveTraceChannel);
342     }
343 
getActiveTags(Context context, SharedPreferences prefs, boolean onlyAvailable)344     public static Set<String> getActiveTags(Context context, SharedPreferences prefs, boolean onlyAvailable) {
345         Set<String> tags = prefs.getStringSet(context.getString(R.string.pref_key_tags),
346                 PresetTraceConfigs.getDefaultTags());
347         Set<String> available = TraceUtils.listCategories().keySet();
348 
349         if (onlyAvailable) {
350             tags.retainAll(available);
351         }
352 
353         Log.v(TAG, "getActiveTags(onlyAvailable=" + onlyAvailable + ") = \"" + tags.toString() + "\"");
354         return tags;
355     }
356 
getActiveUnavailableTags(Context context, SharedPreferences prefs)357     public static Set<String> getActiveUnavailableTags(Context context, SharedPreferences prefs) {
358         Set<String> tags = prefs.getStringSet(context.getString(R.string.pref_key_tags),
359                 PresetTraceConfigs.getDefaultTags());
360         Set<String> available = TraceUtils.listCategories().keySet();
361 
362         tags.removeAll(available);
363 
364         Log.v(TAG, "getActiveUnavailableTags() = \"" + tags.toString() + "\"");
365         return tags;
366     }
367 
isTraceurAllowed(Context context)368     public static boolean isTraceurAllowed(Context context) {
369         boolean developerOptionsEnabled = Settings.Global.getInt(context.getContentResolver(),
370                 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
371         UserManager userManager = context.getSystemService(UserManager.class);
372         boolean isAdminUser = userManager.isAdminUser();
373         boolean debuggingDisallowed = userManager.hasUserRestriction(
374                 UserManager.DISALLOW_DEBUGGING_FEATURES);
375 
376         // For Traceur usage to be allowed, developer options must be enabled, the user must be an
377         // admin, and the user must not have debugging features disallowed.
378         return developerOptionsEnabled && isAdminUser && !debuggingDisallowed;
379     }
380 }
381