Details Activity leaked due to interstitial ad and some objects

Viewed 36

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

enter image description here

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

0 Answers
Related