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 
20 import android.app.IntentService;
21 import android.app.Notification;
22 import android.app.NotificationManager;
23 import android.app.PendingIntent;
24 import android.app.Service;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.SharedPreferences;
28 import android.content.pm.PackageManager;
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 
50     private static String INTENT_EXTRA_TAGS= "tags";
51     private static String INTENT_EXTRA_BUFFER = "buffer";
52     private static String INTENT_EXTRA_APPS = "apps";
53     private static String INTENT_EXTRA_LONG_TRACE = "long_trace";
54     private static String INTENT_EXTRA_LONG_TRACE_SIZE = "long_trace_size";
55     private static String INTENT_EXTRA_LONG_TRACE_DURATION = "long_trace_duration";
56 
57     private static String BETTERBUG_PACKAGE_NAME = "com.google.android.apps.internal.betterbug";
58 
59     private static int TRACE_NOTIFICATION = 1;
60     private static int SAVING_TRACE_NOTIFICATION = 2;
61 
62     private static final int MIN_KEEP_COUNT = 3;
63     private static final long MIN_KEEP_AGE = 4 * DateUtils.WEEK_IN_MILLIS;
64 
startTracing(final Context context, Collection<String> tags, int bufferSizeKb, boolean apps, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)65     public static void startTracing(final Context context,
66             Collection<String> tags, int bufferSizeKb, boolean apps,
67             boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes) {
68         Intent intent = new Intent(context, TraceService.class);
69         intent.setAction(INTENT_ACTION_START_TRACING);
70         intent.putExtra(INTENT_EXTRA_TAGS, new ArrayList(tags));
71         intent.putExtra(INTENT_EXTRA_BUFFER, bufferSizeKb);
72         intent.putExtra(INTENT_EXTRA_APPS, apps);
73         intent.putExtra(INTENT_EXTRA_LONG_TRACE, longTrace);
74         intent.putExtra(INTENT_EXTRA_LONG_TRACE_SIZE, maxLongTraceSizeMb);
75         intent.putExtra(INTENT_EXTRA_LONG_TRACE_DURATION, maxLongTraceDurationMinutes);
76         context.startForegroundService(intent);
77     }
78 
stopTracing(final Context context)79     public static void stopTracing(final Context context) {
80         Intent intent = new Intent(context, TraceService.class);
81         intent.setAction(INTENT_ACTION_STOP_TRACING);
82         context.startForegroundService(intent);
83     }
84 
85     // Silently stops a trace without saving it. This is intended to be called when tracing is no
86     // longer allowed, i.e. if developer options are turned off while tracing. The usual method of
87     // stopping a trace via intent, stopTracing(), will not work because intents cannot be received
88     // when developer options are disabled.
stopTracingWithoutSaving(final Context context)89     static void stopTracingWithoutSaving(final Context context) {
90         NotificationManager notificationManager =
91             context.getSystemService(NotificationManager.class);
92         notificationManager.cancel(TRACE_NOTIFICATION);
93         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
94         prefs.edit().putBoolean(context.getString(
95             R.string.pref_key_tracing_on), false).commit();
96         TraceUtils.traceStop();
97     }
98 
TraceService()99     public TraceService() {
100         this("TraceService");
101     }
102 
TraceService(String name)103     protected TraceService(String name) {
104         super(name);
105         setIntentRedelivery(true);
106     }
107 
108     @Override
onHandleIntent(Intent intent)109     public void onHandleIntent(Intent intent) {
110         Context context = getApplicationContext();
111         // Checks that developer options are enabled before continuing.
112         boolean developerOptionsEnabled =
113                 Settings.Global.getInt(context.getContentResolver(),
114                         Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
115         if (!developerOptionsEnabled) {
116             // Refer to b/204992293.
117             EventLog.writeEvent(0x534e4554, "204992293", -1, "");
118             return;
119         }
120 
121         if (intent.getAction().equals(INTENT_ACTION_START_TRACING)) {
122             startTracingInternal(intent.getStringArrayListExtra(INTENT_EXTRA_TAGS),
123                 intent.getIntExtra(INTENT_EXTRA_BUFFER,
124                     Integer.parseInt(context.getString(R.string.default_buffer_size))),
125                 intent.getBooleanExtra(INTENT_EXTRA_APPS, false),
126                 intent.getBooleanExtra(INTENT_EXTRA_LONG_TRACE, false),
127                 intent.getIntExtra(INTENT_EXTRA_LONG_TRACE_SIZE,
128                     Integer.parseInt(context.getString(R.string.default_long_trace_size))),
129                 intent.getIntExtra(INTENT_EXTRA_LONG_TRACE_DURATION,
130                     Integer.parseInt(context.getString(R.string.default_long_trace_duration))));
131         } else if (intent.getAction().equals(INTENT_ACTION_STOP_TRACING)) {
132             stopTracingInternal(TraceUtils.getOutputFilename(), false, false);
133         } else if (intent.getAction().equals(INTENT_ACTION_NOTIFY_SESSION_STOPPED)) {
134             stopTracingInternal(TraceUtils.getOutputFilename(), true, false);
135         } else if (intent.getAction().equals(INTENT_ACTION_NOTIFY_SESSION_STOLEN)) {
136             stopTracingInternal("", false, true);
137         }
138     }
139 
startTracingInternal(Collection<String> tags, int bufferSizeKb, boolean appTracing, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)140     private void startTracingInternal(Collection<String> tags, int bufferSizeKb, boolean appTracing,
141             boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes) {
142         Context context = getApplicationContext();
143         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
144         Intent stopIntent = new Intent(Receiver.STOP_ACTION,
145             null, context, Receiver.class);
146         stopIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
147 
148         String title = context.getString(R.string.trace_is_being_recorded);
149         String msg = context.getString(R.string.tap_to_stop_tracing);
150 
151         boolean attachToBugreport =
152                 prefs.getBoolean(context.getString(R.string.pref_key_attach_to_bugreport), true);
153 
154         Notification.Builder notification =
155             new Notification.Builder(context, Receiver.NOTIFICATION_CHANNEL_TRACING)
156                 .setSmallIcon(R.drawable.bugfood_icon)
157                 .setContentTitle(title)
158                 .setTicker(title)
159                 .setContentText(msg)
160                 .setContentIntent(
161                     PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE))
162                 .setOngoing(true)
163                 .setLocalOnly(true)
164                 .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
165                 .setColor(getColor(
166                     com.android.internal.R.color.system_notification_accent_color));
167 
168         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
169             notification.extend(new Notification.TvExtender());
170         }
171 
172         startForeground(TRACE_NOTIFICATION, notification.build());
173 
174         if (TraceUtils.traceStart(tags, bufferSizeKb, appTracing,
175                 longTrace, attachToBugreport, maxLongTraceSizeMb, maxLongTraceDurationMinutes)) {
176             stopForeground(Service.STOP_FOREGROUND_DETACH);
177         } else {
178             // Starting the trace was unsuccessful, so ensure that tracing
179             // is stopped and the preference is reset.
180             TraceUtils.traceStop();
181             prefs.edit().putBoolean(context.getString(R.string.pref_key_tracing_on),
182                         false).commit();
183             QsService.updateTile();
184             stopForeground(Service.STOP_FOREGROUND_REMOVE);
185         }
186     }
187 
stopTracingInternal(String outputFilename, boolean forceStop, boolean sessionStolen)188     private void stopTracingInternal(String outputFilename, boolean forceStop,
189             boolean sessionStolen) {
190         Context context = getApplicationContext();
191         NotificationManager notificationManager =
192             getSystemService(NotificationManager.class);
193 
194         Notification.Builder notification;
195         if (sessionStolen) {
196             notification = getBaseTraceurNotification()
197                 .setContentTitle(getString(R.string.attaching_to_report))
198                 .setTicker(getString(R.string.attaching_to_report))
199                 .setProgress(1, 0, true);
200         } else {
201             notification = getBaseTraceurNotification()
202                 .setContentTitle(getString(R.string.saving_trace))
203                 .setTicker(getString(R.string.saving_trace))
204                 .setProgress(1, 0, true);
205         }
206 
207         startForeground(SAVING_TRACE_NOTIFICATION, notification.build());
208 
209         notificationManager.cancel(TRACE_NOTIFICATION);
210 
211         if (sessionStolen) {
212             Notification.Builder notificationAttached = getBaseTraceurNotification()
213                 .setContentTitle(getString(R.string.attached_to_report))
214                 .setTicker(getString(R.string.attached_to_report))
215                 .setAutoCancel(true);
216 
217             Intent openIntent =
218                     getPackageManager().getLaunchIntentForPackage(BETTERBUG_PACKAGE_NAME);
219             if (openIntent != null) {
220                 // Add "Tap to open BetterBug" to notification only if intent is non-null.
221                 notificationAttached.setContentText(getString(
222                         R.string.attached_to_report_summary));
223                 notificationAttached.setContentIntent(PendingIntent.getActivity(
224                         context, 0, openIntent, PendingIntent.FLAG_ONE_SHOT
225                                 | PendingIntent.FLAG_CANCEL_CURRENT
226                                 | PendingIntent.FLAG_IMMUTABLE));
227             }
228 
229             // Adds an action button to the notification for starting a new trace.
230             Intent restartIntent = new Intent(context, InternalReceiver.class);
231             restartIntent.setAction(InternalReceiver.START_ACTION);
232             PendingIntent restartPendingIntent = PendingIntent.getBroadcast(context, 0,
233                     restartIntent, PendingIntent.FLAG_ONE_SHOT
234                             | PendingIntent.FLAG_CANCEL_CURRENT
235                             | PendingIntent.FLAG_IMMUTABLE);
236             Notification.Action action = new Notification.Action.Builder(
237                     R.drawable.bugfood_icon, context.getString(R.string.start_new_trace),
238                     restartPendingIntent).build();
239             notificationAttached.addAction(action);
240 
241             NotificationManager.from(context).notify(0, notificationAttached.build());
242         } else {
243             File file = TraceUtils.getOutputFile(outputFilename);
244 
245             if (TraceUtils.traceDump(file)) {
246                 FileSender.postNotification(getApplicationContext(), file);
247             }
248         }
249 
250         stopForeground(Service.STOP_FOREGROUND_REMOVE);
251 
252         TraceUtils.cleanupOlderFiles(MIN_KEEP_COUNT, MIN_KEEP_AGE);
253     }
254 
getBaseTraceurNotification()255     private Notification.Builder getBaseTraceurNotification() {
256         Context context = getApplicationContext();
257         Notification.Builder notification =
258                 new Notification.Builder(this, Receiver.NOTIFICATION_CHANNEL_OTHER)
259                     .setSmallIcon(R.drawable.bugfood_icon)
260                     .setLocalOnly(true)
261                     .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
262                     .setColor(context.getColor(
263                             com.android.internal.R.color.system_notification_accent_color));
264 
265         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
266             notification.extend(new Notification.TvExtender());
267         }
268 
269         return notification;
270     }
271 }
272