SysUiLeaks: Add GarbageMonitor

Adds a service for monitoring the amount of tracked garbage.
If it exceeds reasonable levels, a notification with a leak
report is posted.

Test: runtest systemui
Change-Id: Ib55281f2aac557743b97c46bc616688261c72e9c
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 4b8734f..b2e2a2c 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -559,6 +559,16 @@
             </intent-filter>
         </receiver>
 
+        <provider
+            android:name="android.support.v4.content.FileProvider"
+            android:authorities="com.android.systemui.fileprovider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/fileprovider" />
+        </provider>
+
         <receiver
             android:name=".statusbar.KeyboardShortcutsReceiver">
             <intent-filter>
diff --git a/packages/SystemUI/res/xml/fileprovider.xml b/packages/SystemUI/res/xml/fileprovider.xml
new file mode 100644
index 0000000..4aaa90f
--- /dev/null
+++ b/packages/SystemUI/res/xml/fileprovider.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <cache-path name="leak" path="leak/"/>
+</paths>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 8d46b43..273b5e3 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -67,7 +67,9 @@
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
 import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.leak.GarbageMonitor;
 import com.android.systemui.util.leak.LeakDetector;
+import com.android.systemui.util.leak.LeakReporter;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -104,6 +106,12 @@
      */
     public static final DependencyKey<Handler> MAIN_HANDLER = new DependencyKey<>("main_handler");
 
+    /**
+     * An email address to send memory leak reports to by default.
+     */
+    public static final DependencyKey<String> LEAK_REPORT_EMAIL
+            = new DependencyKey<>("leak_report_email");
+
     private final ArrayMap<Object, Object> mDependencies = new ArrayMap<>();
     private final ArrayMap<Object, DependencyProvider> mProviders = new ArrayMap<>();
 
@@ -192,6 +200,18 @@
 
         mProviders.put(LeakDetector.class, LeakDetector::create);
 
+        mProviders.put(LEAK_REPORT_EMAIL, () -> null);
+
+        mProviders.put(LeakReporter.class, () -> new LeakReporter(
+                mContext,
+                getDependency(LeakDetector.class),
+                getDependency(LEAK_REPORT_EMAIL)));
+
+        mProviders.put(GarbageMonitor.class, () -> new GarbageMonitor(
+                getDependency(BG_LOOPER),
+                getDependency(LeakDetector.class),
+                getDependency(LeakReporter.class)));
+
         mProviders.put(TunerService.class, () ->
                 new TunerService(mContext));
 
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index e5bda7e..187b557 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -48,6 +48,7 @@
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.usb.StorageNotification;
 import com.android.systemui.util.NotificationChannels;
+import com.android.systemui.util.leak.GarbageMonitor;
 import com.android.systemui.volume.VolumeUI;
 
 import java.util.HashMap;
@@ -81,6 +82,7 @@
             PipUI.class,
             ShortcutKeyDispatcher.class,
             VendorServices.class,
