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 android.content.ClipData;
20 import android.content.ClipDescription;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.net.Uri;
24 import android.os.Build;
25 import android.util.Log;
26 
27 import androidx.core.content.FileProvider;
28 
29 import java.io.BufferedInputStream;
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.zip.ZipEntry;
38 import java.util.zip.ZipOutputStream;
39 
40 /**
41  * Utility class for dumping, compressing, sending, and serving heap dump files.
42  *
43  * <p>Unlike the Internet, this IS a big truck you can dump something on.
44  */
45 public class DumpTruck {
46     private static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
47     private static final String FILEPROVIDER_PATH = "leak";
48 
49     private static final String TAG = "DumpTruck";
50     private static final int BUFSIZ = 1024 * 1024; // 1MB
51 
52     private final Context context;
53     private final GarbageMonitor mGarbageMonitor;
54     private Uri hprofUri;
55     private long rss;
56     final StringBuilder body = new StringBuilder();
57 
DumpTruck(Context context, GarbageMonitor garbageMonitor)58     public DumpTruck(Context context, GarbageMonitor garbageMonitor) {
59         this.context = context;
60         mGarbageMonitor = garbageMonitor;
61     }
62 
63     /**
64      * Capture memory for the given processes and zip them up for sharing.
65      *
66      * @param pids
67      * @return this, for chaining
68      */
captureHeaps(List<Long> pids)69     public DumpTruck captureHeaps(List<Long> pids) {
70         final File dumpDir = new File(context.getCacheDir(), FILEPROVIDER_PATH);
71         dumpDir.mkdirs();
72         hprofUri = null;
73 
74         body.setLength(0);
75         body.append("Build: ").append(Build.DISPLAY).append("\n\nProcesses:\n");
76 
77         final ArrayList<String> paths = new ArrayList<String>();
78         final int myPid = android.os.Process.myPid();
79 
80         for (Long pidL : pids) {
81             final int pid = pidL.intValue();
82             body.append("  pid ").append(pid);
83             GarbageMonitor.ProcessMemInfo info = mGarbageMonitor.getMemInfo(pid);
84             if (info != null) {
85                 body.append(":")
86                         .append(" up=")
87                         .append(info.getUptime())
88                         .append(" rss=")
89                         .append(info.currentRss);
90                 rss = info.currentRss;
91             }
92             if (pid == myPid) {
93                 final String path =
94                         new File(dumpDir, String.format("heap-%d.ahprof", pid)).getPath();
95                 Log.v(TAG, "Dumping memory info for process " + pid + " to " + path);
96                 try {
97                     android.os.Debug.dumpHprofData(path); // will block
98                     paths.add(path);
99                     body.append(" (hprof attached)");
100                 } catch (IOException e) {
101                     Log.e(TAG, "error dumping memory:", e);
102                     body.append("\n** Could not dump heap: \n").append(e.toString()).append("\n");
103                 }
104             }
105             body.append("\n");
106         }
107 
108         try {
109             final String zipfile =
110                     new File(dumpDir, String.format("hprof-%d.zip", System.currentTimeMillis()))
111                             .getCanonicalPath();
112             if (DumpTruck.zipUp(zipfile, paths)) {
113                 final File pathFile = new File(zipfile);
114                 hprofUri = FileProvider.getUriForFile(context, FILEPROVIDER_AUTHORITY, pathFile);
115                 Log.v(TAG, "Heap dump accessible at URI: " + hprofUri);
116             }
117         } catch (IOException e) {
118             Log.e(TAG, "unable to zip up heapdumps", e);
119             body.append("\n** Could not zip up files: \n").append(e.toString()).append("\n");
120         }
121 
122         return this;
123     }
124 
125     /**
126      * Get the Uri of the current heap dump. Be sure to call captureHeaps first.
127      *
128      * @return Uri to the dump served by the SystemUI file provider
129      */
getDumpUri()130     public Uri getDumpUri() {
131         return hprofUri;
132     }
133 
134     /**
135      * Get an ACTION_SEND intent suitable for startActivity() or attaching to a Notification.
136      *
137      * @return share intent
138      */
createShareIntent()139     public Intent createShareIntent() {
140         Intent shareIntent = new Intent(Intent.ACTION_SEND_MULTIPLE);
141         shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
142         shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
143         shareIntent.putExtra(Intent.EXTRA_SUBJECT,
144                 String.format("SystemUI memory dump (rss=%dM)", rss / 1024));
145 
146         shareIntent.putExtra(Intent.EXTRA_TEXT, body.toString());
147 
148         if (hprofUri != null) {
149             final ArrayList<Uri> uriList = new ArrayList<>();
150             uriList.add(hprofUri);
151             shareIntent.setType("application/zip");
152             shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList);
153 
154             // Include URI in ClipData also, so that grantPermission picks it up.
155             // We don't use setData here because some apps interpret this as "to:".
156             ClipData clipdata = new ClipData(new ClipDescription("content",
157                     new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
158                     new ClipData.Item(hprofUri));
159             shareIntent.setClipData(clipdata);
160             shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
161         }
162         return shareIntent;
163     }
164 
zipUp(String zipfilePath, ArrayList<String> paths)165     private static boolean zipUp(String zipfilePath, ArrayList<String> paths) {
166         try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipfilePath))) {
167             final byte[] buf = new byte[BUFSIZ];
168 
169             for (String filename : paths) {
170                 try (InputStream is = new BufferedInputStream(new FileInputStream(filename))) {
171                     ZipEntry entry = new ZipEntry(filename);
172                     zos.putNextEntry(entry);
173                     int len;
174                     while (0 < (len = is.read(buf, 0, BUFSIZ))) {
175                         zos.write(buf, 0, len);
176                     }
177                     zos.closeEntry();
178                 }
179             }
180             return true;
181         } catch (IOException e) {
182             Log.e(TAG, "error zipping up profile data", e);
183         }
184         return false;
185     }
186 }
187