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