In this app, I implemented an Admob interstitial ad in one activity, it should appeared after two clicks / entered the activity, Leak canary shows this ad causes a leak
the leak canary debug details below
┬───
│ GC Root: Global variable in native code
│
├─ com.google.android.gms.dynamic.ObjectWrapper instance
│ Leaking: UNKNOWN
│ Retaining 528 B in 1 objects
│ ↓ ObjectWrapper.a
│ ~
├─ com.google.android.gms.ads.internal.webview.y instance
│ Leaking: UNKNOWN
│ Retaining ١٥٨٫٤ kB in 3348 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mContext instance of com.google.android.gms.ads.internal.webview.aw,
│ wrapping com.mml.dummyapp_kotlin.BaseApplication
│ ↓ y.a
│ ~
├─ com.google.android.gms.ads.internal.webview.ac instance
│ Leaking: UNKNOWN
│ Retaining ١٠٫٥ kB in 239 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ p instance of com.google.android.gms.ads.internal.webview.aw, wrapping com.
│ mml.dummyapp_kotlin.BaseApplication
│ mContext instance of com.google.android.gms.ads.internal.webview.aw,
│ wrapping com.mml.dummyapp_kotlin.BaseApplication
│ ↓ ac.aa
│ ~~
├─ android.view.WindowManagerImpl instance
│ Leaking: UNKNOWN
│ Retaining ١٣٩٫٦ kB in 3175 objects
│ mContext instance of android.app.ContextImpl
│ ↓ WindowManagerImpl.mContext
│ ~~~~
├─ android.app.ContextImpl instance
│ Leaking: YES (ContextImpl.mOuterContext is an instance of com.mml.
│ dummyapp_kotlin.ui.DetailsActivity with Activity.mDestroyed true)
│ Retaining ٥٫١ kB in 59 objects
│ mAutofillClient instance of com.mml.dummyapp_kotlin.ui.DetailsActivity
│ with mDestroyed = true
│ mOuterContext instance of com.mml.dummyapp_kotlin.ui.DetailsActivity with
│ mDestroyed = true
│ ↓ ContextImpl.mAutofillClient
╰→ com.mml.dummyapp_kotlin.ui.DetailsActivity instance
Leaking: YES (ObjectWatcher was watching this because com.mml.
dummyapp_kotlin.ui.DetailsActivity received Activity#onDestroy() callback
and Activity#mDestroyed is true)
Retaining ٢٥٫٧ kB in 509 objects
key = 4b568c48-eada-49c9-a7c0-65b526d5a13d
watchDurationMillis = 5651
retainedDurationMillis = 639
mApplication instance of com.mml.dummyapp_kotlin.BaseApplication
mBase instance of androidx.appcompat.view.ContextThemeWrapper
METADATA
Build.VERSION.SDK_INT: 30
Build.MANUFACTURER: Xiaomi
LeakCanary version: 2.9.1
App process name: com.mml.dummyapp_kotlin
Class count: 26424
Instance count: 198534
Primitive array count: 141309
Object array count: 30666
Thread count: 133
Heap total bytes: 30099397
Bitmap count: 41
Bitmap total bytes: 42759033
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: open /data/user/0/com.mml.dummyapp_kotlin/databases/com.google.android.
datatransport.events
Db 2: closed /data/user/0/com.mml.
dummyapp_kotlin/databases/google_app_measurement_local.db
Db 3: open /data/user/0/com.mml.dummyapp_kotlin/databases/items_database
Db 4: open /data/user/0/com.mml.dummyapp_kotlin/no_backup/androidx.work.workdb
Stats: LruCache[maxSize=٣٠٠٠,hits=٥٩٥٧٦,misses=١٤٩٣٥١,hitRate=٢٨%]
RandomAccess[bytes=7181608,reads=149351,travel=67385031956,range=33697689,size=4
2408825]
Analysis duration: 16749 ms
the DetailsActivity
class DetailsActivity : AppCompatActivity() {
private var _binding: ActivityDetailsBinding? = null
private val binding get() = _binding!!
private var url: String? = null
private var title: String? = null
private var content: String? = null
private var youtubeThumbnailImageSrc: String? = null
private var youTubeLink: String? = null
private var youtubeThumbnailImageSetVisibility: Int? = null
private var html: Spannable? = null
private val postViewModel: PostViewModel by viewModels()
private var postItem: Item? = null
private var postFavoritesSaved = false
private var postFavoritesSavedId = 0
private var menuItem: MenuItem? = null
// private var mInterstitialAd: InterstitialAd? = null
private val args: DetailsActivityArgs by navArgs()
private var adRequest: AdRequest? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityDetailsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
binding.progressBar.visibility = View.VISIBLE
// postViewModel = ViewModelProvider(this)[PostViewModel::class.java]
Log.d(
TAG,
"onCreate checkSavedFavoritesItems: $postFavoritesSavedId"
)
// postItem = if (intent.extras!!.containsKey("postItem")) {
// intent.getParcelableExtra("postItem")!!
// } else {
// val favoriteItem: FavoritesEntity = intent.getParcelableExtra("favoriteItem")!!
// favoriteItem.item
// }
postItem = args.postItem
val document = postItem?.content.let { Jsoup.parse(it.toString()) }
// final Elements elements = document.select("img");
val element = document.body()
for (e in element.getElementsByClass("YOUTUBE-iframe-video")) {
youtubeThumbnailImageSrc = e.attr("data-thumbnail-src")
youTubeLink = e.attr("src")
Log.e("YouTube thumbnail", youtubeThumbnailImageSrc!!)
Log.e("Youtube link", youTubeLink!!)
}
if (youtubeThumbnailImageSrc == null) {
youtubeThumbnailImageSetVisibility = 8
}
url = postItem?.url
title = postItem?.title
content = postItem?.content
// blogImage = (ImageView) findViewById(R.id.blogImage);
binding.apply {
titleTextView.text = title
blogContent.movementMethod = LinkMovementMethod.getInstance()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.scrollView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
// Log.d("ScrollView","scrollX_"+scrollX+"_scrollY_"+scrollY+"_oldScrollX_"+oldScrollX+"_oldScrollY_"+oldScrollY);
if (scrollY > 0 && binding.fab.isShown) {
binding.fab.hide()
} else if (scrollY < 22) {
binding.fab.show()
}
}
} else {
binding.scrollView.viewTreeObserver.addOnScrollChangedListener {
val mScrollY: Int = binding.scrollView.scrollY
if (mScrollY > 0 && binding.fab.isShown) {
binding.fab.hide()
} else if (mScrollY < 22) {
binding.fab.show()
}
}
}
binding.fab.setOnClickListener {
val shareContent = """
$title
$url
""".trimIndent()
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "text/plain"
shareIntent.putExtra(Intent.EXTRA_TEXT, shareContent)
shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
try {
startActivity(Intent.createChooser(shareIntent, title))
} catch (exception: Exception) {
Log.e(TAG, "onCreate: $exception")
}
}
// String imageSrc = getIntent().getStringExtra("blogImage");
// Glide.with(getApplicationContext()).load(imageSrc).into(blogImage);
if (youtubeThumbnailImageSetVisibility != null) {
binding.youtubeThumbnailImage.visibility = youtubeThumbnailImageSetVisibility as Int
}
binding.youtubeThumbnailImage.adjustViewBounds = true
Picasso.get().load(youtubeThumbnailImageSrc).into(binding.youtubeThumbnailImage)
binding.youtubeThumbnailImage.setOnClickListener {
val youTube = Intent(Intent.ACTION_VIEW, Uri.parse(youTubeLink))
startActivity(youTube)
}
val imageGetter = PicassoImageGetter(binding.blogContent, this)
html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY, imageGetter, null) as Spannable
} else {
content?.let {
HtmlCompat.fromHtml(
it,
HtmlCompat.FROM_HTML_MODE_LEGACY,
imageGetter,
null
)
} as Spannable
}
binding.blogContent.text = html
binding.visitSite.setOnClickListener {
openCustomTab(
this,
Uri.parse(url)
)
}
binding.progressBar.visibility = View.GONE
}
private fun clearObjects() {
url = null
title = null
content = null
postItem = null
// postFavoritesSaved = null
// postFavoritesSavedId = null
html = null
menuItem = null
}
// override fun onBackPressed() {
// super.onBackPressed()
//
// }
private fun loadAndShowInterstitial() {
adRequest = Constants.callAndBuildAdRequest()
if (clickNumber == 2) {
InterstitialAd.load(this, "ca-app-pub-3940256099942544/1033173712", adRequest!!,
object : InterstitialAdLoadCallback() {
override fun onAdFailedToLoad(adError: LoadAdError) {
adError.toString().let { Log.d(TAG, it) }
// loadAndShowInterstitial(adRequest)
// loadAndShowInterstitial()
// this@DetailsActivity.mInterstitialAd = null
}
override fun onAdLoaded(interstitialAd: InterstitialAd) {
Log.d(TAG, "Ad was loaded.")
// this@DetailsActivity.mInterstitialAd = interstitialAd
interstitialAd.show(this@DetailsActivity)
interstitialAd.fullScreenContentCallback =
object : FullScreenContentCallback() {
override fun onAdClicked() {
// Called when a click is recorded for an ad.
Log.d(TAG, "Ad was clicked.")
clickNumber = 0
}
override fun onAdDismissedFullScreenContent() {
// Called when ad is dismissed.
Log.d(TAG, "Ad dismissed fullscreen content.")
clickNumber = 0
// mInterstitialAd = null
}
override fun onAdFailedToShowFullScreenContent(adError: AdError) {
// Called when ad fails to show.
Log.e(TAG, "Ad failed to show fullscreen content.")
clickNumber = 0
// mInterstitialAd = null
}
override fun onAdImpression() {
// Called when an impression is recorded for an ad.
Log.d(TAG, "Ad recorded an impression.")
clickNumber = 0
}
override fun onAdShowedFullScreenContent() {
// Called when ad is shown.
Log.d(TAG, "Ad showed fullscreen content.")
clickNumber = 0
}
}
}
})
} else {
clickNumber = clickNumber.plus(1)
Log.d(TAG, "The interstitial ad wasn't ready yet. $clickNumber")
}
}
override fun onStart() {
super.onStart()
loadAndShowInterstitial()
// mInterstitialAd?.show(this)
}
// override fun onPause() {
// super.onPause()
// this@DetailsActivity.finish()
// }
// protected fun finalize() {
// // finalization logic
// clickNumber = 0
// }
//
// override fun onStop() {
// super.onStop()
override fun onDestroy() {
super.onDestroy()
// mInterstitialAd = null
binding.fab.setOnClickListener(null)
clearObjects()
adRequest = null
_binding = null
}
// this@DetailsActivity.finish()
// }
private fun checkSavedFavoritesItems(menuItem: MenuItem?) {
postViewModel.readFavoritePosts.observe(this) { favoritesEntity ->
try {
for (savedPost in favoritesEntity) {
if (savedPost.item.id == postItem?.id) {
menuItem?.setIcon(R.drawable.ic_favorite)
postFavoritesSavedId = savedPost.id
Log.d(
TAG,
"checkSavedFavoritesItems: $postFavoritesSavedId"
)
postFavoritesSaved = true
}
}
} catch (exception: Exception) {
Log.e(
TAG,
"checkSavedFavoritesItems: " + exception.message
)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.details_menu, menu)
menuItem = menu.findItem(R.id.action_add_to_favorites)
checkSavedFavoritesItems(menuItem)
return true
}
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.itemId == R.id.action_add_to_favorites && !postFavoritesSaved) {
saveTogFavorites(menuItem)
} else if (menuItem.itemId == R.id.action_add_to_favorites && postFavoritesSaved) {
removePostFromFavorites(menuItem)
} else if (menuItem.itemId == R.id.action_share) {
val shareActionProvider =
MenuItemCompat.getActionProvider(menuItem) as ShareActionProvider
shareActionProvider.setShareIntent(createShareIntent())
return true
} else if (menuItem.itemId == R.id.copyTheLink) {
val clipboardManager = this.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("link", url)
clipboardManager.setPrimaryClip(clipData)
Toast.makeText(this@DetailsActivity, getString(R.string.linkCopied), Toast.LENGTH_LONG)
.show()
return true
}
return super.onOptionsItemSelected(menuItem)
}
override fun onSupportNavigateUp(): Boolean {
Log.d(TAG, "onSupportNavigateUp: called")
clearObjects()
intent.extras?.clear()
this.finish()
return true
}
private fun saveTogFavorites(menuItem: MenuItem) {
val favoritesEntity = postItem?.let { FavoritesEntity(0, it) }
if (favoritesEntity != null) {
postViewModel.insertFavorites(favoritesEntity)
}
menuItem.setIcon(R.drawable.ic_favorite)
Snackbar.make(binding.root, "Saved", Snackbar.LENGTH_LONG).show()
postFavoritesSaved = true
}
private fun removePostFromFavorites(menuItem: MenuItem) {
val favoritesEntity = postItem?.let { FavoritesEntity(postFavoritesSavedId, it) }
Log.d(
TAG,
"checkSavedFavoritesItems: $postFavoritesSavedId"
)
if (favoritesEntity != null) {
postViewModel.deleteFavoritePost(favoritesEntity)
}
menuItem.setIcon(R.drawable.ic_favorite_border)
Snackbar.make(
binding.root,
"Post deleted from favorites", Snackbar.LENGTH_LONG
).show()
postFavoritesSaved = false
}
private fun createShareIntent(): Intent {
val shareContent = """
$title
$url
""".trimIndent()
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "text/plain"
shareIntent.putExtra(Intent.EXTRA_TEXT, shareContent)
// shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
return shareIntent
}
companion object {
@JvmStatic
private var clickNumber: Int = 0
private const val TAG = "DetailsActivity"
@JvmStatic
private fun openCustomTab(
context: Context?,
uri: Uri?
) {
// Here is a method that returns the chrome package name
val packageName: String? = context?.let { CustomTabsHelper.getPackageNameToUse(it) }
val builder = CustomTabsIntent.Builder()
val mCustomTabsIntent: CustomTabsIntent = builder
.setShowTitle(true)
.build()
// builder.setToolbarColor(ContextCompat.getColor(appCompatActivity, R.color.colorPrimary));
if (packageName != null) {
mCustomTabsIntent.intent.setPackage(packageName)
}
mCustomTabsIntent.launchUrl(context!!, uri!!)
}
}
}
While Android Studio Profiler tool shows other leaks in the same activity, some of the fields related to it and others to this class public class ReportFragment extends android.app.Fragment I don't know which part exactly uses this class but I guessed it's related to the fragment that I navigate from it to Details activity
I tried to fix these leaks with two methods, one is clearObjects() I invalidate most of the objects, and the second onSupportNavigateUp I called clearObjects() from it besides clear nav args extras and finishing the activity, but unfortunately, all this methods doesn't help
