<lambda>null1 package leakcanary.internal
2 
3 import android.app.Activity
4 import android.app.Application
5 import android.app.Application.ActivityLifecycleCallbacks
6 import android.app.UiModeManager
7 import android.content.ComponentName
8 import android.content.Context
9 import android.content.Context.UI_MODE_SERVICE
10 import android.content.Intent
11 import android.content.pm.ApplicationInfo
12 import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
13 import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
14 import android.content.pm.PackageManager.DONT_KILL_APP
15 import android.content.pm.ShortcutInfo.Builder
16 import android.content.pm.ShortcutManager
17 import android.content.res.Configuration
18 import android.graphics.drawable.Icon
19 import android.os.Build.VERSION
20 import android.os.Build.VERSION_CODES
21 import android.os.Handler
22 import android.os.HandlerThread
23 import com.squareup.leakcanary.core.BuildConfig
24 import com.squareup.leakcanary.core.R
25 import leakcanary.AppWatcher
26 import leakcanary.EventListener.Event
27 import leakcanary.GcTrigger
28 import leakcanary.LeakCanary
29 import leakcanary.OnObjectRetainedListener
30 import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope
31 import leakcanary.internal.HeapDumpControl.ICanHazHeap.Yup
32 import leakcanary.internal.InternalLeakCanary.FormFactor.MOBILE
33 import leakcanary.internal.InternalLeakCanary.FormFactor.TV
34 import leakcanary.internal.InternalLeakCanary.FormFactor.WATCH
35 import leakcanary.internal.friendly.mainHandler
36 import leakcanary.internal.friendly.noOpDelegate
37 import leakcanary.internal.tv.TvOnRetainInstanceListener
38 import shark.SharkLog
39 
40 internal object InternalLeakCanary : (Application) -> Unit, OnObjectRetainedListener {
41 
42   private const val DYNAMIC_SHORTCUT_ID = "com.squareup.leakcanary.dynamic_shortcut"
43 
44   private lateinit var heapDumpTrigger: HeapDumpTrigger
45 
46   // You're wrong https://discuss.kotlinlang.org/t/object-or-top-level-property-name-warning/6621/7
47   @Suppress("ObjectPropertyName")
48   private var _application: Application? = null
49 
50   val application: Application
51     get() {
52       check(_application != null) {
53         "LeakCanary not installed, see AppWatcher.manualInstall()"
54       }
55       return _application!!
56     }
57 
58   // BuildConfig.LIBRARY_VERSION is stripped so this static var is how we keep it around to find
59   // it later when parsing the heap dump.
60   @Suppress("unused")
61   @JvmStatic
62   private var version = BuildConfig.LIBRARY_VERSION
63 
64   @Volatile
65   var applicationVisible = false
66     private set
67 
68   private val isDebuggableBuild by lazy {
69     (application.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
70   }
71 
72   fun createLeakDirectoryProvider(context: Context): LeakDirectoryProvider {
73     val appContext = context.applicationContext
74     return LeakDirectoryProvider(appContext, {
75       LeakCanary.config.maxStoredHeapDumps
76     }, {
77       LeakCanary.config.requestWriteExternalStoragePermission
78     })
79   }
80 
81   internal enum class FormFactor {
82     MOBILE,
83     TV,
84     WATCH,
85   }
86 
87   val formFactor by lazy {
88     return@lazy when ((application.getSystemService(UI_MODE_SERVICE) as UiModeManager).currentModeType) {
89       Configuration.UI_MODE_TYPE_TELEVISION -> TV
90       Configuration.UI_MODE_TYPE_WATCH -> WATCH
91       else -> MOBILE
92     }
93   }
94 
95   val isInstantApp by lazy {
96     VERSION.SDK_INT >= VERSION_CODES.O && application.packageManager.isInstantApp
97   }
98 
99   val onRetainInstanceListener by lazy {
100     when (formFactor) {
101       TV -> TvOnRetainInstanceListener(application)
102       else -> DefaultOnRetainInstanceListener()
103     }
104   }
105 
106   var resumedActivity: Activity? = null
107 
108   private val heapDumpPrefs by lazy {
109     application.getSharedPreferences("LeakCanaryHeapDumpPrefs", Context.MODE_PRIVATE)
110   }
111 
112   internal var dumpEnabledInAboutScreen: Boolean
113     get() {
114       return heapDumpPrefs
115         .getBoolean("AboutScreenDumpEnabled", true)
116     }
117     set(value) {
118       heapDumpPrefs
119         .edit()
120         .putBoolean("AboutScreenDumpEnabled", value)
121         .apply()
122     }
123 
124   override fun invoke(application: Application) {
125     _application = application
126 
127     checkRunningInDebuggableBuild()
128 
129     AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
130 
131     val gcTrigger = GcTrigger.Default
132 
133     val configProvider = { LeakCanary.config }
134 
135     val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
136     handlerThread.start()
137     val backgroundHandler = Handler(handlerThread.looper)
138 
139     heapDumpTrigger = HeapDumpTrigger(
140       application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger,
141       configProvider
142     )
143     application.registerVisibilityListener { applicationVisible ->
144       this.applicationVisible = applicationVisible
145       heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
146     }
147     registerResumedActivityListener(application)
148     addDynamicShortcut(application)
149 
150     // We post so that the log happens after Application.onCreate()
151     mainHandler.post {
152       // https://github.com/square/leakcanary/issues/1981
153       // We post to a background handler because HeapDumpControl.iCanHasHeap() checks a shared pref
154       // which blocks until loaded and that creates a StrictMode violation.
155       backgroundHandler.post {
156         SharkLog.d {
157           when (val iCanHasHeap = HeapDumpControl.iCanHasHeap()) {
158             is Yup -> application.getString(R.string.leak_canary_heap_dump_enabled_text)
159             is Nope -> application.getString(
160               R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
161             )
162           }
163         }
164       }
165     }
166   }
167 
168   private fun checkRunningInDebuggableBuild() {
169     if (isDebuggableBuild) {
170       return
171     }
172 
173     if (!application.resources.getBoolean(R.bool.leak_canary_allow_in_non_debuggable_build)) {
174       throw Error(
175         """
176         LeakCanary in non-debuggable build
177 
178         LeakCanary should only be used in debug builds, but this APK is not debuggable.
179         Please follow the instructions on the "Getting started" page to only include LeakCanary in
180         debug builds: https://square.github.io/leakcanary/getting_started/
181 
182         If you're sure you want to include LeakCanary in a non-debuggable build, follow the
183         instructions here: https://square.github.io/leakcanary/recipes/#leakcanary-in-release-builds
184       """.trimIndent()
185       )
186     }
187   }
188 
189   private fun registerResumedActivityListener(application: Application) {
190     application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks by noOpDelegate() {
191       override fun onActivityResumed(activity: Activity) {
192         resumedActivity = activity
193       }
194 
195       override fun onActivityPaused(activity: Activity) {
196         if (resumedActivity === activity) {
197           resumedActivity = null
198         }
199       }
200     })
201   }
202 
203   @Suppress("ReturnCount")
204   private fun addDynamicShortcut(application: Application) {
205     if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
206       return
207     }
208     if (!application.resources.getBoolean(R.bool.leak_canary_add_dynamic_shortcut)) {
209       return
210     }
211     if (isInstantApp) {
212       // Instant Apps don't have access to ShortcutManager
213       return
214     }
215     val shortcutManager = application.getSystemService(ShortcutManager::class.java)
216     if (shortcutManager == null) {
217       // https://github.com/square/leakcanary/issues/2430
218       // ShortcutManager null on Android TV
219       return
220     }
221     val dynamicShortcuts = shortcutManager.dynamicShortcuts
222 
223     val shortcutInstalled =
224       dynamicShortcuts.any { shortcut -> shortcut.id == DYNAMIC_SHORTCUT_ID }
225 
226     if (shortcutInstalled) {
227       return
228     }
229 
230     val mainIntent = Intent(Intent.ACTION_MAIN, null)
231     mainIntent.addCategory(Intent.CATEGORY_LAUNCHER)
232     mainIntent.setPackage(application.packageName)
233     val activities = application.packageManager.queryIntentActivities(mainIntent, 0)
234       .filter {
235         it.activityInfo.name != "leakcanary.internal.activity.LeakLauncherActivity"
236       }
237 
238     if (activities.isEmpty()) {
239       return
240     }
241 
242     val firstMainActivity = activities.first()
243       .activityInfo
244 
245     // Displayed on long tap on app icon
246     val longLabel: String
247     // Label when dropping shortcut to launcher
248     val shortLabel: String
249 
250     val leakActivityLabel = application.getString(R.string.leak_canary_shortcut_label)
251 
252     if (activities.isEmpty()) {
253       longLabel = leakActivityLabel
254       shortLabel = leakActivityLabel
255     } else {
256       val firstLauncherActivityLabel = if (firstMainActivity.labelRes != 0) {
257         application.getString(firstMainActivity.labelRes)
258       } else {
259         application.packageManager.getApplicationLabel(application.applicationInfo)
260       }
261       val fullLengthLabel = "$firstLauncherActivityLabel $leakActivityLabel"
262       // short label should be under 10 and long label under 25
263       if (fullLengthLabel.length > 10) {
264         if (fullLengthLabel.length <= 25) {
265           longLabel = fullLengthLabel
266           shortLabel = leakActivityLabel
267         } else {
268           longLabel = leakActivityLabel
269           shortLabel = leakActivityLabel
270         }
271       } else {
272         longLabel = fullLengthLabel
273         shortLabel = fullLengthLabel
274       }
275     }
276 
277     val componentName = ComponentName(firstMainActivity.packageName, firstMainActivity.name)
278 
279     val shortcutCount = dynamicShortcuts.count { shortcutInfo ->
280       shortcutInfo.activity == componentName
281     } + shortcutManager.manifestShortcuts.count { shortcutInfo ->
282       shortcutInfo.activity == componentName
283     }
284 
285     if (shortcutCount >= shortcutManager.maxShortcutCountPerActivity) {
286       return
287     }
288 
289     val intent = LeakCanary.newLeakDisplayActivityIntent()
290     intent.action = "Dummy Action because Android is stupid"
291     val shortcut = Builder(application, DYNAMIC_SHORTCUT_ID)
292       .setLongLabel(longLabel)
293       .setShortLabel(shortLabel)
294       .setActivity(componentName)
295       .setIcon(Icon.createWithResource(application, R.mipmap.leak_canary_icon))
296       .setIntent(intent)
297       .build()
298 
299     try {
300       shortcutManager.addDynamicShortcuts(listOf(shortcut))
301     } catch (ignored: Throwable) {
302       SharkLog.d(ignored) {
303         "Could not add dynamic shortcut. " +
304           "shortcutCount=$shortcutCount, " +
305           "maxShortcutCountPerActivity=${shortcutManager.maxShortcutCountPerActivity}"
306       }
307     }
308   }
309 
310   override fun onObjectRetained() = scheduleRetainedObjectCheck()
311 
312   fun scheduleRetainedObjectCheck() {
313     if (this::heapDumpTrigger.isInitialized) {
314       heapDumpTrigger.scheduleRetainedObjectCheck()
315     }
316   }
317 
318   fun onDumpHeapReceived(forceDump: Boolean) {
319     if (this::heapDumpTrigger.isInitialized) {
320       heapDumpTrigger.onDumpHeapReceived(forceDump)
321     }
322   }
323 
324   fun setEnabledBlocking(
325     componentClassName: String,
326     enabled: Boolean
327   ) {
328     val component = ComponentName(application, componentClassName)
329     val newState =
330       if (enabled) COMPONENT_ENABLED_STATE_ENABLED else COMPONENT_ENABLED_STATE_DISABLED
331     // Blocks on IPC.
332     application.packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP)
333   }
334 
335   fun sendEvent(event: Event) {
336     for(listener in LeakCanary.config.eventListeners) {
337       listener.onEvent(event)
338     }
339   }
340 
341   private const val LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump"
342 }
343