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