+            GarbageMonitor.Service.class,
             LatencyTester.class,
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
new file mode 100644
index 0000000..ef743e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
@@ -0,0 +1,94 @@
+/*
+ * 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.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.SystemUI;
+
+public class GarbageMonitor {
+
+    private static final String TAG = "GarbageMonitor";
+
+    private static final long GARBAGE_INSPECTION_INTERVAL = 5 * 60 * 1000; // 5min
+    private static final int GARBAGE_ALLOWANCE = 5;
+
+    private final Handler mHandler;
+    private final TrackedGarbage mTrackedGarbage;
+    private final LeakReporter mLeakReporter;
+
+    public GarbageMonitor(Looper bgLooper, LeakDetector leakDetector,
+            LeakReporter leakReporter) {
+        mHandler = bgLooper != null ? new Handler(bgLooper): null;
+        mTrackedGarbage = leakDetector.getTrackedGarbage();
+        mLeakReporter = leakReporter;
+    }
+
+    public void start() {
+        if (mTrackedGarbage == null) {
+            return;
+        }
+
+        scheduleInspectGarbage(this::inspectGarbage);
+    }
+
+    @VisibleForTesting
+    void scheduleInspectGarbage(Runnable runnable) {
+        mHandler.postDelayed(runnable, GARBAGE_INSPECTION_INTERVAL);
+    }
+
+    private void inspectGarbage() {
+        if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
+            Runtime.getRuntime().gc();
+
+            // Allow some time to for ReferenceQueue to catch up.
+            scheduleReinspectGarbage(this::reinspectGarbageAfterGc);
+        }
+        scheduleInspectGarbage(this::inspectGarbage);
+    }
+
+    @VisibleForTesting
+    void scheduleReinspectGarbage(Runnable runnable) {
+        mHandler.postDelayed(runnable, (long) 100);
+    }
+
+    private void reinspectGarbageAfterGc() {
+        int count = mTrackedGarbage.countOldGarbage();
+        if (count > GARBAGE_ALLOWANCE) {
+            mLeakReporter.dumpLeak(count);
+        }
+    }
+
+    public static class Service extends SystemUI {
+
+        private GarbageMonitor mGarbageMonitor;
+
+        @Override
+        public void start() {
+            if (!Build.IS_DEBUGGABLE) {
+                return;
+            }
+            mGarbageMonitor = Dependency.get(GarbageMonitor.class);
+            mGarbageMonitor.start();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
index a0f8659..574fdb98 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
@@ -96,6 +96,10 @@
         }
     }
 
+    TrackedGarbage getTrackedGarbage() {
+        return mTrackedGarbage;
+    }
+
     @Override
     public void dump(FileDescriptor df, PrintWriter w, String[] args) {
         IndentingPrintWriter pw = new IndentingPrintWriter(w, "  ");
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
new file mode 100644
index 0000000..a247f3f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
@@ -0,0 +1,142 @@
+/*
+ * 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.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Debug;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.support.v4.content.FileProvider;
+import android.util.Log;
+
+import com.google.android.collect.Lists;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Dumps data to debug leaks and posts a notification to share the data.
+ */
+public class LeakReporter {
+
+    static final String TAG = "LeakReporter";
+
+    public static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
+
+    static final String LEAK_DIR = "leak";
+    static final String LEAK_HPROF = "leak.hprof";
+    static final String LEAK_DUMP = "leak.dump";
+
+    private final Context mContext;
+    private final LeakDetector mLeakDetector;
+    private final String mLeakReportEmail;
+
+    public LeakReporter(Context context, LeakDetector leakDetector, String leakReportEmail) {
+        mContext = context;
+        mLeakDetector = leakDetector;
+        mLeakReportEmail = leakReportEmail;
+    }
+
+    public void dumpLeak(int garbageCount) {
+        try {
+            File leakDir = new File(mContext.getCacheDir(), LEAK_DIR);
+            leakDir.mkdir();
+
+            File hprofFile = new File(leakDir, LEAK_HPROF);
+            Debug.dumpHprofData(hprofFile.getAbsolutePath());
+
+            File dumpFile = new File(leakDir, LEAK_DUMP);
+            try (FileOutputStream fos = new FileOutputStream(dumpFile)) {
+                PrintWriter w = new PrintWriter(fos);
+                w.print("Build: "); w.println(SystemProperties.get("ro.build.description"));
+                w.println();
+                w.flush();
+                mLeakDetector.dump(fos.getFD(), w, new String[0]);
+                w.close();
+            }
+
+            NotificationManager notiMan = mContext.getSystemService(NotificationManager.class);
+
+            NotificationChannel channel = new NotificationChannel("leak", "Leak Alerts",
+                    NotificationManager.IMPORTANCE_HIGH);
+            channel.enableVibration(true);
+
+            notiMan.createNotificationChannel(channel);
+            Notification.Builder builder = new Notification.Builder(mContext, channel.getId())
+                    .setAutoCancel(true)
+                    .setShowWhen(true)
+                    .setContentTitle("Memory Leak Detected")
+                    .setContentText(String.format(
+                            "SystemUI has detected %d leaked objects. Tap to send", garbageCount))
+                    .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
+                    .setContentIntent(PendingIntent.getActivityAsUser(mContext, 0,
+                            getIntent(hprofFile, dumpFile),
+                            PendingIntent.FLAG_UPDATE_CURRENT, null, UserHandle.CURRENT));
+            notiMan.notify(TAG, 0, builder.build());
+        } catch (IOException e) {
+            Log.e(TAG, "Couldn't dump heap for leak", e);
+        }
+    }
+
+    private Intent getIntent(File hprofFile, File dumpFile) {
+        Uri dumpUri = FileProvider.getUriForFile(mContext, FILEPROVIDER_AUTHORITY, dumpFile);
+        Uri hprofUri = FileProvider.getUriForFile(mContext, FILEPROVIDER_AUTHORITY, hprofFile);
+
+        Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
+        String mimeType = "application/vnd.android.leakreport";
+
+        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        intent.setType(mimeType);
+
+        final String subject = "SystemUI leak report";
+        intent.putExtra(Intent.EXTRA_SUBJECT, subject);
+
+        // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
+        // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
+        // create the ClipData object with the attachments URIs.
+        final StringBuilder messageBody = new StringBuilder("Build info: ")
+                .append(SystemProperties.get("ro.build.description"));
+        intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
+        final ClipData clipData = new ClipData(null, new String[] { mimeType },
+                new ClipData.Item(null, null, null, dumpUri));
+        final ArrayList<Uri> attachments = Lists.newArrayList(dumpUri);
+
+        clipData.addItem(new ClipData.Item(null, null, null, hprofUri));
+        attachments.add(hprofUri);
+
+        intent.setClipData(clipData);
+        intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
+
+        String leakReportEmail = mLeakReportEmail;
+        if (leakReportEmail != null) {
+            intent.putExtra(Intent.EXTRA_EMAIL, new String[] { leakReportEmail });
+        }
+
+        return intent;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java
index fd59aee..98a6450 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java
@@ -106,6 +106,20 @@
         }
     }
 
