From 61fdceb536d864026aefd379ce599e5fa09dafe0 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Ricau Date: Sun, 10 May 2015 09:01:34 -0700 Subject: [PATCH] Heap dumps on SD card * Heap dumps and analysis results were previously saved in the app directory. They are now saved on the external storage (sd card). * Centralized internal static helper methods to a dedicated class: `LeakCanaryInternals`. BREAKING CHANGES * When upgrading, previously saved heap dumps will be lost, but won't be removed from the app directory. You should probably uninstall your app. * Added permission WRITE_EXTERNAL_STORAGE * Public API change: Removed `Application` parameter in `LeakCanary.androidWatcher()` * Public API change: Removed `Application` parameter in `AndroidHeapDumper()` * Fixes #21 (can't share heap dump) * This is a step towards fixing #15 (strict mode violations), although there's still more work. --- .../src/main/AndroidManifest.xml | 3 + .../leakcanary/AndroidHeapDumper.java | 31 ++-- .../leakcanary/DisplayLeakService.java | 23 +-- .../com/squareup/leakcanary/LeakCanary.java | 80 +--------- .../leakcanary/ServiceHeapDumpListener.java | 5 +- .../internal/DisplayLeakActivity.java | 27 ++-- .../internal/LeakCanaryInternals.java | 137 ++++++++++++++++++ 7 files changed, 184 insertions(+), 122 deletions(-) create mode 100644 library/leakcanary-android/src/main/java/com/squareup/leakcanary/internal/LeakCanaryInternals.java diff --git a/library/leakcanary-android/src/main/AndroidManifest.xml b/library/leakcanary-android/src/main/AndroidManifest.xml index f0cdef25aa..05d4ad8127 100644 --- a/library/leakcanary-android/src/main/AndroidManifest.xml +++ b/library/leakcanary-android/src/main/AndroidManifest.xml @@ -19,6 +19,9 @@ package="com.squareup.leakcanary" > + + + serviceClass) { - PackageManager packageManager = context.getPackageManager(); - PackageInfo packageInfo; - try { - packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES); - } catch (Exception e) { - Log.e("AndroidUtils", "Could not get package info for " + context.getPackageName(), e); - return false; - } - String mainProcess = packageInfo.applicationInfo.processName; - - ComponentName component = new ComponentName(context, serviceClass); - ServiceInfo serviceInfo; - try { - serviceInfo = packageManager.getServiceInfo(component, 0); - } catch (PackageManager.NameNotFoundException ignored) { - // Service is disabled. - return false; - } - - if (serviceInfo.processName.equals(mainProcess)) { - Log.e("AndroidUtils", - "Did not expect service " + serviceClass + " to run in main process " + mainProcess); - // Technically we are in the service process, but we're not in the service dedicated process. - return false; - } - - int myPid = android.os.Process.myPid(); - ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - ActivityManager.RunningAppProcessInfo myProcess = null; - for (ActivityManager.RunningAppProcessInfo process : activityManager.getRunningAppProcesses()) { - if (process.pid == myPid) { - myProcess = process; - break; - } - } - if (myProcess == null) { - Log.e("AndroidUtils", "Could not find running process for " + myPid); - return false; - } - - return myProcess.processName.equals(serviceInfo.processName); - } - - static void setEnabled(Context context, Class componentClass, boolean enabled) { - ComponentName component = new ComponentName(context, componentClass); - PackageManager packageManager = context.getPackageManager(); - int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED; - // Blocks on IPC. - packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP); - } - - /** Extracts the class simple name out of a string containing a fully qualified class name. */ - static String classSimpleName(String className) { - int separator = className.lastIndexOf('.'); - if (separator == -1) { - return className; - } else { - return className.substring(separator + 1); - } - } - private LeakCanary() { throw new AssertionError(); } diff --git a/library/leakcanary-android/src/main/java/com/squareup/leakcanary/ServiceHeapDumpListener.java b/library/leakcanary-android/src/main/java/com/squareup/leakcanary/ServiceHeapDumpListener.java index b297f152ad..2ccb14227a 100644 --- a/library/leakcanary-android/src/main/java/com/squareup/leakcanary/ServiceHeapDumpListener.java +++ b/library/leakcanary-android/src/main/java/com/squareup/leakcanary/ServiceHeapDumpListener.java @@ -19,6 +19,7 @@ import com.squareup.leakcanary.internal.HeapAnalyzerService; import static com.squareup.leakcanary.Preconditions.checkNotNull; +import static com.squareup.leakcanary.internal.LeakCanaryInternals.setEnabled; public final class ServiceHeapDumpListener implements HeapDump.Listener { @@ -27,8 +28,8 @@ public final class ServiceHeapDumpListener implements HeapDump.Listener { public ServiceHeapDumpListener(Context context, Class listenerServiceClass) { - LeakCanary.setEnabled(context, listenerServiceClass, true); - LeakCanary.setEnabled(context, HeapAnalyzerService.class, true); + setEnabled(context, listenerServiceClass, true); + setEnabled(context, HeapAnalyzerService.class, true); this.listenerServiceClass = checkNotNull(listenerServiceClass, "listenerServiceClass"); this.context = checkNotNull(context, "context").getApplicationContext(); } diff --git a/library/leakcanary-android/src/main/java/com/squareup/leakcanary/internal/DisplayLeakActivity.java b/library/leakcanary-android/src/main/java/com/squareup/leakcanary/internal/DisplayLeakActivity.java index 8268dff010..240c2ac4c9 100644 --- a/library/leakcanary-android/src/main/java/com/squareup/leakcanary/internal/DisplayLeakActivity.java +++ b/library/leakcanary-android/src/main/java/com/squareup/leakcanary/internal/DisplayLeakActivity.java @@ -57,6 +57,8 @@ import static android.text.format.DateUtils.FORMAT_SHOW_DATE; import static android.text.format.DateUtils.FORMAT_SHOW_TIME; import static com.squareup.leakcanary.LeakCanary.leakInfo; +import static com.squareup.leakcanary.internal.LeakCanaryInternals.detectedLeakDirectory; +import static com.squareup.leakcanary.internal.LeakCanaryInternals.leakResultFile; @SuppressWarnings("ConstantConditions") @TargetApi(Build.VERSION_CODES.HONEYCOMB) public final class DisplayLeakActivity extends Activity { @@ -64,14 +66,6 @@ public final class DisplayLeakActivity extends Activity { private static final String TAG = "DisplayLeakActivity"; private static final String SHOW_LEAK_EXTRA = "show_latest"; - public static File leakDirectory(Context context) { - return new File(context.getFilesDir(), "detected_leaks"); - } - - public static File leakResultFile(File heapdumpFile) { - return new File(heapdumpFile.getParentFile(), heapdumpFile.getName() + ".result"); - } - public static PendingIntent createPendingIntent(Context context, String referenceKey) { Intent intent = new Intent(context, DisplayLeakActivity.class); intent.putExtra(SHOW_LEAK_EXTRA, referenceKey); @@ -257,9 +251,9 @@ public void onItemClick(AdapterView parent, View view, int position, long id) actionButton.setText("Remove all leaks"); actionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - File directory = leakDirectory(DisplayLeakActivity.this); - if (directory.exists()) { - for (File file : directory.listFiles()) { + File[] files = detectedLeakDirectory().listFiles(); + if (files != null) { + for (File file : files) { file.delete(); } } @@ -359,7 +353,7 @@ static void forgetActivity() { LoadLeaks(DisplayLeakActivity activity) { this.activityOrNull = activity; - leakDirectory = leakDirectory(activity); + leakDirectory = detectedLeakDirectory(); mainHandler = new Handler(Looper.getMainLooper()); } @@ -371,8 +365,8 @@ static void forgetActivity() { } }); if (files != null) { - for (File file : files) { - File resultFile = leakResultFile(file); + for (File heapDumpFile : files) { + File resultFile = leakResultFile(heapDumpFile); FileInputStream fis = null; try { fis = new FileInputStream(resultFile); @@ -383,9 +377,10 @@ static void forgetActivity() { } catch (IOException | ClassNotFoundException e) { // Likely a change in the serializable result class. // Let's remove the files, we can't read them anymore. - file.delete(); + heapDumpFile.delete(); resultFile.delete(); - Log.e(TAG, "Could not read result file, deleted result and heap dump:" + file, e); + Log.e(TAG, "Could not read result file, deleted result and heap dump:" + heapDumpFile, + e); } finally { if (fis != null) { try { diff --git a/library/leakcanary-android/src/main/java/com/squareup/leakcanary/internal/LeakCanaryInternals.java b/library/leakcanary-android/src/main/java/com/squareup/leakcanary/internal/LeakCanaryInternals.java new file mode 100644 index 0000000000..efeaa3ab81 --- /dev/null +++ b/library/leakcanary-android/src/main/java/com/squareup/leakcanary/internal/LeakCanaryInternals.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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.squareup.leakcanary.internal; + +import android.app.ActivityManager; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.Environment; +import android.util.Log; +import java.io.File; + +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; +import static android.content.pm.PackageManager.DONT_KILL_APP; +import static android.content.pm.PackageManager.GET_SERVICES; +import static android.os.Environment.DIRECTORY_DOWNLOADS; + +public final class LeakCanaryInternals { + + public static File storageDirectory() { + File downloadsDirectory = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS); + File leakCanaryDirectory = new File(downloadsDirectory, "leakcanary"); + leakCanaryDirectory.mkdirs(); + return leakCanaryDirectory; + } + + public static File detectedLeakDirectory() { + File directory = new File(storageDirectory(), "detected_leaks"); + directory.mkdirs(); + return directory; + } + + public static File leakResultFile(File heapdumpFile) { + return new File(heapdumpFile.getParentFile(), heapdumpFile.getName() + ".result"); + } + + public static boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } + + public static File findNextAvailableHprofFile(int maxFiles) { + File directory = detectedLeakDirectory(); + for (int i = 0; i < maxFiles; i++) { + String heapDumpName = "heap_dump_" + i + ".hprof"; + File file = new File(directory, heapDumpName); + if (!file.exists()) { + return file; + } + } + return null; + } + + /** Extracts the class simple name out of a string containing a fully qualified class name. */ + public static String classSimpleName(String className) { + int separator = className.lastIndexOf('.'); + if (separator == -1) { + return className; + } else { + return className.substring(separator + 1); + } + } + + public static void setEnabled(Context context, Class componentClass, boolean enabled) { + ComponentName component = new ComponentName(context, componentClass); + PackageManager packageManager = context.getPackageManager(); + int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED; + // Blocks on IPC. + packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP); + } + + public static boolean isInServiceProcess(Context context, Class serviceClass) { + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo; + try { + packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES); + } catch (Exception e) { + Log.e("AndroidUtils", "Could not get package info for " + context.getPackageName(), e); + return false; + } + String mainProcess = packageInfo.applicationInfo.processName; + + ComponentName component = new ComponentName(context, serviceClass); + ServiceInfo serviceInfo; + try { + serviceInfo = packageManager.getServiceInfo(component, 0); + } catch (PackageManager.NameNotFoundException ignored) { + // Service is disabled. + return false; + } + + if (serviceInfo.processName.equals(mainProcess)) { + Log.e("AndroidUtils", + "Did not expect service " + serviceClass + " to run in main process " + mainProcess); + // Technically we are in the service process, but we're not in the service dedicated process. + return false; + } + + int myPid = android.os.Process.myPid(); + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.RunningAppProcessInfo myProcess = null; + for (ActivityManager.RunningAppProcessInfo process : activityManager.getRunningAppProcesses()) { + if (process.pid == myPid) { + myProcess = process; + break; + } + } + if (myProcess == null) { + Log.e("AndroidUtils", "Could not find running process for " + myPid); + return false; + } + + return myProcess.processName.equals(serviceInfo.processName); + } + + private LeakCanaryInternals() { + throw new AssertionError(); + } +}