/* * 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() { @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() { @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()); } }