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.IntentService; 20 import android.app.Notification; 21 import android.app.NotificationManager; 22 import android.app.PendingIntent; 23 import android.app.Service; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.SharedPreferences; 27 import android.content.pm.PackageManager; 28 import android.net.Uri; 29 import android.preference.PreferenceManager; 30 31 import java.io.File; 32 import java.util.ArrayList; 33 import java.util.Collection; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Set; 37 import java.util.Optional; 38 39 public class TraceService extends IntentService { 40 // Authority used to share trace files from Traceur to other apps 41 static final String AUTHORITY = "com.android.traceur.files"; 42 /* Indicates Perfetto has stopped tracing due to either the supplied long trace limitations 43 * or limited storage capacity. */ 44 static String INTENT_ACTION_NOTIFY_SESSION_STOPPED = 45 "com.android.traceur.NOTIFY_SESSION_STOPPED"; 46 /* Indicates a Traceur-associated tracing session has been attached to a bug report */ 47 static String INTENT_ACTION_NOTIFY_SESSION_STOLEN = 48 "com.android.traceur.NOTIFY_SESSION_STOLEN"; 49 private static String INTENT_ACTION_STOP_TRACING = "com.android.traceur.STOP_TRACING"; 50 private static String INTENT_ACTION_START_TRACING = "com.android.traceur.START_TRACING"; 51 private static String INTENT_ACTION_START_STACK_SAMPLING = 52 "com.android.traceur.START_STACK_SAMPLING"; 53 private static String INTENT_ACTION_START_HEAP_DUMP = 54 "com.android.traceur.START_HEAP_DUMP"; 55 56 private static String INTENT_EXTRA_TAGS= "tags"; 57 private static String INTENT_EXTRA_BUFFER = "buffer"; 58 private static String INTENT_EXTRA_WINSCOPE = "winscope"; 59 private static String INTENT_EXTRA_APPS = "apps"; 60 private static String INTENT_EXTRA_LONG_TRACE = "long_trace"; 61 private static String INTENT_EXTRA_LONG_TRACE_SIZE = "long_trace_size"; 62 private static String INTENT_EXTRA_LONG_TRACE_DURATION = "long_trace_duration"; 63 64 private static String BETTERBUG_PACKAGE_NAME = "com.google.android.apps.internal.betterbug"; 65 66 private static int TRACE_NOTIFICATION = 1; 67 private static int SAVING_TRACE_NOTIFICATION = 2; 68 startTracing(final Context context, Collection<String> tags, int bufferSizeKb, boolean winscope, boolean apps, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)69 public static void startTracing(final Context context, 70 Collection<String> tags, int bufferSizeKb, boolean winscope, boolean apps, 71 boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes) { 72 Intent intent = new Intent(context, TraceService.class); 73 intent.setAction(INTENT_ACTION_START_TRACING); 74 intent.putExtra(INTENT_EXTRA_TAGS, new ArrayList(tags)); 75 intent.putExtra(INTENT_EXTRA_BUFFER, bufferSizeKb); 76 intent.putExtra(INTENT_EXTRA_WINSCOPE, winscope); 77 intent.putExtra(INTENT_EXTRA_APPS, apps); 78 intent.putExtra(INTENT_EXTRA_LONG_TRACE, longTrace); 79 intent.putExtra(INTENT_EXTRA_LONG_TRACE_SIZE, maxLongTraceSizeMb); 80 intent.putExtra(INTENT_EXTRA_LONG_TRACE_DURATION, maxLongTraceDurationMinutes); 81 context.startForegroundService(intent); 82 } 83 startStackSampling(final Context context)84 public static void startStackSampling(final Context context) { 85 Intent intent = new Intent(context, TraceService.class); 86 intent.setAction(INTENT_ACTION_START_STACK_SAMPLING); 87 context.startForegroundService(intent); 88 } 89 startHeapDump(final Context context)90 public static void startHeapDump(final Context context) { 91 Intent intent = new Intent(context, TraceService.class); 92 intent.setAction(INTENT_ACTION_START_HEAP_DUMP); 93 context.startForegroundService(intent); 94 } 95 stopTracing(final Context context)96 public static void stopTracing(final Context context) { 97 Intent intent = new Intent(context, TraceService.class); 98 intent.setAction(INTENT_ACTION_STOP_TRACING); 99 context.startForegroundService(intent); 100 } 101 102 // Silently stops a trace without saving it. This is intended to be called when tracing is no 103 // longer allowed, i.e. if developer options are turned off while tracing. The usual method of 104 // stopping a trace via intent, stopTracing(), will not work because intents cannot be received 105 // when developer options are disabled. stopTracingWithoutSaving(final Context context)106 static void stopTracingWithoutSaving(final Context context) { 107 NotificationManager notificationManager = 108 context.getSystemService(NotificationManager.class); 109 notificationManager.cancel(TRACE_NOTIFICATION); 110 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 111 prefs.edit().putBoolean(context.getString( 112 R.string.pref_key_tracing_on), false).commit(); 113 TraceUtils.traceStop(context); 114 } 115 TraceService()116 public TraceService() { 117 this("TraceService"); 118 } 119 TraceService(String name)120 protected TraceService(String name) { 121 super(name); 122 setIntentRedelivery(true); 123 } 124 125 @Override onHandleIntent(Intent intent)126 public void onHandleIntent(Intent intent) { 127 Context context = getApplicationContext(); 128 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 129 if (!Receiver.isTraceurAllowed(context)) { 130 return; 131 } 132 133 TraceUtils.RecordingType type = getRecentTraceType(context); 134 135 if (intent.getAction().equals(INTENT_ACTION_START_TRACING)) { 136 startTracingInternal(intent.getStringArrayListExtra(INTENT_EXTRA_TAGS), 137 intent.getIntExtra(INTENT_EXTRA_BUFFER, 138 Integer.parseInt(context.getString(R.string.default_buffer_size))), 139 intent.getBooleanExtra(INTENT_EXTRA_WINSCOPE, false), 140 intent.getBooleanExtra(INTENT_EXTRA_APPS, false), 141 intent.getBooleanExtra(INTENT_EXTRA_LONG_TRACE, false), 142 intent.getIntExtra(INTENT_EXTRA_LONG_TRACE_SIZE, 143 Integer.parseInt(context.getString(R.string.default_long_trace_size))), 144 intent.getIntExtra(INTENT_EXTRA_LONG_TRACE_DURATION, 145 Integer.parseInt(context.getString(R.string.default_long_trace_duration)))); 146 } else if (intent.getAction().equals(INTENT_ACTION_START_STACK_SAMPLING)) { 147 startStackSamplingInternal(); 148 } else if (intent.getAction().equals(INTENT_ACTION_START_HEAP_DUMP)) { 149 startHeapDumpInternal(); 150 } else if (intent.getAction().equals(INTENT_ACTION_STOP_TRACING)) { 151 stopTracingInternal(TraceUtils.getOutputFilename(type), false); 152 } else if (intent.getAction().equals(INTENT_ACTION_NOTIFY_SESSION_STOPPED)) { 153 stopTracingInternal(TraceUtils.getOutputFilename(type), false); 154 } else if (intent.getAction().equals(INTENT_ACTION_NOTIFY_SESSION_STOLEN)) { 155 stopTracingInternal("", true); 156 } 157 } 158 updateAllQuickSettingsTiles()159 static void updateAllQuickSettingsTiles() { 160 TracingQsService.updateTile(); 161 StackSamplingQsService.updateTile(); 162 } 163 getRecentTraceType(Context context)164 private static TraceUtils.RecordingType getRecentTraceType(Context context) { 165 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 166 boolean recordingWasTrace = prefs.getBoolean( 167 context.getString(R.string.pref_key_recording_was_trace), true); 168 boolean recordingWasStackSamples = prefs.getBoolean( 169 context.getString(R.string.pref_key_recording_was_stack_samples), true); 170 if (recordingWasTrace) { 171 return TraceUtils.RecordingType.TRACE; 172 } else if (recordingWasStackSamples) { 173 return TraceUtils.RecordingType.STACK_SAMPLES; 174 } else { 175 return TraceUtils.RecordingType.HEAP_DUMP; 176 } 177 } 178 startTracingInternal(Collection<String> tags, int bufferSizeKb, boolean winscopeTracing, boolean appTracing, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)179 private void startTracingInternal(Collection<String> tags, int bufferSizeKb, 180 boolean winscopeTracing, boolean appTracing, boolean longTrace, int maxLongTraceSizeMb, 181 int maxLongTraceDurationMinutes) { 182 Context context = getApplicationContext(); 183 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 184 185 Intent stopIntent = new Intent(Receiver.STOP_ACTION, 186 null, context, Receiver.class); 187 stopIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 188 189 boolean attachToBugreport = 190 prefs.getBoolean(context.getString(R.string.pref_key_attach_to_bugreport), true); 191 192 Notification.Builder notification = getTraceurNotification( 193 context.getString(R.string.trace_is_being_recorded), 194 context.getString(R.string.tap_to_stop_tracing), 195 Receiver.NOTIFICATION_CHANNEL_TRACING); 196 notification.setOngoing(true) 197 .setContentIntent(PendingIntent.getBroadcast(context, 0, stopIntent, 198 PendingIntent.FLAG_IMMUTABLE)); 199 200 startForeground(TRACE_NOTIFICATION, notification.build()); 201 202 if (TraceUtils.traceStart(this, tags, bufferSizeKb, winscopeTracing, 203 appTracing, longTrace, attachToBugreport, maxLongTraceSizeMb, 204 maxLongTraceDurationMinutes)) { 205 stopForeground(Service.STOP_FOREGROUND_DETACH); 206 } else { 207 // Starting the trace was unsuccessful, so ensure that tracing 208 // is stopped and the preference is reset. 209 TraceUtils.traceStop(this); 210 prefs.edit().putBoolean(context.getString(R.string.pref_key_tracing_on), 211 false).commit(); 212 updateAllQuickSettingsTiles(); 213 stopForeground(Service.STOP_FOREGROUND_REMOVE); 214 } 215 216 // This is used to keep track of whether the most recent recording was a trace for the 217 // purpose of 1) determining which notification should be sent after the recording is done, 218 // and 2) choosing the filename format for the saved recording. 219 prefs.edit().putBoolean( 220 context.getString(R.string.pref_key_recording_was_trace), true).commit(); 221 prefs.edit().putBoolean( 222 context.getString(R.string.pref_key_recording_was_stack_samples), false).commit(); 223 } 224 startStackSamplingInternal()225 private void startStackSamplingInternal() { 226 Context context = getApplicationContext(); 227 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 228 229 Intent stopIntent = new Intent(Receiver.STOP_ACTION, null, context, Receiver.class); 230 stopIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 231 232 boolean attachToBugreport = 233 prefs.getBoolean(context.getString(R.string.pref_key_attach_to_bugreport), true); 234 235 Notification.Builder notification = getTraceurNotification( 236 context.getString(R.string.stack_samples_are_being_recorded), 237 context.getString(R.string.tap_to_stop_stack_sampling), 238 Receiver.NOTIFICATION_CHANNEL_TRACING); 239 notification.setOngoing(true) 240 .setContentIntent(PendingIntent.getBroadcast(context, 0, stopIntent, 241 PendingIntent.FLAG_IMMUTABLE)); 242 243 startForeground(TRACE_NOTIFICATION, notification.build()); 244 245 if (TraceUtils.stackSampleStart(attachToBugreport)) { 246 stopForeground(Service.STOP_FOREGROUND_DETACH); 247 } else { 248 // Starting stack sampling was unsuccessful, so ensure that it is stopped and the 249 // preference is reset. 250 TraceUtils.traceStop(this); 251 prefs.edit().putBoolean( 252 context.getString(R.string.pref_key_stack_sampling_on), false).commit(); 253 updateAllQuickSettingsTiles(); 254 stopForeground(Service.STOP_FOREGROUND_REMOVE); 255 } 256 257 // This is used to keep track of whether the most recent recording was a trace for the 258 // purpose of 1) determining which notification should be sent after the recording is done, 259 // and 2) choosing the filename format for the saved recording. 260 prefs.edit().putBoolean( 261 context.getString(R.string.pref_key_recording_was_trace), false).commit(); 262 prefs.edit().putBoolean( 263 context.getString(R.string.pref_key_recording_was_stack_samples), true).commit(); 264 } 265 startHeapDumpInternal()266 private void startHeapDumpInternal() { 267 Context context = getApplicationContext(); 268 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 269 270 Intent stopIntent = new Intent(Receiver.STOP_ACTION, null, context, Receiver.class); 271 stopIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 272 273 boolean attachToBugreport = 274 prefs.getBoolean(context.getString(R.string.pref_key_attach_to_bugreport), true); 275 boolean continuousDump = 276 prefs.getBoolean(context.getString(R.string.pref_key_continuous_heap_dump), false); 277 Set<String> processes = prefs.getStringSet( 278 context.getString(R.string.pref_key_heap_dump_processes), Collections.emptySet()); 279 280 int dumpIntervalSeconds = Integer.parseInt( 281 prefs.getString(context.getString(R.string.pref_key_continuous_heap_dump_interval), 282 context.getString(R.string.default_continuous_heap_dump_interval))); 283 284 Notification.Builder notification = getTraceurNotification( 285 context.getString(R.string.heap_dump_is_being_recorded), 286 context.getString(R.string.tap_to_stop_heap_dump), 287 Receiver.NOTIFICATION_CHANNEL_TRACING); 288 notification.setOngoing(true) 289 .setContentIntent(PendingIntent.getBroadcast(context, 0, stopIntent, 290 PendingIntent.FLAG_IMMUTABLE)); 291 292 startForeground(TRACE_NOTIFICATION, notification.build()); 293 294 if (TraceUtils.heapDumpStart(processes, continuousDump, dumpIntervalSeconds, 295 attachToBugreport)) { 296 stopForeground(Service.STOP_FOREGROUND_DETACH); 297 } else { 298 TraceUtils.traceStop(this); 299 prefs.edit().putBoolean( 300 context.getString(R.string.pref_key_heap_dump_on), false).commit(); 301 updateAllQuickSettingsTiles(); 302 stopForeground(Service.STOP_FOREGROUND_REMOVE); 303 } 304 305 prefs.edit().putBoolean( 306 context.getString(R.string.pref_key_recording_was_trace), false).commit(); 307 prefs.edit().putBoolean( 308 context.getString(R.string.pref_key_recording_was_stack_samples), false).commit(); 309 } 310 stopTracingInternal(String outputFilename, boolean sessionStolen)311 private void stopTracingInternal(String outputFilename, boolean sessionStolen) { 312 Context context = getApplicationContext(); 313 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 314 NotificationManager notificationManager = 315 getSystemService(NotificationManager.class); 316 317 // This helps determine which text to show on the post-recording notifications. 318 TraceUtils.RecordingType type = getRecentTraceType(context); 319 int savingTextResId; 320 switch (type) { 321 case STACK_SAMPLES: 322 savingTextResId = R.string.saving_stack_samples; 323 break; 324 case HEAP_DUMP: 325 savingTextResId = R.string.saving_heap_dump; 326 break; 327 case TRACE: 328 case UNKNOWN: 329 default: 330 savingTextResId = R.string.saving_trace; 331 break; 332 } 333 Notification.Builder notification = getTraceurNotification(context.getString( 334 sessionStolen ? R.string.attaching_to_report : savingTextResId), 335 null, Receiver.NOTIFICATION_CHANNEL_OTHER); 336 notification.setProgress(1, 0, true); 337 338 startForeground(SAVING_TRACE_NOTIFICATION, notification.build()); 339 340 notificationManager.cancel(TRACE_NOTIFICATION); 341 342 if (sessionStolen) { 343 Notification.Builder notificationAttached = getTraceurNotification( 344 context.getString(R.string.attached_to_report), null, 345 Receiver.NOTIFICATION_CHANNEL_OTHER); 346 notification.setAutoCancel(true); 347 348 Intent openIntent = 349 getPackageManager().getLaunchIntentForPackage(BETTERBUG_PACKAGE_NAME); 350 if (openIntent != null) { 351 // Add "Tap to open BetterBug" to notification only if intent is non-null. 352 notificationAttached.setContentText(getString( 353 R.string.attached_to_report_summary)); 354 notificationAttached.setContentIntent(PendingIntent.getActivity( 355 context, 0, openIntent, PendingIntent.FLAG_ONE_SHOT 356 | PendingIntent.FLAG_CANCEL_CURRENT 357 | PendingIntent.FLAG_IMMUTABLE)); 358 } 359 360 // Adds an action button to the notification for starting a new trace. This is only 361 // enabled for standard traces. 362 if (type == TraceUtils.RecordingType.TRACE) { 363 Intent restartIntent = new Intent(context, InternalReceiver.class); 364 restartIntent.setAction(InternalReceiver.START_ACTION); 365 PendingIntent restartPendingIntent = PendingIntent.getBroadcast(context, 0, 366 restartIntent, PendingIntent.FLAG_ONE_SHOT 367 | PendingIntent.FLAG_CANCEL_CURRENT 368 | PendingIntent.FLAG_IMMUTABLE); 369 Notification.Action action = new Notification.Action.Builder( 370 R.drawable.bugfood_icon, context.getString(R.string.start_new_trace), 371 restartPendingIntent).build(); 372 notificationAttached.addAction(action); 373 } 374 375 NotificationManager.from(context).notify(0, notificationAttached.build()); 376 } else { 377 Optional<List<File>> files = TraceUtils.traceDump(this, outputFilename); 378 if (files.isPresent()) { 379 postFileSharingNotification(getApplicationContext(), files.get()); 380 } 381 } 382 383 stopForeground(Service.STOP_FOREGROUND_REMOVE); 384 385 TraceUtils.cleanupOlderFiles(); 386 } 387 postFileSharingNotification(Context context, List<File> files)388 private void postFileSharingNotification(Context context, List<File> files) { 389 if (files.isEmpty()) { 390 return; 391 } 392 393 // Files are kept on private storage, so turn into Uris that we can 394 // grant temporary permissions for. 395 final List<Uri> traceUris = FileSender.getUriForFiles(context, files, AUTHORITY); 396 397 // Intent to send the file 398 Intent sendIntent = FileSender.buildSendIntent(context, traceUris); 399 sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 400 401 // This dialog will show to warn the user about sharing traces, then will execute 402 // the above file-sharing intent. 403 final Intent intent = new Intent(context, UserConsentActivityDialog.class); 404 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_RECEIVER_FOREGROUND); 405 intent.putExtra(Intent.EXTRA_INTENT, sendIntent); 406 407 TraceUtils.RecordingType type = getRecentTraceType(context); 408 int titleResId; 409 switch (type) { 410 case STACK_SAMPLES: 411 titleResId = R.string.stack_samples_saved; 412 break; 413 case HEAP_DUMP: 414 titleResId = R.string.heap_dump_saved; 415 break; 416 case TRACE: 417 case UNKNOWN: 418 default: 419 titleResId = R.string.trace_saved; 420 break; 421 } 422 final Notification.Builder builder = getTraceurNotification(context.getString(titleResId), 423 context.getString(R.string.tap_to_share), Receiver.NOTIFICATION_CHANNEL_OTHER) 424 .setContentIntent(PendingIntent.getActivity(context, 425 traceUris.get(0).hashCode(), intent,PendingIntent.FLAG_ONE_SHOT 426 | PendingIntent.FLAG_CANCEL_CURRENT 427 | PendingIntent.FLAG_IMMUTABLE)) 428 .setAutoCancel(true); 429 NotificationManager.from(context).notify(files.get(0).getName(), 0, builder.build()); 430 } 431 432 // Creates a Traceur notification for the given channel using the provided title and message. getTraceurNotification(String title, String msg, String channel)433 private Notification.Builder getTraceurNotification(String title, String msg, String channel) { 434 Context context = getApplicationContext(); 435 Notification.Builder notification = new Notification.Builder(context, channel) 436 .setContentTitle(title) 437 .setTicker(title) 438 .setSmallIcon(R.drawable.bugfood_icon) 439 .setLocalOnly(true) 440 .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) 441 .setColor(context.getColor( 442 com.android.internal.R.color.system_notification_accent_color)); 443 444 // Some Traceur notifications only have a title. 445 if (msg != null) { 446 notification.setContentText(msg); 447 } 448 449 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { 450 notification.extend(new Notification.TvExtender()); 451 } 452 453 return notification; 454 } 455 } 456