Merge "Move heap dump sharing to SHELL." am: c29b5cba29 am: 2cdf2d5ad7
am: 215d92f880

Change-Id: Ic092f2d5c0c0a1497c19e73417c68b8e8616cc63
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 54e291f..c59f342 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -180,6 +180,8 @@
     <uses-permission android:name="android.permission.MANAGE_WIFI_WHEN_WIRELESS_CONSENT_REQUIRED" />
     <!-- Permission needed to invoke DynamicSystem (AOT) -->
     <uses-permission android:name="android.permission.INSTALL_DYNAMIC_SYSTEM" />
+    <!-- Used to clean up heap dumps on boot. -->
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
 
 
     <uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
@@ -209,7 +211,7 @@
 
     <!-- Permission required to test ExplicitHealthCheckServiceImpl. -->
     <uses-permission android:name="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE" />
-    
+
     <!-- Permission required for CTS test - CrossProfileAppsHostSideTest -->
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
 
@@ -239,6 +241,11 @@
             </intent-filter>
         </provider>
 
+        <provider android:name=".HeapDumpProvider"
+                  android:authorities="com.android.shell.heapdump"
+                  android:grantUriPermissions="true"
+                  android:exported="true" />
+
         <activity
             android:name=".BugreportWarningActivity"
             android:theme="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight"
@@ -246,6 +253,14 @@
             android:excludeFromRecents="true"
             android:exported="false" />
 
+        <activity android:name=".HeapDumpActivity"
+                  android:theme="@*android:style/Theme.Translucent.NoTitleBar"
+                  android:label="@*android:string/dump_heap_title"
+                  android:finishOnCloseSystemDialogs="true"
+                  android:noHistory="true"
+                  android:excludeFromRecents="true"
+                  android:exported="false" />
+
         <receiver
             android:name=".BugreportRequestedReceiver"
             android:permission="android.permission.TRIGGER_SHELL_BUGREPORT">
@@ -254,6 +269,16 @@
             </intent-filter>
         </receiver>
 
+        <receiver
+            android:name=".HeapDumpReceiver"
+            android:permission="android.permission.DUMP">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="com.android.internal.intent.action.HEAP_DUMP_FINISHED" />
+                <action android:name="com.android.shell.action.DELETE_HEAP_DUMP" />
+            </intent-filter>
+        </receiver>
+
         <service
             android:name=".BugreportProgressService"
             android:exported="false"/>
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index 1b35770..30ad9c5 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -1560,7 +1560,7 @@
         return false;
     }
 
