/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.util.leak; import android.content.ClipData; import android.content.ClipDescription; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.util.Log; import androidx.core.content.FileProvider; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * Utility class for dumping, compressing, sending, and serving heap dump files. * *

Unlike the Internet, this IS a big truck you can dump something on. */ public class DumpTruck { private static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider"; private static final String FILEPROVIDER_PATH = "leak"; private static final String TAG = "DumpTruck"; private static final int BUFSIZ = 1024 * 1024; // 1MB private final Context context; private final GarbageMonitor mGarbageMonitor; private Uri hprofUri; private long rss; final StringBuilder body = new StringBuilder(); public DumpTruck(Context context, GarbageMonitor garbageMonitor) { this.context = context; mGarbageMonitor = garbageMonitor; } /** * Capture memory for the given processes and zip them up for sharing. * * @param pids * @return this, for chaining */ public DumpTruck captureHeaps(List pids) { final File dumpDir = new File(context.getCacheDir(), FILEPROVIDER_PATH); dumpDir.mkdirs(); hprofUri = null; body.setLength(0); body.append("Build: ").append(Build.DISPLAY).append("\n\nProcesses:\n"); final ArrayList paths = new ArrayList(); final int myPid = android.os.Process.myPid(); for (Long pidL : pids) { final int pid = pidL.intValue(); body.append(" pid ").append(pid); GarbageMonitor.ProcessMemInfo info = mGarbageMonitor.getMemInfo(pid); if (info != null) { body.append(":") .append(" up=") .append(info.getUptime()) .append(" rss=") .append(info.currentRss); rss = info.currentRss; } if (pid == myPid) { final String path = new File(dumpDir, String.format("heap-%d.ahprof", pid)).getPath(); Log.v(TAG, "Dumping memory info for process " + pid + " to " + path); try { android.os.Debug.dumpHprofData(path); // will block paths.add(path); body.append(" (hprof attached)"); } catch (IOException e) { Log.e(TAG, "error dumping memory:", e); body.append("\n** Could not dump heap: \n").append(e.toString()).append("\n"); } } body.append("\n"); } try { final String zipfile = new File(dumpDir, String.format("hprof-%d.zip", System.currentTimeMillis())) .getCanonicalPath(); if (DumpTruck.zipUp(zipfile, paths)) { final File pathFile = new File(zipfile); hprofUri = FileProvider.getUriForFile(context, FILEPROVIDER_AUTHORITY, pathFile); Log.v(TAG, "Heap dump accessible at URI: " + hprofUri); } } catch (IOException e) { Log.e(TAG, "unable to zip up heapdumps", e); body.append("\n** Could not zip up files: \n").append(e.toString()).append("\n"); } return this; } /** * Get the Uri of the current heap dump. Be sure to call captureHeaps first. * * @return Uri to the dump served by the SystemUI file provider */ public Uri getDumpUri() { return hprofUri; } /** * Get an ACTION_SEND intent suitable for startActivity() or attaching to a Notification. * * @return share intent */ public Intent createShareIntent() { Intent shareIntent = new Intent(Intent.ACTION_SEND_MULTIPLE); shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); shareIntent.putExtra(Intent.EXTRA_SUBJECT, String.format("SystemUI memory dump (rss=%dM)", rss / 1024)); shareIntent.putExtra(Intent.EXTRA_TEXT, body.toString()); if (hprofUri != null) { final ArrayList uriList = new ArrayList<>(); uriList.add(hprofUri); shareIntent.setType("application/zip"); shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList); // Include URI in ClipData also, so that grantPermission picks it up. // We don't use setData here because some apps interpret this as "to:". ClipData clipdata = new ClipData(new ClipDescription("content", new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}), new ClipData.Item(hprofUri)); shareIntent.setClipData(clipdata); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } return shareIntent; } private static boolean zipUp(String zipfilePath, ArrayList paths) { try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipfilePath))) { final byte[] buf = new byte[BUFSIZ]; for (String filename : paths) { try (InputStream is = new BufferedInputStream(new FileInputStream(filename))) { ZipEntry entry = new ZipEntry(filename); zos.putNextEntry(entry); int len; while (0 < (len = is.read(buf, 0, BUFSIZ))) { zos.write(buf, 0, len); } zos.closeEntry(); } } return true; } catch (IOException e) { Log.e(TAG, "error zipping up profile data", e); } return false; } }