1 /*
2  * Copyright (C) 2017 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.systemui.util.leak;
18 
19 import static com.android.systemui.Dependency.LEAK_REPORT_EMAIL_NAME;
20 
21 import android.app.Notification;
22 import android.app.NotificationChannel;
23 import android.app.NotificationManager;
24 import android.app.PendingIntent;
25 import android.content.ClipData;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.net.Uri;
29 import android.os.Debug;
30 import android.os.SystemProperties;
31 import android.text.TextUtils;
32 import android.util.Log;
33 
34 import androidx.core.content.FileProvider;
35 
36 import com.android.systemui.dagger.SysUISingleton;
37 import com.android.systemui.settings.UserTracker;
38 
39 import com.google.android.collect.Lists;
40 
41 import java.io.File;
42 import java.io.FileOutputStream;
43 import java.io.IOException;
44 import java.io.PrintWriter;
45 import java.util.ArrayList;
46 
47 import javax.inject.Inject;
48 import javax.inject.Named;
49 
50 /**
51  * Dumps data to debug leaks and posts a notification to share the data.
52  */
53 @SysUISingleton
54 public class LeakReporter {
55 
56     static final String TAG = "LeakReporter";
57 
58     public static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
59 
60     static final String LEAK_DIR = "leak";
61     static final String LEAK_HPROF = "leak.hprof";
62     static final String LEAK_DUMP = "leak.dump";
63 
64     private final Context mContext;
65     private final UserTracker mUserTracker;
66     private final LeakDetector mLeakDetector;
67     private final String mLeakReportEmail;
68 
69     @Inject
LeakReporter(Context context, UserTracker userTracker, LeakDetector leakDetector, @Named(LEAK_REPORT_EMAIL_NAME) String leakReportEmail)70     public LeakReporter(Context context, UserTracker userTracker, LeakDetector leakDetector,
71             @Named(LEAK_REPORT_EMAIL_NAME) String leakReportEmail) {
72         mContext = context;
73         mUserTracker = userTracker;
74         mLeakDetector = leakDetector;
75         mLeakReportEmail = leakReportEmail;
76     }
77 
dumpLeak(int garbageCount)78     public void dumpLeak(int garbageCount) {
79         try {
80             File leakDir = new File(mContext.getCacheDir(), LEAK_DIR);
81             leakDir.mkdir();
82 
83             File hprofFile = new File(leakDir, LEAK_HPROF);
84             Debug.dumpHprofData(hprofFile.getAbsolutePath());
85 
86             File dumpFile = new File(leakDir, LEAK_DUMP);
87             try (FileOutputStream fos = new FileOutputStream(dumpFile)) {
88                 PrintWriter w = new PrintWriter(fos);
89                 w.print("Build: "); w.println(SystemProperties.get("ro.build.description"));
90                 w.println();
91                 w.flush();
92                 mLeakDetector.dump(w, new String[0]);
93                 w.close();
94             }
95 
96             NotificationManager notiMan = mContext.getSystemService(NotificationManager.class);
97 
98             NotificationChannel channel = new NotificationChannel("leak", "Leak Alerts",
99                     NotificationManager.IMPORTANCE_HIGH);
100             channel.enableVibration(true);
101 
102             notiMan.createNotificationChannel(channel);
103             Notification.Builder builder = new Notification.Builder(mContext, channel.getId())
104                     .setAutoCancel(true)
105                     .setShowWhen(true)
106                     .setContentTitle("Memory Leak Detected")
107                     .setContentText(String.format(
108                             "SystemUI has detected %d leaked objects. Tap to send", garbageCount))
109                     .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
110                     .setContentIntent(PendingIntent.getActivityAsUser(
111                             mContext,
112                             0,
113                             getIntent(hprofFile, dumpFile),
114                             PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE,
115                             null,
116                             mUserTracker.getUserHandle()));
117             notiMan.notify(TAG, 0, builder.build());
118         } catch (IOException e) {
119             Log.e(TAG, "Couldn't dump heap for leak", e);
120         }
121     }
122 
getIntent(File hprofFile, File dumpFile)123     private Intent getIntent(File hprofFile, File dumpFile) {
124         Uri dumpUri = FileProvider.getUriForFile(mContext, FILEPROVIDER_AUTHORITY, dumpFile);
125         Uri hprofUri = FileProvider.getUriForFile(mContext, FILEPROVIDER_AUTHORITY, hprofFile);
126 
127         Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
128         String mimeType = "application/vnd.android.leakreport";
129 
130         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
131         intent.addCategory(Intent.CATEGORY_DEFAULT);
132         intent.setType(mimeType);
133 
134         final String subject = "SystemUI leak report";
135         intent.putExtra(Intent.EXTRA_SUBJECT, subject);
136 
137         // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
138         // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
139         // create the ClipData object with the attachments URIs.
140         final StringBuilder messageBody = new StringBuilder("Build info: ")
141                 .append(SystemProperties.get("ro.build.description"));
142         intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
143         final ClipData clipData = new ClipData(null, new String[] { mimeType },
144                 new ClipData.Item(null, null, null, dumpUri));
145         final ArrayList<Uri> attachments = Lists.newArrayList(dumpUri);
146 
147         clipData.addItem(new ClipData.Item(null, null, null, hprofUri));
148         attachments.add(hprofUri);
149 
150         intent.setClipData(clipData);
151         intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
152 
153         if (!TextUtils.isEmpty(mLeakReportEmail)) {
154             intent.putExtra(Intent.EXTRA_EMAIL, new String[] { mLeakReportEmail });
155         }
156 
157         return intent;
158     }
159 }
160