-    private static boolean isTv(Context context) {
+    static boolean isTv(Context context) {
         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
     }
 
diff --git a/packages/Shell/src/com/android/shell/HeapDumpActivity.java b/packages/Shell/src/com/android/shell/HeapDumpActivity.java
new file mode 100644
index 0000000..0ff0d33
--- /dev/null
+++ b/packages/Shell/src/com/android/shell/HeapDumpActivity.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2019 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.shell;
+
+import static com.android.shell.HeapDumpProvider.makeUri;
+import static com.android.shell.HeapDumpReceiver.ACTION_DELETE_HEAP_DUMP;
+import static com.android.shell.HeapDumpReceiver.EXTRA_IS_USER_INITIATED;
+import static com.android.shell.HeapDumpReceiver.EXTRA_PROCESS_NAME;
+import static com.android.shell.HeapDumpReceiver.EXTRA_REPORT_PACKAGE;
+import static com.android.shell.HeapDumpReceiver.EXTRA_SIZE_BYTES;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Process;
+import android.util.DebugUtils;
+import android.util.Log;
+
+import com.android.internal.R;
+
+/**
+ * This activity is displayed when the system has collected a heap dump.
+ */
+public class HeapDumpActivity extends Activity {
+    private static final String TAG = "HeapDumpActivity";
+
+    static final String KEY_URI = "uri";
+
+    private AlertDialog mDialog;
+    private Uri mDumpUri;
+    private boolean mHandled = false;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        String process = getIntent().getStringExtra(EXTRA_PROCESS_NAME);
+        long size = getIntent().getLongExtra(EXTRA_SIZE_BYTES, 0);
+        final boolean isUserInitiated = getIntent().getBooleanExtra(EXTRA_IS_USER_INITIATED, false);
+        final int uid = getIntent().getIntExtra(Intent.EXTRA_UID, 0);
+        final boolean isSystemProcess = uid == Process.SYSTEM_UID;
+        mDumpUri = makeUri(process);
+        final String procDisplayName = isSystemProcess
+                ? getString(com.android.internal.R.string.android_system_label)
+                : process;
+
+        final Intent sendIntent = new Intent();
+        ClipData clip = ClipData.newUri(getContentResolver(), "Heap Dump", mDumpUri);
+        sendIntent.setClipData(clip);
+        sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        sendIntent.setType(clip.getDescription().getMimeType(0));
+        sendIntent.putExtra(Intent.EXTRA_STREAM, mDumpUri);
+
+        String directLaunchPackage = getIntent().getStringExtra(EXTRA_REPORT_PACKAGE);
+        if (directLaunchPackage != null) {
+            sendIntent.setAction(ActivityManager.ACTION_REPORT_HEAP_LIMIT);
+            sendIntent.setPackage(directLaunchPackage);
+            try {
+                startActivity(sendIntent);
+                mHandled = true;
+                finish();
+                return;
+            } catch (ActivityNotFoundException e) {
+                Log.e(TAG, "Unable to direct launch to " + directLaunchPackage, e);
+            }
+        }
+
+        final int messageId;
+        if (isUserInitiated) {
+            messageId = com.android.internal.R.string.dump_heap_ready_text;
+        } else if (isSystemProcess) {
+            messageId = com.android.internal.R.string.dump_heap_system_text;
+        } else {
+            messageId = com.android.internal.R.string.dump_heap_text;
+        }
+        mDialog = new AlertDialog.Builder(this, android.R.style.Theme_Material_Light_Dialog_Alert)
+                .setTitle(com.android.internal.R.string.dump_heap_title)
+                .setMessage(getString(messageId, procDisplayName,
+                        DebugUtils.sizeValueToString(size, null)))
+                .setNegativeButton(android.R.string.cancel, (dialog, which) -> {
+                    mHandled = true;
+                    finish();
+                })
+                .setNeutralButton(R.string.delete, (dialog, which) -> {
+                    mHandled = true;
+                    Intent deleteIntent = new Intent(ACTION_DELETE_HEAP_DUMP);
+                    deleteIntent.setClass(getApplicationContext(), HeapDumpReceiver.class);
+                    deleteIntent.putExtra(KEY_URI, mDumpUri.toString());
+                    sendBroadcast(deleteIntent);
+                    finish();
+                })
+                .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+                    mHandled = true;
+                    sendIntent.setAction(Intent.ACTION_SEND);
+                    sendIntent.setPackage(null);
+                    startActivity(Intent.createChooser(sendIntent,
+                            getText(com.android.internal.R.string.dump_heap_title)));
+                    finish();
+                })
+                .show();
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (!isChangingConfigurations()) {
+            if (!mHandled) {
+                Intent deleteIntent = new Intent(ACTION_DELETE_HEAP_DUMP);
+                deleteIntent.setClass(getApplicationContext(), HeapDumpReceiver.class);
+                deleteIntent.putExtra(KEY_URI, mDumpUri.toString());
+                sendBroadcast(deleteIntent);
+            }
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mDialog != null) {
+            mDialog.dismiss();
+        }
+    }
+}
diff --git a/packages/Shell/src/com/android/shell/HeapDumpProvider.java b/packages/Shell/src/com/android/shell/HeapDumpProvider.java
new file mode 100644
index 0000000..3eceb91
--- /dev/null
+++ b/packages/Shell/src/com/android/shell/HeapDumpProvider.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 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.shell;
+
+import android.annotation.NonNull;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+/** ContentProvider to write and access heap dumps. */
+public class HeapDumpProvider extends ContentProvider {
+    private static final String FILENAME_SUFFIX = "_javaheap.bin";
+    private static final Object sLock = new Object();
+
+    private File mRoot;
+
+    @Override
+    public boolean onCreate() {
+        synchronized (sLock) {
+            mRoot = new File(getContext().createCredentialProtectedStorageContext().getFilesDir(),
+                    "heapdumps");
+            return mRoot.mkdir();
+        }
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return "application/octet-stream";
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException("Insert not allowed.");
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        String path = sanitizePath(uri.getEncodedPath());
+        String tag = Uri.decode(path);
+        return (new File(mRoot, tag)).delete() ? 1 : 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("Update not allowed.");
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        String path = sanitizePath(uri.getEncodedPath());
+        String tag = Uri.decode(path);
+        final int pMode;
+        if (Binder.getCallingUid() == Process.SYSTEM_UID) {
+            pMode = ParcelFileDescriptor.MODE_CREATE
+                    | ParcelFileDescriptor.MODE_TRUNCATE
+                    | ParcelFileDescriptor.MODE_WRITE_ONLY;
+        } else {
+            pMode = ParcelFileDescriptor.MODE_READ_ONLY;
+        }
+
+        synchronized (sLock) {
+            return ParcelFileDescriptor.open(new File(mRoot, tag), pMode);
+        }
+    }
+
+    @NonNull
+    static Uri makeUri(@NonNull String procName) {
+        return Uri.parse("content://com.android.shell.heapdump/" + procName + FILENAME_SUFFIX);
+    }
+
+    private String sanitizePath(String path) {
+        return path.replaceAll("[^a-zA-Z0-9_.]", "");
+    }
+}
diff --git a/packages/Shell/src/com/android/shell/HeapDumpReceiver.java b/packages/Shell/src/com/android/shell/HeapDumpReceiver.java
new file mode 100644
index 0000000..858c521
--- /dev/null
+++ b/packages/Shell/src/com/android/shell/HeapDumpReceiver.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2019 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.shell;
+
+import static com.android.shell.BugreportProgressService.isTv;
+
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.FileUtils;
+import android.os.Process;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+import java.io.File;
+
+/**
+ * Receiver that handles finished heap dumps.
+ */
+public class HeapDumpReceiver extends BroadcastReceiver {
+    private static final String TAG = "HeapDumpReceiver";
+
+    /**
+     * Broadcast action to determine when to delete a specific dump heap. Must include a {@link
+     * HeapDumpActivity#KEY_URI} String extra.
+     */
+    static final String ACTION_DELETE_HEAP_DUMP = "com.android.shell.action.DELETE_HEAP_DUMP";
+
+    /** Broadcast sent when heap dump collection has been completed. */
+    private static final String ACTION_HEAP_DUMP_FINISHED =
+            "com.android.internal.intent.action.HEAP_DUMP_FINISHED";
+
+    /** The process we are reporting */
+    static final String EXTRA_PROCESS_NAME = "com.android.internal.extra.heap_dump.PROCESS_NAME";
+
+    /** The size limit the process reached. */
+    static final String EXTRA_SIZE_BYTES = "com.android.internal.extra.heap_dump.SIZE_BYTES";
+
+    /** Whether the user initiated the dump or not. */
+    static final String EXTRA_IS_USER_INITIATED =
+            "com.android.internal.extra.heap_dump.IS_USER_INITIATED";
+
+    /** Optional name of package to directly launch. */
+    static final String EXTRA_REPORT_PACKAGE =
+            "com.android.internal.extra.heap_dump.REPORT_PACKAGE";
+
+    private static final String NOTIFICATION_CHANNEL_ID = "heapdumps";
+    private static final int NOTIFICATION_ID = 2019;
+
+    /**
+     * Always keep heap dumps taken in the last week.
+     */
+    private static final long MIN_KEEP_AGE_MS = DateUtils.WEEK_IN_MILLIS;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.d(TAG, "onReceive(): " + intent);
+        final String action = intent.getAction();
+        if (action == null) {
+            Log.e(TAG, "null action received");
+            return;
+        }
+        switch (action) {
+            case Intent.ACTION_BOOT_COMPLETED:
+                cleanupOldFiles(context);
+                break;
+            case ACTION_DELETE_HEAP_DUMP:
+                deleteHeapDump(context, intent.getStringExtra(HeapDumpActivity.KEY_URI));
+                break;
+            case ACTION_HEAP_DUMP_FINISHED:
+                showDumpNotification(context, intent);
+                break;
+        }
+    }
+
+    private void cleanupOldFiles(Context context) {
+        final PendingResult result = goAsync();
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                try {
+                    Log.d(TAG, "Deleting from " + new File(context.getFilesDir(), "heapdumps"));
+                    FileUtils.deleteOlderFiles(new File(context.getFilesDir(), "heapdumps"), 0,
+                            MIN_KEEP_AGE_MS);
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Couldn't delete old files", e);
+                }
+                result.finish();
+                return null;
+            }
+        }.execute();
+    }
+
+    private void deleteHeapDump(Context context, @Nullable final String uri) {
+        if (uri == null) {
+            Log.e(TAG, "null URI for delete heap dump intent");
+            return;
+        }
+        final PendingResult result = goAsync();
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                context.getContentResolver().delete(Uri.parse(uri), null, null);
+                result.finish();
+                return null;
+            }
+        }.execute();
+    }
+
+    private void showDumpNotification(Context context, Intent intent) {
+        final boolean isUserInitiated = intent.getBooleanExtra(
+                EXTRA_IS_USER_INITIATED, false);
+        final String procName = intent.getStringExtra(EXTRA_PROCESS_NAME);
+        final int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+
+        final String reportPackage = intent.getStringExtra(
+                EXTRA_REPORT_PACKAGE);
+        final long size = intent.getLongExtra(EXTRA_SIZE_BYTES, 0);
+
+        if (procName == null) {
+            Log.e(TAG, "No process name sent over");
+            return;
+        }
+
+        NotificationManager nm = NotificationManager.from(context);
+        nm.createNotificationChannel(
+                new NotificationChannel(NOTIFICATION_CHANNEL_ID,
+                        "Heap dumps",
+                        NotificationManager.IMPORTANCE_DEFAULT));
+
+        final int titleId = isUserInitiated
+                ? com.android.internal.R.string.dump_heap_ready_notification
+                : com.android.internal.R.string.dump_heap_notification;
+        final String procDisplayName = uid == Process.SYSTEM_UID
+                ? context.getString(com.android.internal.R.string.android_system_label)
+                : procName;
+        String text = context.getString(titleId, procDisplayName);
+
+        Intent shareIntent = new Intent();
+        shareIntent.setClassName(context, HeapDumpActivity.class.getName());
+        shareIntent.putExtra(EXTRA_PROCESS_NAME, procName);
+        shareIntent.putExtra(EXTRA_SIZE_BYTES, size);
+        shareIntent.putExtra(EXTRA_IS_USER_INITIATED, isUserInitiated);
+        shareIntent.putExtra(Intent.EXTRA_UID, uid);
+        if (reportPackage != null) {
+            shareIntent.putExtra(EXTRA_REPORT_PACKAGE, reportPackage);
+        }
+        final Notification.Builder builder = new Notification.Builder(context,
+                NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(
+                        isTv(context) ? R.drawable.ic_bug_report_black_24dp
+                                : com.android.internal.R.drawable.stat_sys_adb)
+                .setLocalOnly(true)
+                .setColor(context.getColor(
+                        com.android.internal.R.color.system_notification_accent_color))
+                .setContentTitle(text)
+                .setTicker(text)
+                .setAutoCancel(true)
+                .setContentText(context.getText(
+                        com.android.internal.R.string.dump_heap_notification_detail))
+                .setContentIntent(PendingIntent.getActivity(context, 2, shareIntent,
+                        PendingIntent.FLAG_UPDATE_CURRENT));
+
+        Log.v(TAG, "Creating share heap dump notification");
+        NotificationManager.from(context).notify(NOTIFICATION_ID, builder.build());
+    }
+}