SystemUI heap monitor & hprof dump tile.

Adding the tile to your active set will start a continuous memory tracker,
updating the tile every minute with the latest pss value. Tapping the tile
will pull a full heap dump, zip it, and share it via ACTION_SEND.

Additionally, @integer/watch_heap_limit can be set to the heap size (in KB)
at which ActivityManager will automatically generate a heap dump and notify
the user when it's ready.

Available only on IS_DEBUGGABLE builds. Supplies limited. Order now!

Bug: 76208386
Test: atest com.android.systemui.util.leak.GarbageMonitorTest
Test: runtest systemui
Change-Id: I0434dd7dc330784c750469b013e40b31ef92638d
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 7d19784..beb3c53 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -247,10 +247,14 @@
                 getDependency(LeakDetector.class),
                 getDependency(LEAK_REPORT_EMAIL)));
 
-        mProviders.put(GarbageMonitor.class, () -> new GarbageMonitor(
-                getDependency(BG_LOOPER),
-                getDependency(LeakDetector.class),
-                getDependency(LeakReporter.class)));
+        mProviders.put(
+                GarbageMonitor.class,
+                () ->
+                        new GarbageMonitor(
+                                mContext,
+                                getDependency(BG_LOOPER),
+                                getDependency(LeakDetector.class),
+                                getDependency(LeakReporter.class)));
 
         mProviders.put(TunerService.class, () ->
                 new TunerServiceImpl(mContext));
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index 9593b0f..53a576d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -24,6 +24,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Handler;
 import android.service.quicksettings.TileService;
 import android.text.TextUtils;
@@ -37,8 +38,10 @@
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
+import com.android.systemui.util.leak.GarbageMonitor;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 
@@ -68,7 +71,6 @@
         // Enqueue jobs to fetch every system tile and then ever package tile.
         addStockTiles(host);
         addPackageTiles(host);
