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.preference.PreferenceManager;
36 import android.provider.Settings;
37 import android.text.TextUtils;
38 import android.util.ArraySet;
39 import android.util.Log;
40 
41 import com.android.internal.statusbar.IStatusBarService;
42 
43 import java.util.Arrays;
44 import java.util.List;
45 import java.util.Set;
46 
47 public class Receiver extends BroadcastReceiver {
48 
49     public static final String STOP_ACTION = "com.android.traceur.STOP";
50     public static final String OPEN_ACTION = "com.android.traceur.OPEN";
51     public static final String BUGREPORT_STARTED =
52             "com.android.internal.intent.action.BUGREPORT_STARTED";
53 
54     public static final String NOTIFICATION_CHANNEL_TRACING = "trace-is-being-recorded";
55     public static final String NOTIFICATION_CHANNEL_OTHER = "system-tracing";
56 
57     private static final List<String> TRACE_TAGS = Arrays.asList(
58             "am", "binder_driver", "camera", "dalvik", "freq", "gfx", "hal",
59             "idle", "input", "memory", "memreclaim", "power", "res", "sched",
60             "sync", "thermal", "view", "webview", "wm", "workq");
61 
62     /* The user list doesn't include workq or sync, because the user builds don't have
63      * permissions for them. */
64     private static final List<String> TRACE_TAGS_USER = Arrays.asList(
65             "am", "binder_driver", "camera", "dalvik", "freq", "gfx", "hal",
66             "idle", "input", "memory", "memreclaim", "power", "res", "sched",
67             "thermal", "view", "webview", "wm");
68 
69     private static final String TAG = "Traceur";
70 
71     private static final String BETTERBUG_PACKAGE_NAME =
72             "com.google.android.apps.internal.betterbug";
73 
74     private static Set<String> mDefaultTagList = null;
75     private static ContentObserver mDeveloperOptionsObserver;
76 
77     @Override
onReceive(Context context, Intent intent)78     public void onReceive(Context context, Intent intent) {
79         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
80 
81         if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
82             Log.i(TAG, "Received BOOT_COMPLETE");
83             createNotificationChannels(context);
84             updateDeveloperOptionsWatcher(context);
85             // We know that Perfetto won't be tracing already at boot, so pass the
86             // tracingIsOff argument to avoid the Perfetto check.
87             updateTracing(context, /* assumeTracingIsOff= */ true);
88         } else if (STOP_ACTION.equals(intent.getAction())) {
89             prefs.edit().putBoolean(
90                     context.getString(R.string.pref_key_tracing_on), false).commit();
91             updateTracing(context);
92         } else if (OPEN_ACTION.equals(intent.getAction())) {
93             context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
94             context.startActivity(new Intent(context, MainActivity.class)
95                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
96         } else if (BUGREPORT_STARTED.equals(intent.getAction())) {
97             // If stop_on_bugreport is set and attach_to_bugreport is not, stop tracing.
98             // Otherwise, if attach_to_bugreport is set perfetto will end the session,
99             // and we should not take action on the Traceur side.
100             if (prefs.getBoolean(context.getString(R.string.pref_key_stop_on_bugreport), false) &&
101                 !prefs.getBoolean(context.getString(
102                         R.string.pref_key_attach_to_bugreport), true)) {
103                 Log.d(TAG, "Bugreport started, ending trace.");
104                 prefs.edit().putBoolean(context.getString(R.string.pref_key_tracing_on), false).commit();
105                 updateTracing(context);
106             }
107         }
108     }
109 
110     /*
111      * Updates the current tracing state based on the current state of preferences.
112      */
updateTracing(Context context)113     public static void updateTracing(Context context) {
114         updateTracing(context, false);
115     }
116 
updateTracing(Context context, boolean assumeTracingIsOff)117     public static void updateTracing(Context context, boolean assumeTracingIsOff) {
118         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
119         boolean prefsTracingOn =
120                 prefs.getBoolean(context.getString(R.string.pref_key_tracing_on), false);
121 
122         boolean traceUtilsTracingOn = assumeTracingIsOff ? false : TraceUtils.isTracingOn();
123 
124         if (prefsTracingOn != traceUtilsTracingOn) {
125             if (prefsTracingOn) {
126                 // Show notification if the tags in preferences are not all actually available.
127                 Set<String> activeAvailableTags = getActiveTags(context, prefs, true);
128                 Set<String> activeTags = getActiveTags(context, prefs, false);
129 
130                 if (!activeAvailableTags.equals(activeTags)) {
131                     postCategoryNotification(context, prefs);
132                 }
133 
134                 int bufferSize = Integer.parseInt(
135                     prefs.getString(context.getString(R.string.pref_key_buffer_size),
136                         context.getString(R.string.default_buffer_size)));
137 
138                 boolean appTracing = prefs.getBoolean(context.getString(R.string.pref_key_apps), true);
139                 boolean longTrace = prefs.getBoolean(context.getString(R.string.pref_key_long_traces), true);
140 
141                 int maxLongTraceSize = Integer.parseInt(
142                     prefs.getString(context.getString(R.string.pref_key_max_long_trace_size),
143                         context.getString(R.string.default_long_trace_size)));
144 
145                 int maxLongTraceDuration = Integer.parseInt(
146                     prefs.getString(context.getString(R.string.pref_key_max_long_trace_duration),
147                         context.getString(R.string.default_long_trace_duration)));
148 
149                 TraceService.startTracing(context, activeAvailableTags, bufferSize,
150                     appTracing, longTrace, maxLongTraceSize, maxLongTraceDuration);
151             } else {
152                 TraceService.stopTracing(context);
153             }
154         }
155 
156         // Update the main UI and the QS tile.
157         context.sendBroadcast(new Intent(MainFragment.ACTION_REFRESH_TAGS));
158         QsService.updateTile();
159     }
160 
161     /*
162      * Updates the current Quick Settings tile state based on the current state
163      * of preferences.
164      */
updateQuickSettings(Context context)165     public static void updateQuickSettings(Context context) {
166         boolean quickSettingsEnabled =
167             PreferenceManager.getDefaultSharedPreferences(context)
168               .getBoolean(context.getString(R.string.pref_key_quick_setting), false);
169 
170         ComponentName name = new ComponentName(context, QsService.class);
171         context.getPackageManager().setComponentEnabledSetting(name,
172             quickSettingsEnabled
173                 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
174                 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
175             PackageManager.DONT_KILL_APP);
176 
177         IStatusBarService statusBarService = IStatusBarService.Stub.asInterface(
178             ServiceManager.checkService(Context.STATUS_BAR_SERVICE));
179 
180         try {
181             if (statusBarService != null) {
182                 if (quickSettingsEnabled) {
183                     statusBarService.addTile(name);
184                 } else {
185                     statusBarService.remTile(name);
186                 }
187             }
188         } catch (RemoteException e) {
189             Log.e(TAG, "Failed to modify QS tile for Traceur.", e);
190         }
191 
192         QsService.updateTile();
193     }
194 
195     /*
196      * When Developer Options are toggled, also toggle the Storage Provider that
197      * shows "System traces" in Files.
198      * When Developer Options are turned off, reset the Show Quick Settings Tile
199      * preference to false to hide the tile. The user will need to re-enable the
200      * preference if they decide to turn Developer Options back on again.
201      */
updateDeveloperOptionsWatcher(Context context)202     static void updateDeveloperOptionsWatcher(Context context) {
203         if (mDeveloperOptionsObserver == null) {
204             Uri settingUri = Settings.Global.getUriFor(
205                 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED);
206 
207             mDeveloperOptionsObserver =
208                 new ContentObserver(new Handler()) {
209                     @Override
210                     public void onChange(boolean selfChange) {
211                         super.onChange(selfChange);
212 
213                         boolean developerOptionsEnabled = (1 ==
214                             Settings.Global.getInt(context.getContentResolver(),
215                                 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0));
216 
217                         ComponentName name = new ComponentName(context,
218                             StorageProvider.class);
219                         context.getPackageManager().setComponentEnabledSetting(name,
220                            developerOptionsEnabled
221                                 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
222                                 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
223                             PackageManager.DONT_KILL_APP);
224 
225                         if (!developerOptionsEnabled) {
226                             SharedPreferences prefs =
227                                 PreferenceManager.getDefaultSharedPreferences(context);
228                             prefs.edit().putBoolean(
229                                 context.getString(R.string.pref_key_quick_setting), false)
230                                 .commit();
231                             updateQuickSettings(context);
232                             // Stop an ongoing trace if one exists.
233                             if (TraceUtils.isTracingOn()) {
234                                 TraceService.stopTracingWithoutSaving(context);
235                             }
236                         }
237                     }
238                 };
239 
240             context.getContentResolver().registerContentObserver(settingUri,
241                 false, mDeveloperOptionsObserver);
242             mDeveloperOptionsObserver.onChange(true);
243         }
244     }
245 
postCategoryNotification(Context context, SharedPreferences prefs)246     private static void postCategoryNotification(Context context, SharedPreferences prefs) {
247         Intent sendIntent = new Intent(context, MainActivity.class);
248 
249         String title = context.getString(R.string.tracing_categories_unavailable);
250         String msg = TextUtils.join(", ", getActiveUnavailableTags(context, prefs));
251         final Notification.Builder builder =
252             new Notification.Builder(context, NOTIFICATION_CHANNEL_OTHER)
253                 .setSmallIcon(R.drawable.bugfood_icon)
254                 .setContentTitle(title)
255                 .setTicker(title)
256                 .setContentText(msg)
257                 .setContentIntent(PendingIntent.getActivity(
258                         context, 0, sendIntent, PendingIntent.FLAG_ONE_SHOT
259                                 | PendingIntent.FLAG_CANCEL_CURRENT
260                                 | PendingIntent.FLAG_IMMUTABLE))
261                 .setAutoCancel(true)
262                 .setLocalOnly(true)
263                 .setColor(context.getColor(
264                         com.android.internal.R.color.system_notification_accent_color));
265 
266         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
267             builder.extend(new Notification.TvExtender());
268         }
269 
270         context.getSystemService(NotificationManager.class)
271             .notify(Receiver.class.getName(), 0, builder.build());
272     }
273 
createNotificationChannels(Context context)274     private static void createNotificationChannels(Context context) {
275         NotificationChannel tracingChannel = new NotificationChannel(
276             NOTIFICATION_CHANNEL_TRACING,
277             context.getString(R.string.trace_is_being_recorded),
278             NotificationManager.IMPORTANCE_HIGH);
279         tracingChannel.setBypassDnd(true);
280         tracingChannel.enableVibration(true);
281         tracingChannel.setSound(null, null);
282 
283         NotificationChannel saveTraceChannel = new NotificationChannel(
284             NOTIFICATION_CHANNEL_OTHER,
285             context.getString(R.string.saving_trace),
286             NotificationManager.IMPORTANCE_HIGH);
287         saveTraceChannel.setBypassDnd(true);
288         saveTraceChannel.enableVibration(true);
289         saveTraceChannel.setSound(null, null);
290 
291         NotificationManager notificationManager =
292             context.getSystemService(NotificationManager.class);
293         notificationManager.createNotificationChannel(tracingChannel);
294         notificationManager.createNotificationChannel(saveTraceChannel);
295     }
296 
getActiveTags(Context context, SharedPreferences prefs, boolean onlyAvailable)297     public static Set<String> getActiveTags(Context context, SharedPreferences prefs, boolean onlyAvailable) {
298         Set<String> tags = prefs.getStringSet(context.getString(R.string.pref_key_tags),
299                 getDefaultTagList());
300         Set<String> available = TraceUtils.listCategories().keySet();
301 
302         if (onlyAvailable) {
303             tags.retainAll(available);
304         }
305 
306         Log.v(TAG, "getActiveTags(onlyAvailable=" + onlyAvailable + ") = \"" + tags.toString() + "\"");
307         return tags;
308     }
309 
getActiveUnavailableTags(Context context, SharedPreferences prefs)310     public static Set<String> getActiveUnavailableTags(Context context, SharedPreferences prefs) {
311         Set<String> tags = prefs.getStringSet(context.getString(R.string.pref_key_tags),
312                 getDefaultTagList());
313         Set<String> available = TraceUtils.listCategories().keySet();
314 
315         tags.removeAll(available);
316 
317         Log.v(TAG, "getActiveUnavailableTags() = \"" + tags.toString() + "\"");
318         return tags;
319     }
320 
getDefaultTagList()321     public static Set<String> getDefaultTagList() {
322         if (mDefaultTagList == null) {
323             mDefaultTagList = new ArraySet<String>(Build.TYPE.equals("user")
324                 ? TRACE_TAGS_USER : TRACE_TAGS);
325         }
326 
327         return mDefaultTagList;
328     }
329 }
330