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