-        // TODO: Live?
     }
 
     public boolean isFinished() {
@@ -77,10 +79,14 @@
 
     private void addStockTiles(QSTileHost host) {
         String possible = mContext.getString(R.string.quick_settings_tiles_stock);
-        String[] possibleTiles = possible.split(",");
+        final ArrayList<String> possibleTiles = new ArrayList<>();
+        possibleTiles.addAll(Arrays.asList(possible.split(",")));
+        if (Build.IS_DEBUGGABLE) {
+            possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
+        }
+
         final ArrayList<QSTile> tilesToAdd = new ArrayList<>();
-        for (int i = 0; i < possibleTiles.length; i++) {
-            final String spec = possibleTiles[i];
+        for (String spec : possibleTiles) {
             final QSTile tile = host.createTile(spec);
             if (tile == null) {
                 continue;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index 8d48890..ac7ef5d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -15,6 +15,7 @@
 package com.android.systemui.qs.tileimpl;
 
 import android.content.Context;
+import android.os.Build;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
 
@@ -41,6 +42,7 @@
 import com.android.systemui.qs.tiles.WifiTile;
 import com.android.systemui.qs.tiles.WorkModeTile;
 import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.util.leak.GarbageMonitor;
 
 public class QSFactoryImpl implements QSFactory {
 
@@ -60,30 +62,58 @@
     }
 
     private QSTileImpl createTileInternal(String tileSpec) {
-        if (tileSpec.equals("wifi")) return new WifiTile(mHost);
-        else if (tileSpec.equals("bt")) return new BluetoothTile(mHost);
-        else if (tileSpec.equals("cell")) return new CellularTile(mHost);
-        else if (tileSpec.equals("dnd")) return new DndTile(mHost);
-        else if (tileSpec.equals("inversion")) return new ColorInversionTile(mHost);
-        else if (tileSpec.equals("airplane")) return new AirplaneModeTile(mHost);
-        else if (tileSpec.equals("work")) return new WorkModeTile(mHost);
-        else if (tileSpec.equals("rotation")) return new RotationLockTile(mHost);
-        else if (tileSpec.equals("flashlight")) return new FlashlightTile(mHost);
-        else if (tileSpec.equals("location")) return new LocationTile(mHost);
-        else if (tileSpec.equals("cast")) return new CastTile(mHost);
-        else if (tileSpec.equals("hotspot")) return new HotspotTile(mHost);
-        else if (tileSpec.equals("user")) return new UserTile(mHost);
-        else if (tileSpec.equals("battery")) return new BatterySaverTile(mHost);
-        else if (tileSpec.equals("saver")) return new DataSaverTile(mHost);
-        else if (tileSpec.equals("night")) return new NightDisplayTile(mHost);
-        else if (tileSpec.equals("nfc")) return new NfcTile(mHost);
-        // Intent tiles.
-        else if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(mHost, tileSpec);
-        else if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(mHost, tileSpec);
-        else {
-            Log.w(TAG, "Bad tile spec: " + tileSpec);
-            return null;
+        // Stock tiles.
+        switch (tileSpec) {
+            case "wifi":
+                return new WifiTile(mHost);
+            case "bt":
+                return new BluetoothTile(mHost);
+            case "cell":
+                return new CellularTile(mHost);
+            case "dnd":
+                return new DndTile(mHost);
+            case "inversion":
+                return new ColorInversionTile(mHost);
+            case "airplane":
+                return new AirplaneModeTile(mHost);
+            case "work":
+                return new WorkModeTile(mHost);
+            case "rotation":
+                return new RotationLockTile(mHost);
+            case "flashlight":
+                return new FlashlightTile(mHost);
+            case "location":
+                return new LocationTile(mHost);
+            case "cast":
+                return new CastTile(mHost);
+            case "hotspot":
+                return new HotspotTile(mHost);
+            case "user":
+                return new UserTile(mHost);
+            case "battery":
+                return new BatterySaverTile(mHost);
+            case "saver":
+                return new DataSaverTile(mHost);
+            case "night":
+                return new NightDisplayTile(mHost);
+            case "nfc":
+                return new NfcTile(mHost);
         }
+
+        // Intent tiles.
+        if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(mHost, tileSpec);
+        if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(mHost, tileSpec);
+
+        // Debug tiles.
+        if (Build.IS_DEBUGGABLE) {
+            if (tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC)) {
+                return new GarbageMonitor.MemoryTile(mHost);
+            }
+        }
+
+        // Broken tiles.
+        Log.w(TAG, "Bad tile spec: " + tileSpec);
+        return null;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
new file mode 100644
index 0000000..2995eba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
@@ -0,0 +1,175 @@
+/*
+ * 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.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.support.v4.content.FileProvider;
+import android.util.Log;
+
+import com.android.systemui.Dependency;
+
+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.Arrays;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Utility class for dumping, compressing, sending, and serving heap dump files.
+ *
+ * <p>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 = 512 * 1024; // 512K
+
+    private final Context context;
+    private Uri hprofUri;
+    final StringBuilder body = new StringBuilder();
+
+    public DumpTruck(Context context) {
+        this.context = context;
+    }
+
+    /**
+     * Capture memory for the given processes and zip them up for sharing.
+     *
+     * @param pids
+     * @return this, for chaining
+     */
+    public DumpTruck captureHeaps(int[] pids) {
+        final GarbageMonitor gm = Dependency.get(GarbageMonitor.class);
+
+        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<String> paths = new ArrayList<String>();
+        final int myPid = android.os.Process.myPid();
+
+        final int[] pids_copy = Arrays.copyOf(pids, pids.length);
+        for (int pid : pids_copy) {
+            body.append("  pid ").append(pid);
+            if (gm != null) {
+                GarbageMonitor.ProcessMemInfo info = gm.getMemInfo(pid);
+                if (info != null) {
+                    body.append(":")
+                            .append(" up=")
+                            .append(info.getUptime())
+                            .append(" pss=")
+                            .append(info.currentPss)
+                            .append(" uss=")
+                            .append(info.currentUss);
+                }
+            }
+            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);
+            }
+        } 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);
+        shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        shareIntent.putExtra(Intent.EXTRA_SUBJECT, "SystemUI memory dump");
+
+        shareIntent.putExtra(Intent.EXTRA_TEXT, body.toString());
+
+        if (hprofUri != null) {
+            shareIntent.setType("application/zip");
+            shareIntent.putExtra(Intent.EXTRA_STREAM, hprofUri);
+        }
+        return shareIntent;
+    }
+
+    private static boolean zipUp(String zipfilePath, ArrayList<String> 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;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
index 021f9c4..b2cc269 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
@@ -16,88 +16,469 @@
 
 package com.android.systemui.util.leak;
 
+import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN;
 
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.os.Build;
+import android.os.Debug;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
 import android.os.SystemProperties;
 import android.provider.Settings;
-import android.support.annotation.VisibleForTesting;
+import android.service.quicksettings.Tile;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.LongSparseArray;
 
 import com.android.systemui.Dependency;
+import com.android.systemui.R;
 import com.android.systemui.SystemUI;
+import com.android.systemui.plugins.qs.QSTile;
+import com.android.systemui.qs.QSHost;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
+
+import java.util.ArrayList;
 
 public class GarbageMonitor {
+    private static final boolean LEAK_REPORTING_ENABLED =
+            Build.IS_DEBUGGABLE
+                    && SystemProperties.getBoolean("debug.enable_leak_reporting", false);
+    private static final String FORCE_ENABLE_LEAK_REPORTING = "sysui_force_enable_leak_reporting";
+
+    private static final boolean HEAP_TRACKING_ENABLED = Build.IS_DEBUGGABLE;
+    private static final boolean ENABLE_AM_HEAP_LIMIT = true; // use ActivityManager.setHeapLimit
 
     private static final String TAG = "GarbageMonitor";
 
-    private static final long GARBAGE_INSPECTION_INTERVAL = 5 * 60 * 1000; // 5min
+    private static final long GARBAGE_INSPECTION_INTERVAL =
+            15 * DateUtils.MINUTE_IN_MILLIS; // 15 min
+    private static final long HEAP_TRACK_INTERVAL = 1 * DateUtils.MINUTE_IN_MILLIS; // 1 min
+
+    private static final int DO_GARBAGE_INSPECTION = 1000;
+    private static final int DO_HEAP_TRACK = 3000;
+
     private static final int GARBAGE_ALLOWANCE = 5;
 
     private final Handler mHandler;
     private final TrackedGarbage mTrackedGarbage;
     private final LeakReporter mLeakReporter;
+    private final Context mContext;
+    private final ActivityManager mAm;
+    private MemoryTile mQSTile;
+    private DumpTruck mDumpTruck;
 
-    public GarbageMonitor(Looper bgLooper, LeakDetector leakDetector,
+    private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
+    private final ArrayList<Long> mPids = new ArrayList<>();
+    private int[] mPidsArray = new int[1];
+
+    private long mHeapLimit;
+
+    public GarbageMonitor(
+            Context context,
+            Looper bgLooper,
+            LeakDetector leakDetector,
             LeakReporter leakReporter) {
-        mHandler = bgLooper != null ? new Handler(bgLooper): null;
+        mContext = context.getApplicationContext();
+        mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+        mHandler = new BackgroundHeapCheckHandler(bgLooper);
+
         mTrackedGarbage = leakDetector.getTrackedGarbage();
         mLeakReporter = leakReporter;
+
+        mDumpTruck = new DumpTruck(mContext);
+
+        if (ENABLE_AM_HEAP_LIMIT) {
+            mHeapLimit = mContext.getResources().getInteger(R.integer.watch_heap_limit);
+        }
     }
 
-    public void start() {
+    public void startLeakMonitor() {
         if (mTrackedGarbage == null) {
             return;
         }
 
-        scheduleInspectGarbage(this::inspectGarbage);
+        mHandler.sendEmptyMessage(DO_GARBAGE_INSPECTION);
     }
 
-    @VisibleForTesting
-    void scheduleInspectGarbage(Runnable runnable) {
-        mHandler.postDelayed(runnable, GARBAGE_INSPECTION_INTERVAL);
+    public void startHeapTracking() {
+        startTrackingProcess(
+                android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
+        mHandler.sendEmptyMessage(DO_HEAP_TRACK);
     }
 
-    private void inspectGarbage() {
+    private boolean gcAndCheckGarbage() {
         if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
             Runtime.getRuntime().gc();
-
-            // Allow some time to for ReferenceQueue to catch up.
-            scheduleReinspectGarbage(this::reinspectGarbageAfterGc);
+            return true;
         }
-        scheduleInspectGarbage(this::inspectGarbage);
+        return false;
     }
 
-    @VisibleForTesting
-    void scheduleReinspectGarbage(Runnable runnable) {
-        mHandler.postDelayed(runnable, (long) 100);
-    }
-
-    private void reinspectGarbageAfterGc() {
+    void reinspectGarbageAfterGc() {
         int count = mTrackedGarbage.countOldGarbage();
         if (count > GARBAGE_ALLOWANCE) {
             mLeakReporter.dumpLeak(count);
         }
     }
 
+    public ProcessMemInfo getMemInfo(int pid) {
+        return mData.get(pid);
+    }
+
+    public int[] getTrackedProcesses() {
+        return mPidsArray;
+    }
+
+    public void startTrackingProcess(long pid, String name, long start) {
+        synchronized (mPids) {
+            if (mPids.contains(pid)) return;
+
+            mPids.add(pid);
+            updatePidsArrayL();
+
+            mData.put(pid, new ProcessMemInfo(pid, name, start));
+        }
+    }
+
+    private void updatePidsArrayL() {
+        final int N = mPids.size();
+        mPidsArray = new int[N];
+        StringBuffer sb = new StringBuffer("Now tracking processes: ");
+        for (int i = 0; i < N; i++) {
+            final int p = mPids.get(i).intValue();
+            mPidsArray[i] = p;
+            sb.append(p);
+            sb.append(" ");
+        }
+        Log.v(TAG, sb.toString());
+    }
+
+    private void update() {
+        synchronized (mPids) {
+            Debug.MemoryInfo[] dinfos = mAm.getProcessMemoryInfo(mPidsArray);
+            for (int i = 0; i < dinfos.length; i++) {
+                Debug.MemoryInfo dinfo = dinfos[i];
+                if (i > mPids.size()) {
+                    Log.e(TAG, "update: unknown process info received: " + dinfo);
+                    break;
+                }
+                final long pid = mPids.get(i).intValue();
+                final ProcessMemInfo info = mData.get(pid);
+                info.head = (info.head + 1) % info.pss.length;
+                info.pss[info.head] = info.currentPss = dinfo.getTotalPss();
+                info.uss[info.head] = info.currentUss = dinfo.getTotalPrivateDirty();
+                if (info.currentPss > info.max) info.max = info.currentPss;
+                if (info.currentUss > info.max) info.max = info.currentUss;
+                if (info.currentPss == 0) {
+                    Log.v(TAG, "update: pid " + pid + " has pss=0, it probably died");
+                    mData.remove(pid);
+                }
+            }
+            for (int i = mPids.size() - 1; i >= 0; i--) {
+                final long pid = mPids.get(i).intValue();
+                if (mData.get(pid) == null) {
+                    mPids.remove(i);
+                    updatePidsArrayL();
+                }
+            }
+        }
+        if (mQSTile != null) mQSTile.update();
+    }
+
+    private void setTile(MemoryTile tile) {
+        mQSTile = tile;
+        if (tile != null) tile.update();
+    }
+
+    private static String formatBytes(long b) {
+        String[] SUFFIXES = {"B", "K", "M", "G", "T"};
+        int i;
+        for (i = 0; i < SUFFIXES.length; i++) {
+            if (b < 1024) break;
+            b /= 1024;
+        }
+        return b + SUFFIXES[i];
+    }
+
+    private void dumpHprofAndShare() {
+        final Intent share = mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent();
+        mContext.startActivity(share);
+    }
+
+    private static class MemoryIconDrawable extends Drawable {
+        long pss, limit;
+        final Drawable baseIcon;
+        final Paint paint = new Paint();
+        final float dp;
+
+        MemoryIconDrawable(Context context) {
+            baseIcon = context.getDrawable(R.drawable.ic_memory).mutate();
+            dp = context.getResources().getDisplayMetrics().density;
+            paint.setColor(QSTileImpl.getColorForState(context, Tile.STATE_ACTIVE));
+        }
+
+        public void setPss(long pss) {
+            if (pss != this.pss) {
+                this.pss = pss;
+                invalidateSelf();
+            }
+        }
+
+        public void setLimit(long limit) {
+            if (limit != this.limit) {
+                this.limit = limit;
+                invalidateSelf();
+            }
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            baseIcon.draw(canvas);
+
+            if (limit > 0 && pss > 0) {
+                float frac = Math.min(1f, (float) pss / limit);
+
+                final Rect bounds = getBounds();
+                canvas.translate(bounds.left + 8 * dp, bounds.top + 5 * dp);
+                //android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
+                canvas.drawRect(0, 14 * dp * (1 - frac), 8 * dp + 1, 14 * dp + 1, paint);
+            }
+        }
+
+        @Override
+        public void setBounds(int left, int top, int right, int bottom) {
+            super.setBounds(left, top, right, bottom);
+            baseIcon.setBounds(left, top, right, bottom);
+        }
+
+        @Override
+        public int getIntrinsicHeight() {
+            return baseIcon.getIntrinsicHeight();
+        }
+
+        @Override
+        public int getIntrinsicWidth() {
+            return baseIcon.getIntrinsicWidth();
+        }
+
+        @Override
+        public void setAlpha(int i) {
+            baseIcon.setAlpha(i);
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+            baseIcon.setColorFilter(colorFilter);
+            paint.setColorFilter(colorFilter);
+        }
+
+        @Override
+        public void setTint(int tint) {
+            super.setTint(tint);
+            baseIcon.setTint(tint);
+        }
+
+        @Override
+        public void setTintList(ColorStateList tint) {
+            super.setTintList(tint);
+            baseIcon.setTintList(tint);
+        }
+
+        @Override
+        public void setTintMode(PorterDuff.Mode tintMode) {
+            super.setTintMode(tintMode);
+            baseIcon.setTintMode(tintMode);
+        }
+
+        @Override
+        public int getOpacity() {
+            return PixelFormat.TRANSLUCENT;
+        }
+    }
+
+    private static class MemoryGraphIcon extends QSTile.Icon {
+        long pss, limit;
+
+        public void setPss(long pss) {
+            this.pss = pss;
+        }
+
+        public void setHeapLimit(long limit) {
+            this.limit = limit;
+        }
+
+        @Override
+        public Drawable getDrawable(Context context) {
+            final MemoryIconDrawable drawable = new MemoryIconDrawable(context);
+            drawable.setPss(pss);
+            drawable.setLimit(limit);
+            return drawable;
+        }
+    }
+
+    public static class MemoryTile extends QSTileImpl<QSTile.State> {
+        public static final String TILE_SPEC = "dbg:mem";
+
+        private final GarbageMonitor gm;
+        private ProcessMemInfo pmi;
+
+        public MemoryTile(QSHost host) {
+            super(host);
+            gm = Dependency.get(GarbageMonitor.class);
+        }
+
+        @Override
+        public State newTileState() {
+            return new QSTile.State();
+        }
+
+        @Override
+        public Intent getLongClickIntent() {
+            return new Intent();
+        }
+
+        @Override
+        protected void handleClick() {
+            getHost().collapsePanels();
+            mHandler.post(gm::dumpHprofAndShare);
+        }
+
+        @Override
+        public int getMetricsCategory() {
+            return VIEW_UNKNOWN;
+        }
+
+        @Override
+        public void handleSetListening(boolean listening) {
+            if (gm != null) gm.setTile(listening ? this : null);
+
+            final ActivityManager am = mContext.getSystemService(ActivityManager.class);
+            if (listening && gm.mHeapLimit > 0) {
+                am.setWatchHeapLimit(1024 * gm.mHeapLimit); // why is this in bytes?
+            } else {
+                am.clearWatchHeapLimit();
+            }
+        }
+
+        @Override
+        public CharSequence getTileLabel() {
+            return getState().label;
+        }
+
+        @Override
+        protected void handleUpdateState(State state, Object arg) {
+            pmi = gm.getMemInfo(Process.myPid());
+            final MemoryGraphIcon icon = new MemoryGraphIcon();
+            icon.setHeapLimit(gm.mHeapLimit);
+            if (pmi != null) {
+                icon.setPss(pmi.currentPss);
+                state.label = mContext.getString(R.string.heap_dump_tile_name);
+                state.secondaryLabel =
+                        String.format(
+                                "pss: %s / %s",
+                                formatBytes(pmi.currentPss * 1024),
+                                formatBytes(gm.mHeapLimit * 1024));
+            } else {
+                icon.setPss(0);
+                state.label = "Dump SysUI";
+                state.secondaryLabel = null;
+            }
+            state.icon = icon;
+        }
+
+        public void update() {
+            refreshState();
+        }
+
+        public long getPss() {
+            return pmi != null ? pmi.currentPss : 0;
+        }
+
+        public long getHeapLimit() {
+            return gm != null ? gm.mHeapLimit : 0;
+        }
+    }
+
+    public static class ProcessMemInfo {
+        public long pid;
+        public String name;
+        public long startTime;
+        public long currentPss, currentUss;
+        public long[] pss = new long[256];
+        public long[] uss = new long[256];
+        public long max = 1;
+        public int head = 0;
+
+        public ProcessMemInfo(long pid, String name, long start) {
+            this.pid = pid;
+            this.name = name;
+            this.startTime = start;
+        }
+
+        public long getUptime() {
+            return System.currentTimeMillis() - startTime;
+        }
+    }
+
     public static class Service extends SystemUI {
-
-        // TODO(b/35345376): Turn this back on for debuggable builds after known leak fixed.
-        private static final boolean ENABLED = Build.IS_DEBUGGABLE
-                && SystemProperties.getBoolean("debug.enable_leak_reporting", false);
-        private static final String FORCE_ENABLE = "sysui_force_garbage_monitor";
-
         private GarbageMonitor mGarbageMonitor;
 
         @Override
         public void start() {
-            boolean forceEnable = Settings.Secure.getInt(mContext.getContentResolver(),
-                    FORCE_ENABLE, 0) != 0;
-            if (!ENABLED && !forceEnable) {
-                return;
-            }
+            boolean forceEnable =
+                    Settings.Secure.getInt(
+                                    mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0)
+                            != 0;
             mGarbageMonitor = Dependency.get(GarbageMonitor.class);
-            mGarbageMonitor.start();
+            if (LEAK_REPORTING_ENABLED || forceEnable) {
+                mGarbageMonitor.startLeakMonitor();
+            }
+            if (HEAP_TRACKING_ENABLED || forceEnable) {
+                mGarbageMonitor.startHeapTracking();
+            }
+        }
+    }
+
+    private class BackgroundHeapCheckHandler extends Handler {
+        BackgroundHeapCheckHandler(Looper onLooper) {
+            super(onLooper);
+            if (Looper.getMainLooper().equals(onLooper)) {
+                throw new RuntimeException(
+                        "BackgroundHeapCheckHandler may not run on the ui thread");
+            }
+        }
+
+        @Override
+        public void handleMessage(Message m) {
+            switch (m.what) {
+                case DO_GARBAGE_INSPECTION:
+                    if (gcAndCheckGarbage()) {
+                        postDelayed(GarbageMonitor.this::reinspectGarbageAfterGc, 100);
+                    }
+
+                    removeMessages(DO_GARBAGE_INSPECTION);
+                    sendEmptyMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
+                    break;
+
+                case DO_HEAP_TRACK:
+                    update();
+                    removeMessages(DO_HEAP_TRACK);
+                    sendEmptyMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
+                    break;
+            }
         }
     }
 }