darryncampbell / darryncampbell-cordova-plugin-intent

General purpose intent shim layer for cordova appliations on Android. Handles various techniques for sending and receiving intents.
MIT License
86 stars 134 forks source link

onIntent() does not function on 1st restore from background #51

Closed robwatt closed 6 years ago

robwatt commented 6 years ago

Hi,

My app requires the user to be able to open it from say the Photos app (sharing images). The user can select either a single image, or multiple images.

If I:

  1. launch the app through the LAUNCHER
  2. put the app in the background
  3. goto the Photos App
  4. share a photo to my app I will not receive an intent through onIntent()

If I repeat steps 2-4 again, I get an intent. It appears to be the correct one.

I have written a small Ionic app showing the issues

@Component({
  templateUrl: 'app.html'
})
export class MyApp implements OnDestroy {

  rootPage:any = HomePage;

  private onResumeSubscription: Subscription;

  constructor(platform: Platform,
              statusBar: StatusBar,
              splashScreen: SplashScreen) {

    platform.ready().then((readySource) => {
      console.log('platform.ready: ' + readySource);
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      statusBar.styleDefault();
      splashScreen.hide();

      // called on startup of the app - if opening using SEND or SEND_MULTIPLE will return the correct number of
      // clipItems & extras
      (<any>window).plugins.intentShim.getIntent(intent => {
        console.log('ready::getIntent');
        console.log(intent);
      }, (err) => {console.log(err);});

      this.onResumeSubscription = platform.resume.subscribe(() => {
        // resume is always called correctly.
        console.log('platform.resumed');

        // not called the 1st time from being put in the background after launching the app
        // repeated background/share attempts appear to yield the correct intent
        (<any>window).plugins.intentShim.onIntent(intent => {
          console.log('----- onIntent');
          console.log(intent);
        });
      });
    });
  }

  public ngOnDestroy(): void {
    this.onResumeSubscription.unsubscribe();
  }
}

These are the intent-filters I added

<intent-filter android:label="INTENT SEND">
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="*/*" />
</intent-filter>
<intent-filter android:label="INTENT SEND_MULTIPLE">
    <action android:name="android.intent.action.SEND_MULTIPLE" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="*/*" />
</intent-filter>

` cli packages: (/usr/local/lib/node_modules)

@ionic/cli-utils  : 1.19.2
ionic (Ionic CLI) : 3.20.0

global packages:

cordova (Cordova CLI) : 8.0.0 

local packages:

@ionic/app-scripts : 3.1.9
Cordova Platforms  : android 7.0.0
Ionic Framework    : ionic-angular 3.9.2

`

Are you aware of this issue, or have I not integrated your plugin correctly?

darryncampbell commented 6 years ago

Hi, this plugin's onIntent() method maps to Android's onNewIntent() method in the Activity class: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent) onNewIntent() is only called for activities that set their launch mode to singleTop. I always forget the details of how it works but in the past I have used https://www.mobomo.com/2011/06/android-understanding-activity-launchmode/ as a good guide. You may have more joy using the getIntent() method of this plugin in the onResume() callback to determine the intent that launched the app.

robwatt commented 6 years ago

Hi,

Ionic apps by default use 'singleTop'.

I switched onIntent() to use getIntent(), and the behaviour I see is incorrect.

When the application launches I correctly get an intent: android.intent.action.MAIN

However, when I try to share an image to the app, I expect onResume to get an intent of: android.intent.action.SEND, but I get android.intent.action.MAIN (every time).

I feel like getIntent will return the intent that launched the application, not the intent that is being used to bring the application into the foreground.

I have tried to switch my activity to singleTask, but something keeps rewriting my AndroidManifest.xml (I change it in a hook). However, I don't see how switching it to 'singleTask' would change the behvaiour. 'singleTop', should send the new Intent via onNewIntent().

robwatt commented 6 years ago

I think I found the source of my issue.

It appears that when you share something from one app to the other, Cordova will call onNewIntent() BEFORE execute().

This means, that your code for the action.equals("onIntent") is called AFTER onNewIntent(). So the new intent is never sent. However, because the context is now saved, when you do it a 2nd time, it has the context so it will fire off the event to the callback.

Here is a snippet of my log showing the timeline of events.

CallbackContext is assigned AFTER onNewIntent is called

::: Tried to share a photo with my app 06-19 15:26:58.453 4518 7156 I ActivityManager: START u0 {act=android.intent.action.SEND typ=image/jpeg flg=0x10000001 cmp=io.ionic.starter/.MainActivity clip={image/jpeg U:content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F66/REQUIRE_ORIGINAL/NONE/956197963} (has extras)} from uid 10086

::: Cordova fires off onNewIntent() 06-19 15:26:58.637 13468 13468 D Cordova Intents Shim: On New Intent: Intent { act=android.intent.action.SEND typ=image/jpeg flg=0x10400001 cmp=io.ionic.starter/.MainActivity clip={image/jpeg U:content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F66/REQUIRE_ORIGINAL/NONE/956197963} (has extras) }

::: There is no reference to this.onNewIntentCallbackContext, so anything in the if statement is skipped 06-19 15:26:58.637 13468 13468 D Cordova Intents Shim: On New Intent: null

06-19 15:26:58.777 13468 13468 D CordovaActivity: Started the activity. 06-19 15:26:58.778 13468 13468 D CordovaActivity: Resumed the activity. ::: Cordova then fires off execute(), where the context will get cached 06-19 15:26:58.832 13468 13558 D Cordova Intents Shim: Action: onIntent 06-19 15:26:58.832 13468 13558 D Cordova Intents Shim: Callback: org.apache.cordova.CallbackContext@7154835 06-19 15:26:58.832 13468 13558 D Cordova Intents Shim: Action :: onIntent 06-19 15:26:58.832 13468 13558 D Cordova Intents Shim: Set onNewIntentCallbackContext: org.apache.cordova.CallbackContext@7154835

::: next call to onNewIntent() will work since the callbackContext has been saved

I solved this by holding onto a reference of the Intent (from onNewIntent(), if the context is null. Then in execute() if that reference exists, I would fire the event (and null out the reference afterwards).

I don't like this solution, but I am still fairly new to Cordova life cycle. This was just to test a possible solution.

darryncampbell commented 6 years ago

Thanks, that's really interesting.

I don't have a better suggestion than what you describe i.e. maintaining a reference to the last onNewIntent received and providing that to the caller during execute(). If you're happy with it as a solution after doing some further testing can you please provide the code and I'll get it integrated with this plugin? Thanks.

darryncampbell commented 6 years ago

Closing this issue as fix has been merged. Thanks again.

robwatt commented 6 years ago

When you plan on releasing to NPM? Thanks.

darryncampbell commented 6 years ago

Hi, will be pushing to npm next week.