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