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