Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[firebase_admob] Fix firebase_admob issues caused by un-handled exception situations #1807

Closed
wants to merge 12 commits into from
Closed

[firebase_admob] Fix firebase_admob issues caused by un-handled exception situations #1807

wants to merge 12 commits into from

Conversation

archanpaul
Copy link

Description

While using BannerAd from fairly complex application which expects BannerAd to be disposed gracefully there are multiple un-handled exception situations in the plugin which are addressed in following commits in https://github.com/archanpaul/flutter-plugins/commits/fix-firebase_admob

commit 7dfeeac (HEAD -> fix-firebase_admob, origin/fix-firebase_admob)
Author: Archan Paul [email protected]
Date: Sat Jul 6 18:36:29 2019 +0530

Handle exception in _invokeBooleanMethod(...) and return false in case of exception.

commit a2b1eac
Author: Archan Paul [email protected]
Date: Sat Jul 6 17:51:34 2019 +0530

Interim fix for dangling BannerAd during dispose call.

Related code and log references

  BannerAd _adbmobBanner = BannerAd(
      adUnitId: BannerAd.testAdUnitId,
      size: AdSize.smartBanner,
      targetingInfo: _targetingInfo,
      listener: (MobileAdEvent event) {
        _subjectAdEvent.sink.add(event);
        switch (event) {
          case MobileAdEvent.clicked:
            break;
          case MobileAdEvent.closed:
            break;
          case MobileAdEvent.failedToLoad:
            break;
          case MobileAdEvent.impression:
            break;
          case MobileAdEvent.leftApplication:
            break;
          case MobileAdEvent.loaded:
            break;
          case MobileAdEvent.opened:
            break;
          default:
            break;
        }
      },
    );
  ...
  Future dispose() async {
    await _adbmobBanner?.dispose();
  }

E/flutter (29705): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: PlatformException(no_ad_for_id, dispose failed, no add exists for id=663181399, null)
E/flutter (29705): #0      StandardMethodCodec.decodeEnvelope 
package:flutter/…/services/message_codecs.dart:564
E/flutter (29705): #1      MethodChannel.invokeMethod 
package:flutter/…/services/platform_channel.dart:316
E/flutter (29705): <asynchronous suspension>
E/flutter (29705): #2      _invokeBooleanMethod 
package:firebase_admob/firebase_admob.dart:524
E/flutter (29705): <asynchronous suspension>
E/flutter (29705): #3      MobileAd.dispose 
package:firebase_admob/firebase_admob.dart:242
E/flutter (29705): #4      AdmobProvider.dispose 
package:fitzy_20190415/…/controllers/AdmobProvider.dart:39
E/flutter (29705): <asynchronous suspension>
E/flutter (29705): #5      _WidgetPromoState.dispose 
package:fitzy_20190415/…/widgets/widgetPromo.dart:36
E/flutter (29705): <asynchronous suspension>
E/flutter (29705): #6      StatefulElement.unmount 
package:flutter/…/widgets/framework.dart:4107
E/flutter (29705): #7      _InactiveElements._unmount 
package:flutter/…/widgets/framework.dart:1737
E/flutter (29705): #8      _InactiveElements._unmount.<anonymous closure> 
package:flutter/…/widgets/framework.dart:1735
E/flutter (29705): #9      MultiChildRenderObjectElement.visitChildren 
package:flutter/…/widgets/framework.dart:5181
E/flutter (29705): #10     _InactiveElements._unmount 
package:flutter/…/widgets/framework.dart:1733
E/flutter (29705): #11     ListIterable.forEach  (dart:_internal/iterable.dart:39:13)
E/flutter (29705): #12     _InactiveElements._unmountAll 
package:flutter/…/widgets/framework.dart:1746
E/flutter (29705): #13     BuildOwner.finalizeTree.<anonymous closure> 
package:flutter/…/widgets/framework.dart:2426
E/flutter (29705): #14     BuildOwner.lockState 
package:flutter/…/widgets/framework.dart:2258
E/flutter (29705): #15     BuildOwner.finalizeTree 
package:flutter/…/widgets/framework.dart:2425
E/flutter (29705): #16     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&SemanticsBinding&RendererBinding&WidgetsBinding.drawFrame 
package:flutter/…/widgets/binding.dart:702
E/flutter (29705): #17     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&SemanticsBinding&RendererBinding._handlePersistentFrameCallback 
package:flutter/…/rendering/binding.dart:285
E/flutter (29705): #18     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._invokeFrameCallback 
package:flutter/…/scheduler/binding.dart:1016
E/flutter (29705): #19     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding.handleDrawFrame 
package:flutter/…/scheduler/binding.dart:958
E/flutter (29705): #20     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._handleDrawFrame 
package:flutter/…/scheduler/binding.dart:874
E/flutter (29705): #21     _rootRun  (dart:async/zone.dart:1124:13)
E/flutter (29705): #22     _CustomZone.run  (dart:async/zone.dart:1021:19)
E/flutter (29705): #23     _CustomZone.runGuarded  (dart:async/zone.dart:923:7)
E/flutter (29705): #24     _invoke  (dart:ui/hooks.dart:236:10)
E/flutter (29705): #25     _drawFrame  (dart:ui/hooks.dart:194:3)
E/flutter (29705):

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here (e.g. I signed it!) and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@archanpaul
Copy link
Author

I signed CLA.

@cyanglaz cyanglaz changed the title Fix firebase_admob issues caused by un-handled exception situations [firebase_admob] Fix firebase_admob issues caused by un-handled exception situations Jul 9, 2019
@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

