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.os.UserManager; 29 import android.preference.PreferenceManager; 30 import android.provider.Settings; 31 import android.text.format.DateUtils; 32 import android.util.EventLog; 33 import android.util.Log; 34 35 import java.io.File; 36 import java.util.ArrayList; 37 import java.util.Collection; 38 39 public class TraceService extends IntentService { 40 /* Indicates Perfetto has stopped tracing due to either the supplied long trace limitations 41 * or limited storage capacity. */ 42 static String INTENT_ACTION_NOTIFY_SESSION_STOPPED = 43 "com.android.traceur.NOTIFY_SESSION_STOPPED"; 44 /* Indicates a Traceur-associated tracing session has been attached to a bug report */ 45 static String INTENT_ACTION_NOTIFY_SESSION_STOLEN = 46 "com.android.traceur.NOTIFY_SESSION_STOLEN"; 47 private static String INTENT_ACTION_STOP_TRACING = "com.android.traceur.STOP_TRACING"; 48 private static String INTENT_ACTION_START_TRACING = "com.android.traceur.START_TRACING"; 49 private static String INTENT_ACTION_START_STACK_SAMPLING = 50 "com.android.traceur.START_STACK_SAMPLING"; 51 52 private static String INTENT_EXTRA_TAGS= "tags"; 53 private static String INTENT_EXTRA_BUFFER = "buffer"; 54 private static String INTENT_EXTRA_APPS = "apps"; 55 private static String INTENT_EXTRA_LONG_TRACE = "long_trace"; 56 private static String INTENT_EXTRA_LONG_TRACE_SIZE = "long_trace_size"; 57 private static String INTENT_EXTRA_LONG_TRACE_DURATION = "long_trace_duration"; 58 59 private static String BETTERBUG_PACKAGE_NAME = "com.google.android.apps.internal.betterbug"; 60 61 private static int TRACE_NOTIFICATION = 1; 62 private static int SAVING_TRACE_NOTIFICATION = 2; 63 64 private static final int MIN_KEEP_COUNT = 3; 65 private static final long MIN_KEEP_AGE = 4 * DateUtils.WEEK_IN_MILLIS; 66 startTracing(final Context context, Collection<String> tags, int bufferSizeKb, boolean apps, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)67 public static void startTracing(final Context context, 68 Collection<String> tags, int bufferSizeKb, boolean apps, 69 boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes) { 70 Intent intent = new Intent(context, TraceService.class); 71 intent.setAction(INTENT_ACTION_START_TRACING); 72 intent.putExtra(INTENT_EXTRA_TAGS, new ArrayList(tags)); 73 intent.putExtra(INTENT_EXTRA_BUFFER, bufferSizeKb); 74 intent.putExtra(INTENT_EXTRA_APPS, apps); 75 intent.putExtra(INTENT_EXTRA_LONG_TRACE, longTrace); 76 intent.putExtra(INTENT_EXTRA_LONG_TRACE_SIZE, maxLongTraceSizeMb); 77 intent.putExtra(INTENT_EXTRA_LONG_TRACE_DURATION, maxLongTraceDurationMinutes); 78 context.startForegroundService(intent); 79 } 80 startStackSampling(final Context context)81 public static void startStackSampling(final Context context) { 82 Intent intent = new Intent(context, TraceService.class); 83 intent.setAction(INTENT_ACTION_START_STACK_SAMPLING); 84 context.startForegroundService(intent); 85 } 86 stopTracing(final Context context)87 public static void stopTracing(final Context context) { 88 Intent intent = new Intent(context, TraceService.class); 89 intent.setAction(INTENT_ACTION_STOP_TRACING); 90 context.startForegroundService(intent); 91 } 92 93 // Silently stops a trace without saving it. This is intended to be called when tracing is no 94 // longer allowed, i.e. if developer options are turned off while tracing. The usual method of 95 // stopping a trace via intent, stopTracing(), will not work because intents cannot be received 96 // when developer options are disabled. stopTracingWithoutSaving(final Context context)97 static void stopTracingWithoutSaving(final Context context) { 98 NotificationManager notificationManager = 99 context.getSystemService(NotificationManager.class); 100 notificationManager.cancel(TRACE_NOTIFICATION); 101 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 102 prefs.edit().putBoolean(context.getString( 103 R.string.pref_key_tracing_on), false).commit(); 104 TraceUtils.traceStop(); 105 } 106 TraceService()107 public TraceService() { 108 this("TraceService"); 109 } 110 TraceService(String name)111 protected TraceService(String name) { 112 super(name); 113 setIntentRedelivery(true); 114 } 115 116 @Override onHandleIntent(Intent intent)117 public void onHandleIntent(Intent intent) { 118 Context context = getApplicationContext(); 119 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 120 // Checks that developer options are enabled and the user is an admin before continuing. 121 boolean developerOptionsEnabled = 122 Settings.Global.getInt(context.getContentResolver(), 123 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; 124 if (!developerOptionsEnabled) { 125 // Refer to b/204992293. 126 EventLog.writeEvent(0x534e4554, "204992293", -1, ""); 127 return; 128 } 129 UserManager userManager = context.getSystemService(UserManager.class); 130 boolean isAdminUser = userManager.isAdminUser(); 131 boolean debuggingDisallowed = userManager.hasUserRestriction( 132 UserManager.DISALLOW_DEBUGGING_FEATURES); 133 if (!isAdminUser || debuggingDisallowed) { 134 return; 135 } 136 137 boolean recordingWasTrace = prefs.getBoolean( 138 context.getString(R.string.pref_key_recording_was_trace), true); 139 TraceUtils.RecordingType type = recordingWasTrace ? TraceUtils.RecordingType.TRACE 140 : TraceUtils.RecordingType.STACK_SAMPLES; 141 142 if (intent.getAction().equals(INTENT_ACTION_START_TRACING)) { 143 startTracingInternal(intent.getStringArrayListExtra(INTENT_EXTRA_TAGS), 144 intent.getIntExtra(INTENT_EXTRA_BUFFER, 145 Integer.parseInt(context.getString(R.string.default_buffer_size))), 146 intent.getBooleanExtra(INTENT_EXTRA_APPS, false), 147 intent.getBooleanExtra(INTENT_EXTRA_LONG_TRACE, false), 148 intent.getIntExtra(INTENT_EXTRA_LONG_TRACE_SIZE, 149 Integer.parseInt(context.getString(R.string.default_long_trace_size))), 150 intent.getIntExtra(INTENT_EXTRA_LONG_TRACE_DURATION, 151 Integer.parseInt(context.getString(R.string.default_long_trace_duration)))); 152 } else if (intent.getAction().equals(INTENT_ACTION_START_STACK_SAMPLING)) { 153 startStackSamplingInternal(); 154 } else if (intent.getAction().equals(INTENT_ACTION_STOP_TRACING)) { 155 stopTracingInternal(TraceUtils.getOutputFilename(type), false); 156 } else if (intent.getAction().equals(INTENT_ACTION_NOTIFY_SESSION_STOPPED)) { 157 stopTracingInternal(TraceUtils.getOutputFilename(type), false); 158 } else if (intent.getAction().equals(INTENT_ACTION_NOTIFY_SESSION_STOLEN)) { 159 stopTracingInternal("", true); 160 } 161 } 162 startTracingInternal(Collection<String> tags, int bufferSizeKb, boolean appTracing, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)163 private void startTracingInternal(Collection<String> tags, int bufferSizeKb, boolean appTracing, 164 boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes) { 165 Context context = getApplicationContext(); 166 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 167 168 Intent stopIntent = new Intent(Receiver.STOP_ACTION, 169 null, context, Receiver.class); 170 stopIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 171 172 boolean attachToBugreport = 173 prefs.getBoolean(context.getString(R.string.pref_key_attach_to_bugreport), true); 174 175 Notification.Builder notification = getTraceurNotification( 176 context.getString(R.string.trace_is_being_recorded), 177 context.getString(R.string.tap_to_stop_tracing), 178 Receiver.NOTIFICATION_CHANNEL_TRACING); 179 notification.setOngoing(true) 180 .setContentIntent(PendingIntent.getBroadcast(context, 0, stopIntent, 181 PendingIntent.FLAG_IMMUTABLE)); 182 183 startForeground(TRACE_NOTIFICATION, notification.build()); 184 185 if (TraceUtils.traceStart(tags, bufferSizeKb, appTracing, 186 longTrace, attachToBugreport, maxLongTraceSizeMb, maxLongTraceDurationMinutes)) { 187 stopForeground(Service.STOP_FOREGROUND_DETACH); 188 } else { 189 // Starting the trace was unsuccessful, so ensure that tracing 190 // is stopped and the preference is reset. 191 TraceUtils.traceStop(); 192 prefs.edit().putBoolean(context.getString(R.string.pref_key_tracing_on), 193 false).commit(); 194 QsService.updateTile(); 195 stopForeground(Service.STOP_FOREGROUND_REMOVE); 196 } 197 198 // This is used to keep track of whether the most recent recording was a trace for the 199 // purpose of 1) determining which notification should be sent after the recording is done, 200 // and 2) choosing the filename format for the saved recording. 201 prefs.edit().putBoolean( 202 context.getString(R.string.pref_key_recording_was_trace), true).commit(); 203 } 204 startStackSamplingInternal()205 private void startStackSamplingInternal() { 206 Context context = getApplicationContext(); 207 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 208 209 Intent stopIntent = new Intent(Receiver.STOP_ACTION, null, context, Receiver.class); 210 stopIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 211 212 boolean attachToBugreport = 213 prefs.getBoolean(context.getString(R.string.pref_key_attach_to_bugreport), true); 214 215 Notification.Builder notification = getTraceurNotification( 216 context.getString(R.string.stack_samples_are_being_recorded), 217 context.getString(R.string.tap_to_stop_stack_sampling), 218 Receiver.NOTIFICATION_CHANNEL_TRACING); 219 notification.setOngoing(true) 220 .setContentIntent(PendingIntent.getBroadcast(context, 0, stopIntent, 221 PendingIntent.FLAG_IMMUTABLE)); 222 223 startForeground(TRACE_NOTIFICATION, notification.build()); 224 225 if (TraceUtils.stackSampleStart(attachToBugreport)) { 226 stopForeground(Service.STOP_FOREGROUND_DETACH); 227 } else { 228 // Starting stack sampling was unsuccessful, so ensure that it is stopped and the 229 // preference is reset. 230 TraceUtils.traceStop(); 231 prefs.edit().putBoolean( 232 context.getString(R.string.pref_key_stack_sampling_on), false).commit(); 233 QsService.updateTile(); 234 stopForeground(Service.STOP_FOREGROUND_REMOVE); 235 } 236 237 // This is used to keep track of whether the most recent recording was a trace for the 238 // purpose of 1) determining which notification should be sent after the recording is done, 239 // and 2) choosing the filename format for the saved recording. 240 prefs.edit().putBoolean( 241 context.getString(R.string.pref_key_recording_was_trace), false).commit(); 242 } 243 stopTracingInternal(String outputFilename, boolean sessionStolen)244 private void stopTracingInternal(String outputFilename, boolean sessionStolen) { 245 Context context = getApplicationContext(); 246 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 247 NotificationManager notificationManager = 248 getSystemService(NotificationManager.class); 249 250 // This helps determine which text to show on the post-recording notifications. 251 boolean recordingWasTrace = prefs.getBoolean( 252 context.getString(R.string.pref_key_recording_was_trace), true); 253 254 Notification.Builder notification = getTraceurNotification(context.getString( 255 sessionStolen ? R.string.attaching_to_report : 256 recordingWasTrace ? R.string.saving_trace : R.string.saving_stack_samples), 257 null, Receiver.NOTIFICATION_CHANNEL_OTHER); 258 notification.setProgress(1, 0, true); 259 260 startForeground(SAVING_TRACE_NOTIFICATION, notification.build()); 261 262 notificationManager.cancel(TRACE_NOTIFICATION); 263 264 if (sessionStolen) { 265 Notification.Builder notificationAttached = getTraceurNotification( 266 context.getString(R.string.attached_to_report), null, 267 Receiver.NOTIFICATION_CHANNEL_OTHER); 268 notification.setAutoCancel(true); 269 270 Intent openIntent = 271 getPackageManager().getLaunchIntentForPackage(BETTERBUG_PACKAGE_NAME); 272 if (openIntent != null) { 273 // Add "Tap to open BetterBug" to notification only if intent is non-null. 274 notificationAttached.setContentText(getString( 275 R.string.attached_to_report_summary)); 276 notificationAttached.setContentIntent(PendingIntent.getActivity( 277 context, 0, openIntent, PendingIntent.FLAG_ONE_SHOT 278 | PendingIntent.FLAG_CANCEL_CURRENT 279 | PendingIntent.FLAG_IMMUTABLE)); 280 } 281 282 // Adds an action button to the notification for starting a new trace. This is currently 283 // not enabled for stack samples because we don't expect continuous stack samples 284 // (repeatedly stopped and attached to different bug reports) to be a common use case. 285 if (recordingWasTrace) { 286 Intent restartIntent = new Intent(context, InternalReceiver.class); 287 restartIntent.setAction(InternalReceiver.START_ACTION); 288 PendingIntent restartPendingIntent = PendingIntent.getBroadcast(context, 0, 289 restartIntent, PendingIntent.FLAG_ONE_SHOT 290 | PendingIntent.FLAG_CANCEL_CURRENT 291 | PendingIntent.FLAG_IMMUTABLE); 292 Notification.Action action = new Notification.Action.Builder( 293 R.drawable.bugfood_icon, context.getString(R.string.start_new_trace), 294 restartPendingIntent).build(); 295 notificationAttached.addAction(action); 296 } 297 298 NotificationManager.from(context).notify(0, notificationAttached.build()); 299 } else { 300 File file = TraceUtils.getOutputFile(outputFilename); 301 302 if (TraceUtils.traceDump(file)) { 303 FileSender.postNotification(getApplicationContext(), file); 304 } 305 } 306 307 stopForeground(Service.STOP_FOREGROUND_REMOVE); 308 309 TraceUtils.cleanupOlderFiles(MIN_KEEP_COUNT, MIN_KEEP_AGE); 310 } 311 312 // Creates a Traceur notification for the given channel using the provided title and message. getTraceurNotification(String title, String msg, String channel)313 private Notification.Builder getTraceurNotification(String title, String msg, String channel) { 314 Context context = getApplicationContext(); 315 Notification.Builder notification = new Notification.Builder(context, channel) 316 .setContentTitle(title) 317 .setTicker(title) 318 .setSmallIcon(R.drawable.bugfood_icon) 319 .setLocalOnly(true) 320 .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) 321 .setColor(context.getColor( 322 com.android.internal.R.color.system_notification_accent_color)); 323 324 // Some Traceur notifications only have a title. 325 if (msg != null) { 326 notification.setContentText(msg); 327 } 328 329 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { 330 notification.extend(new Notification.TvExtender()); 331 } 332 333 return notification; 334 } 335 } 336