• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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