Copy link
Contributor

@bparrishMines bparrishMines left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @archanpaul

Thanks for the contribution! I left you a comment.

@collinjackson collinjackson added the submit queue The Flutter team is in the process of landing this PR. label Jul 15, 2019
@collinjackson collinjackson self-requested a review July 15, 2019 17:05
Copy link
Contributor

@collinjackson collinjackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution!

Can you please update pubspec.yaml and CHANGELOG for release?

It looks like it should be possible to write a unit test for the added behavior (right now all the unit tests assert that dispose() returns true). Could you please add that test here: https://github.com/flutter/plugins/blob/master/packages/firebase_admob/test/firebase_admob_test.dart

@archanpaul
Copy link
Author

Thanks @collinjackson for your feedback.

I think it is not easy to add a test case to simulate the exact scenario, which prompted me to added the patch. In the unit test we need to create a scenario in which, while Platform is working on loadBannerAd/showAd ( _bannerAd..load()..show() .... not listening and acting based on event callbacks) real Ad (over a non LTE network), the banner object is released without waiting (eg: timeout scenario in real app). This might result into a scenario of no_ad_for_id error, which should return false. Do you have better idea?

For the benefit of plugin users, you may consider merging the current patch now and later add a comprehensive unit-test related to "dispose() returns false".

Can you please update pubspec.yaml and CHANGELOG for release?

Do you want me to create 0.9.0+2 ?

@collinjackson
Copy link
Contributor

collinjackson commented Jul 16, 2019

Thanks @collinjackson for your feedback.

I think it is not easy to add a test case to simulate the exact scenario, which prompted me to added the patch. In the unit test we need to create a scenario in which, while Platform is working on loadBannerAd/showAd ( _bannerAd..load()..show() .... not listening and acting based on event callbacks) real Ad (over a non LTE network), the banner object is released without waiting (eg: timeout scenario in real app). This might result into a scenario of no_ad_for_id error, which should return false. Do you have better idea?

This is a unit test, not an integration test; you should be able to simulate the no_ad_for_id error in the setMockMethodCallHandler.

You could either throw a PlatformException in the unit test or (my preference) update the native side so that disposeAd returns false instead of a "no_ad_for_id" PlatformException. Then you won't need to worry about catching and handling an exception on the Dart side, and there's no need to test the special handing of the "no_ad_for_id" on the Dart side.

Can you please update pubspec.yaml and CHANGELOG for release?

Do you want me to create 0.9.0+2 ?

Yes.

@archanpaul archanpaul requested a review from amirh as a code owner July 16, 2019 15:13
@archanpaul
Copy link
Author

@collinjackson

You could either throw a PlatformException in the unit test or (my preference) update the native side so that disposeAd returns false instead of a "no_ad_for_id" PlatformException. Then you won't need to worry about catching and handling an exception on the Dart side, and there's no need to test the special handing of the "no_ad_for_id" on the Dart side.

I have added unit test case in 24cdbe6 where I throw PlatformException with corresponding error code.

I prefer Platform to make Dart side of plugin aware that no_ad_for_id happened and let Dart plugin make a note and take care of it (return false to upper layer).

Can you please update pubspec.yaml and CHANGELOG for release?

Done.

Copy link
Contributor

@collinjackson collinjackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. This is looking good, I edited your changelog and I have one other nit

size: AdSize.banner,
);
final int id = banner.id;
invalidAdId = banner.id;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what you're doing here is fragile and might cause the other disposeAd tests to fail depending on the order that the tests run, because they're re-using the same id testAdUnitId.

You might want to reset invalidAdId to null in setUp()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@collinjackson thanks for pointing out. I have updated in f409fc4.

@collinjackson collinjackson self-requested a review July 17, 2019 15:13
Copy link
Contributor

@collinjackson collinjackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking at this PR more carefully and I'm wondering what is the situation is causing dispose() to be called on a banner ad more than once. It seems to me that dispose() should be a Future<void> that can only be called once per ad, that calling dispose on an ad more than once should be prevented with a Dart assertion; in release builds where the assertion is disabled we could return early out of dispose() rather than sending the request over the bridge where we know a platform exception will be generated. This would help developers catch bugs in their code where they are keeping around references to banner ads after disposal.

If you feel that being able to call dispose() on ads multiple times is an important use case I'd love to see an example of usage.

Thanks.

@archanpaul
Copy link
Author

@collinjackson

I do not have any use-case where BannerAd.dispose() is required to be called multiple times. The dispose() should return immediately in case of non-existent adID .. there is no need to wait for PlatformException to happen which is anyway inevitable in this case.

Generally speaking, most of the app developers only care to know the bool return of dispose() and ensure that bannerAd = null after successful dispose.

if you want to enforce the app developer to know that dispose is called on a non-existent adID, instead of assert (which actually does not give any meaningful reason in log), you can either throw a Dart exception informing the developer the reason explicitly or throw the PlatformException as it is (though not intuitive).

I personally like to have hide() api call which should immediately hide the Ad instead of dispose(). As a Flutter app developer I will not like to await on dispose() to hide the Ad from UI. It is always preferable to have features like load() once (and sometimes await refresh() with corresponding event listner) the Ad and do show() & hide() as an when required to make Admob and this plugin intuitive/useful for the developer. This is my wish, not related to this PR.

@collinjackson
Copy link
Contributor

Migrated to firebase/flutterfire#37

collinjackson added a commit to collinjackson/flutterfire that referenced this pull request Aug 30, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes flutterfire submit queue The Flutter team is in the process of landing this PR.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants