diff --git a/app/build.gradle b/app/build.gradle index efc3b69f0..4e8115af5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,4 +89,8 @@ dependencies { implementation 'frankiesardo:icepick:3.2.0' annotationProcessor 'frankiesardo:icepick-processor:3.2.0' + + debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' + betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' + releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' } diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index 4d37094ba..ba1fd90cc 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -1,9 +1,21 @@ package org.schabi.newpipe; import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; +import com.squareup.leakcanary.AndroidHeapDumper; +import com.squareup.leakcanary.DefaultLeakDirectoryProvider; +import com.squareup.leakcanary.HeapDumper; +import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.LeakDirectoryProvider; +import com.squareup.leakcanary.RefWatcher; + +import java.io.File; +import java.util.concurrent.TimeUnit; public class DebugApp extends App { private static final String TAG = DebugApp.class.toString(); @@ -41,4 +53,35 @@ public class DebugApp extends App { // Initialize Stetho with the Initializer Stetho.initialize(initializer); } + + @Override + protected RefWatcher installLeakCanary() { + return LeakCanary.refWatcher(this) + .heapDumper(new ToggleableHeapDumper(this)) + // give each object 10 seconds to be gc'ed, before leak canary gets nosy on it + .watchDelay(10, TimeUnit.SECONDS) + .buildAndInstall(); + } + + public static class ToggleableHeapDumper implements HeapDumper { + private final HeapDumper dumper; + private final SharedPreferences preferences; + private final String dumpingAllowanceKey; + + ToggleableHeapDumper(@NonNull final Context context) { + LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context); + this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider); + this.preferences = PreferenceManager.getDefaultSharedPreferences(context); + this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key); + } + + private boolean isDumpingAllowed() { + return preferences.getBoolean(dumpingAllowanceKey, false); + } + + @Override + public File dumpHeap() { + return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 79221db7f..b15a38aae 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -5,11 +5,14 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Build; +import android.support.annotation.Nullable; import android.util.Log; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; +import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; import org.acra.ACRA; import org.acra.config.ACRAConfiguration; @@ -54,6 +57,7 @@ import io.reactivex.plugins.RxJavaPlugins; public class App extends Application { protected static final String TAG = App.class.toString(); + private RefWatcher refWatcher; @SuppressWarnings("unchecked") private static final Class[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class}; @@ -69,6 +73,13 @@ public class App extends Application { public void onCreate() { super.onCreate(); + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + refWatcher = installLeakCanary(); + // Initialize settings first because others inits can use its values SettingsActivity.initSettings(this); @@ -157,4 +168,13 @@ public class App extends Application { mNotificationManager.createNotificationChannel(mChannel); } + @Nullable + public static RefWatcher getRefWatcher(Context context) { + final App application = (App) context.getApplicationContext(); + return application.refWatcher; + } + + protected RefWatcher installLeakCanary() { + return RefWatcher.DISABLED; + } } diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 6cd79e2c9..d3e4a4b28 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -13,6 +13,7 @@ import android.view.View; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; +import com.squareup.leakcanary.RefWatcher; import icepick.Icepick; @@ -67,6 +68,14 @@ public abstract class BaseFragment extends Fragment { protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { } + @Override + public void onDestroy() { + super.onDestroy(); + + RefWatcher refWatcher = App.getRefWatcher(getActivity()); + if (refWatcher != null) refWatcher.watch(this); + } + /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 9a1ecd07a..ea6715f16 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -20,12 +20,14 @@ package org.schabi.newpipe; +import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; @@ -39,6 +41,7 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.Toast; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.fragments.BackPressable; @@ -211,6 +214,22 @@ public class MainActivity extends AppCompatActivity { } } + @SuppressLint("ShowToast") + private void onHeapDumpToggled(@NonNull MenuItem item) { + final boolean isHeapDumpEnabled = !item.isChecked(); + + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putBoolean(getString(R.string.allow_heap_dumping_key), isHeapDumpEnabled).apply(); + item.setChecked(isHeapDumpEnabled); + + final String heapDumpNotice; + if (isHeapDumpEnabled) { + heapDumpNotice = getString(R.string.enable_leak_canary_notice); + } else { + heapDumpNotice = getString(R.string.disable_leak_canary_notice); + } + Toast.makeText(getApplicationContext(), heapDumpNotice, Toast.LENGTH_SHORT).show(); + } /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @@ -232,6 +251,10 @@ public class MainActivity extends AppCompatActivity { inflater.inflate(R.menu.main_menu, menu); } + if (DEBUG) { + getMenuInflater().inflate(R.menu.debug_menu, menu); + } + ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); @@ -242,6 +265,17 @@ public class MainActivity extends AppCompatActivity { return true; } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem heapDumpToggle = menu.findItem(R.id.action_toggle_heap_dump); + if (heapDumpToggle != null) { + final boolean isToggled = PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(getString(R.string.allow_heap_dumping_key), false); + heapDumpToggle.setChecked(isToggled); + } + return super.onPrepareOptionsMenu(menu); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); @@ -262,6 +296,9 @@ public class MainActivity extends AppCompatActivity { case R.id.action_history: NavigationHelper.openHistory(this); return true; + case R.id.action_toggle_heap_dump: + onHeapDumpToggled(item); + return true; default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/res/menu/debug_menu.xml b/app/src/main/res/menu/debug_menu.xml new file mode 100644 index 000000000..448f9cf23 --- /dev/null +++ b/app/src/main/res/menu/debug_menu.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index c66b5b43f..ee784b5f7 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -83,6 +83,9 @@ last_orientation_landscape_key + + allow_heap_dumping_key + theme light_theme diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c2923a529..d9a2b3254 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -410,4 +410,8 @@ Normal Font Larger Font + + Monitor Leaks + Memory leak monitoring enabled, app may become unresponsive when heap dumping + Memory leak monitoring disabled