+    public synchronized int countOldGarbage() {
+        cleanUp();
+
+        long now = SystemClock.uptimeMillis();
+
+        int result = 0;
+        for (LeakReference ref : mGarbage) {
+            if (isOld(ref.createdUptimeMillis, now)) {
+                result++;
+            }
+        }
+        return result;
+    }
+
     private boolean isOld(long createdUptimeMillis, long now) {
         return createdUptimeMillis + GARBAGE_COLLECTION_DEADLINE_MILLIS < now;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
new file mode 100644
index 0000000..9a825c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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 static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GarbageMonitorTest {
+
+    private LeakReporter mLeakReporter;
+    private TrackedGarbage mTrackedGarbage;
+    private TestableGarbageMonitor mGarbageMonitor;
+
+    @Before
+    public void setup() {
+        mTrackedGarbage = mock(TrackedGarbage.class);
+        mLeakReporter = mock(LeakReporter.class);
+        mGarbageMonitor = new TestableGarbageMonitor(
+                new LeakDetector(null, mTrackedGarbage, null),
+                mLeakReporter);
+    }
+
+    @Test
+    public void testCallbacks_getScheduled() {
+        mGarbageMonitor.start();
+        mGarbageMonitor.runCallbacksOnce();
+        mGarbageMonitor.runCallbacksOnce();
+        mGarbageMonitor.runCallbacksOnce();
+    }
+
+    @Test
+    public void testNoGarbage_doesntDump() {
+        when(mTrackedGarbage.countOldGarbage()).thenReturn(0);
+
+        mGarbageMonitor.start();
+        mGarbageMonitor.runCallbacksOnce();
+        mGarbageMonitor.runCallbacksOnce();
+        mGarbageMonitor.runCallbacksOnce();
+
+        verify(mLeakReporter, never()).dumpLeak(anyInt());
+    }
+
+    @Test
+    public void testALittleGarbage_doesntDump() {
+        when(mTrackedGarbage.countOldGarbage()).thenReturn(4);
+
+        mGarbageMonitor.start();
+        mGarbageMonitor.runCallbacksOnce();
+        mGarbageMonitor.runCallbacksOnce();
+        mGarbageMonitor.runCallbacksOnce();
+
+        verify(mLeakReporter, never()).dumpLeak(anyInt());
+    }
+
+    @Test
+    public void testTransientGarbage_doesntDump() {
+        when(mTrackedGarbage.countOldGarbage()).thenReturn(100);
+
+        mGarbageMonitor.start();
+        mGarbageMonitor.runInspectCallback();
+
+        when(mTrackedGarbage.countOldGarbage()).thenReturn(0);
+
+        mGarbageMonitor.runReinspectCallback();
+
+        verify(mLeakReporter, never()).dumpLeak(anyInt());
+    }
+
+    @Test
+    public void testLotsOfPersistentGarbage_dumps() {
+        when(mTrackedGarbage.countOldGarbage()).thenReturn(100);
+
+        mGarbageMonitor.start();
+        mGarbageMonitor.runCallbacksOnce();
+
+        verify(mLeakReporter).dumpLeak(anyInt());
+    }
+
+    private static class TestableGarbageMonitor extends GarbageMonitor {
+        Runnable mInspectCallback;
+        Runnable mReinspectCallback;
+
+        public TestableGarbageMonitor(LeakDetector leakDetector,
+                LeakReporter leakReporter) {
+            super(null /* bgLooper */, leakDetector, leakReporter);
+        }
+
+        @Override
+        void scheduleInspectGarbage(Runnable runnable) {
+            assertNull("must not have more than one pending inspect callback", mInspectCallback);
+            mInspectCallback = runnable;
+        }
+
+        void runInspectCallback() {
+            assertNotNull("expected an inspect callback to be scheduled", mInspectCallback);
+            Runnable callback = mInspectCallback;
+            mInspectCallback = null;
+            callback.run();
+        }
+
+        @Override
+        void scheduleReinspectGarbage(Runnable runnable) {
+            assertNull("must not have more than one reinspect callback", mReinspectCallback);
+            mReinspectCallback = runnable;
+        }
+
+        void runReinspectCallback() {
+            assertNotNull("expected a reinspect callback to be scheduled", mInspectCallback);
+            maybeRunReinspectCallback();
+        }
+
+        void maybeRunReinspectCallback() {
+            Runnable callback = mReinspectCallback;
+            mReinspectCallback = null;
+            if (callback != null) {
+                callback.run();
+            }
+        }
+
+        void runCallbacksOnce() {
+            runInspectCallback();
+            maybeRunReinspectCallback();
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java
new file mode 100644
index 0000000..96a9bee
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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 static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.NotificationManager;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.PrintWriter;
+
+@RunWith(AndroidJUnit4.class)
+public class LeakReporterTest extends SysuiTestCase {
+
+    private LeakDetector mLeakDetector;
+    private LeakReporter mLeakReporter;
+    private File mLeakDir;
+    private File mLeakDump;
+    private File mLeakHprof;
+    private NotificationManager mNotificationManager;
+
+    @Before
+    public void setup() {
+        mLeakDir = new File(mContext.getCacheDir(), LeakReporter.LEAK_DIR);
+        mLeakDump = new File(mLeakDir, LeakReporter.LEAK_DUMP);
+        mLeakHprof = new File(mLeakDir, LeakReporter.LEAK_HPROF);
+
+        mNotificationManager = mock(NotificationManager.class);
+        mContext.addMockSystemService(NotificationManager.class, mNotificationManager);
+
+        mLeakDetector = mock(LeakDetector.class);
+        doAnswer(invocation -> {
+            invocation.<PrintWriter>getArgument(1).println("test");
+            return null;
+        }).when(mLeakDetector).dump(any(), any(), any());
+
+        mLeakReporter = new LeakReporter(mContext, mLeakDetector, "test@example.com");
+    }
+
+    @After
+    public void teardown() {
+        mLeakDump.delete();
+        mLeakHprof.delete();
+        mLeakDir.delete();
+    }
+
+    @Test
+    public void testDump_postsNotification() {
+        mLeakReporter.dumpLeak(5);
+        verify(mNotificationManager).notify(any(), anyInt(), any());
+    }
+
+    @Test
+    public void testDump_Repeated() {
+        mLeakReporter.dumpLeak(1);
+        mLeakReporter.dumpLeak(2);
+    }
+
+    @Test
+    public void testDump_ProducesNonZeroFiles() {
+        mLeakReporter.dumpLeak(5);
+
+        assertTrue(mLeakDump.exists());
+        assertTrue(mLeakDump.length() > 0);
+
+        assertTrue(mLeakHprof.exists());
+        assertTrue(mLeakHprof.length() > 0);
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java
index c4f1003..1429390 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java
@@ -33,8 +33,8 @@
 import android.view.LayoutInflater;
 
 import com.android.systemui.SysUiServiceProvider;
-import com.android.systemui.utils.leaks.Tracker;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.utils.leaks.Tracker;
 
 public class TestableContext extends ContextWrapper implements SysUiServiceProvider {
 
@@ -81,6 +81,10 @@
         return super.getResources();
     }
 
+    public <T> void addMockSystemService(Class<T> service, T mock) {
+        addMockSystemService(getSystemServiceName(service), mock);
+    }
+
     public void addMockSystemService(String name, Object service) {
         mMockSystemServices = lazyInit(mMockSystemServices);
         mMockSystemServices.put(name, service);