Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-19567] Last change made after useractivitywillsave does not make it to other device

GitHub Issuen/a
TypeBug
PriorityHigh
StatusClosed
ResolutionFixed
Resolution Date2015-10-29T20:38:40.000+0000
Affected Version/sRelease 5.0.0
Fix Version/sRelease 5.2.0
ComponentsiOS
Labelshandoff
ReporterFokke Zandbergen
AssigneeChee Kiat Ng
Created2015-09-22T14:22:37.000+0000
Updated2016-02-23T10:47:24.000+0000

Description

While I was working on https://github.com/appcelerator-developer-relations/appc-sample-handoff when our SDK and Apple's were still not GA and since they became GA it seems like the behaviour around useractivitywillsave has changed: * Before it fired directly after setting needsSave:true but now it only does before the activity is handed off. This is expected behaviour, so I guess OK. * Before I any change I did to the activity's userInfo in the event listener for useractivitywillsave would be received by the continueactivity event on the other device. But now it is no longer. This is not expected behaviour, so definitely a bug! To reproduce: 1. Build https://github.com/appcelerator-developer-relations/appc-sample-handoff to 2 devices 2. Make a change to the needsSave tab's title/message and continue on the other device 3. Check the logs on both and see that while the userInfo was updated in the useractivitywillsave on the first, it is not received in continueactivity on the other. This seems like a serious bug because this makes handoff not usable for dynamic content.

Comments

  1. Chee Kiat Ng 2015-09-29

    [~fokkezb], try out my classic app.js sample code
       var win = Ti.UI.createWindow({
       	title: 'testActibity',
       	backgroundColor: 'white'
       });
       
       var btn = Ti.UI.createButton({
       	top: 40,
       	title: 'make activity current'
       });
       
       btn.addEventListener('click',function(e){
       	if (activity.supported) {
       	    activity.becomeCurrent();
       	}
       });
       
       var btn2 = Ti.UI.createButton({
       	top: 80,
       	title: 'needs save'
       });
       
       btn2.addEventListener('click',function(e){
       	activity.needsSave = true;
       });
       
       win.add(btn);
       win.add(btn2);
       win.open();
       
       var activity = Ti.App.iOS.createUserActivity({
           activityType: 'com.appcelerator.sg.SGTestHandoff2.needssave',
           title: 'first activity',
           userInfo: {
               body: 'first activity'
           }
       });
       activity.addEventListener('useractivitywillsave', onUseractivitywillsave);
       activity.addEventListener('useractivitywascontinued', onUseractivitywascontinued);
       
       Ti.App.iOS.addEventListener('continueactivity', onContinueactivity);
       
       
       
       /**
        * Called when our activity was continued on another device.
        */
       function onContinueactivity(e) {
       
       	// We only respond to the writing activity
       	if (e.activityType !== 'com.appcelerator.sg.SGTestHandoff2.needssave') {
       		return;
       	}
       
       	alert('Ti.App.iOS:continueactivity: ' + JSON.stringify(e));
       
       	// Update our content with activity handed off to us
       
       }
       
       /**
        * Called before our activity is continued on another device so we can update its state.
        */
       function onUseractivitywillsave(e) {
       	alert('Ti.App.iOS.UserActivity:useractivitywillsave ' + JSON.stringify(e));
       
       	activity.title = 'before you go';
       
       	activity.userInfo = {
       		body: 'i changed my body text'
       	};
       
       	Ti.API.info('Updated activity: '+ activity.title +', ' + activity.userInfo.body);
       }
       
       /**
        * Called when our activity was continued on another device.
        */
       function onUseractivitywascontinued(e) {
       	alert('Ti.App.iOS.UserActivity:useractivitywascontinued ' + JSON.stringify(e));
       }
       
    and include in tiapp.xml
        <key>NSUserActivityTypes</key>
       		        <array>
       		          <string>com.appcelerator.sg.SGTestHandoff2.needssave</string>
       		        </array>
       
    It seems to be behaving correctly.

    Steps to verify

    1. install app on 2 devices 2. on device A, open the app, and press "make activity current" 3. An alert will pop up, indicating will save event with the current activity info 4. on device B, continue the activity via the lock screen 5. alert will pop up in device A, indicating will save event with the new activity info 6. close the alert 7. alert will pop up in device A, indicating new activity is continued on another device 8. alert will pop up in device B, indicating new activity is continuing, with NEW activity info
  2. Chee Kiat Ng 2015-09-29

    Though i noticed, i completely did not use 'needSave' property. i'm assuming that's true by default.
  3. Fokke Zandbergen 2015-09-29

    [~cng] useractivitywillsave will always fire right after becomeCurrent() regardless of needsSave, but indeed I see that in your sample it is also fires when you handover without first setting needsSave, while I've also seen it not do that. It seems to be very unpredictable. I've modified your sample to better demonstrates what I can still demonstrate going wrong:
       var win = Ti.UI.createWindow({
       	title: 'testActibity',
       	backgroundColor: 'white'
       });
        
       var btn = Ti.UI.createButton({
       	top: 40,
       	title: 'make activity current'
       });
        
       btn.addEventListener('click',function(e){
       	if (activity.supported) {
       	    activity.becomeCurrent();
       	}
       });
        
       var btn2 = Ti.UI.createButton({
       	top: 80,
       	title: 'needs save'
       });
        
       btn2.addEventListener('click',function(e){
       	activity.needsSave = true;
       
       	log.value = log.value + 'Set activity.needsSave=true\n\n';
       });
        
       var btn3 = Ti.UI.createButton({
       	top: 120,
       	title: 'clear log'
       });
        
       btn3.addEventListener('click',function(e){
       	log.value = '';
       });
       
       var log = Ti.UI.createTextArea({
       	top: 160,
       	right: 0,
       	bottom: 0,
       	left: 0,
       	font: {
       		fontFamily: 'Courier'
       	},
       	editable: false
       });
        
       win.add(btn);
       win.add(btn2);
       win.add(btn3);
       win.add(log);
       win.open();
        
       var activity = Ti.App.iOS.createUserActivity({
           activityType: 'com.appcelerator.sg.SGTestHandoff2.needssave',
           title: 'Initial title',
           userInfo: {
               body: 'Initial body'
           }
       });
       activity.addEventListener('useractivitywillsave', onUseractivitywillsave);
        
       Ti.App.iOS.addEventListener('continueactivity', onContinueactivity);
        
       /**
        * Called when our activity was continued on another device.
        */
       function onContinueactivity(e) {
        
       	// We only respond to the writing activity
       	if (e.activityType !== 'com.appcelerator.sg.SGTestHandoff2.needssave') {
       		return;
       	}
       
       	log.value = log.value + 'continueactivity: ' + JSON.stringify(e) + '\n\n';
       }
        
       /**
        * Called before our activity is continued on another device so we can update its state.
        */
       function onUseractivitywillsave(e) {
       	log.value = log.value + 'useractivitywillsave: ' + JSON.stringify(e) + '\n\n';
       
       	var time = 'Time: ' + Date.now().toString();
        
       	activity.title = time;
       	activity.userInfo = {
       		body: time
       	};
       
       	log.value = log.value + 'Updated activity with "' + time + '": ' + JSON.stringify({
       		title: activity.title,
       		userinfo: activity.userInfo
       	}) + '\n\n';
       }
       
    *Steps* 1. Make current on first device 2. Handoff on other device 3. See that the activity the other device received does not have the timestamp set by the first in useractivitywillsave *Logs Device 1*
       useractivitywillsave: {"title":"Initial title","activityType":"com.appcelerator.sg.SGTestHandoff2.needssave","userInfo":{"body":"Initial body"},"bubbles":true,"type":"useractivitywillsave","source":{},"cancelBubble":false}
       
       Updated activity with "Time: 1443512899529": {"title":"Initial title","userinfo":{"body":"Initial body"}}
       
       useractivitywillsave: {"title":"Time: 1443512899529","activityType":"com.appcelerator.sg.SGTestHandoff2.needssave","userInfo":{"body":"Time: 1443512899529"},"bubbles":true,"type":"useractivitywillsave","source":{},"cancelBubble":false}
       
       Updated activity with "Time: 1443512907367": {"title":"Time: 1443512907367","userinfo":{"body":"Time: 1443512907367"}}
       
    *Log Device 2*
       continueactivity: {"title":"Time: 1443512899529","activityType":"com.appcelerator.sg.SGTestHandoff2.needssave","userInfo":{"body":"Time: 1443512899529"},"bubbles":true,"type":"continueactivity","source":{},"cancelBubble":false}
       
    As you can see when I update the activity on device 1 even on that device after setting activity.title a get on that same variable returns the old value. So there seems to be something wrong there. Sometimes the get does give the right value, sometimes only for the title, other times only the body. Very odd!
  4. Chee Kiat Ng 2015-10-01

    Hm. Looking at the code, i think the reason is because of this line: https://github.com/appcelerator/titanium_mobile/blob/master/iphone/Classes/TiAppiOSUserActivityProxy.m#L189 see it fires the event to JS, after which it continues to do what it is supposed to do, to continue activity on another device. So it looks like it won't wait for you to make changes to the activity. that's why you see the race condition. Some thoughts have to be put in on this, this may be a little tricky.
  5. Fokke Zandbergen 2015-10-01

    The workaround is to update the activity when you set needsSave:true, but that's.. well, ugly. Could we do a callback or something?
  6. Chee Kiat Ng 2015-10-02

    We could somehow hack it such that the main thread waits for the JS to complete a callback or something, but 1. That sounds like a bad idea 2. i don't think we have ever done something like that in the sdk We might have to consider deprecating and removing this event altogether. users could stick to "useractivitywascontinued", used for remembering the state the app was in before continued on to another device. Because i relate a user activity to a app state, and can only imagine a small use case whereby you want to change the activity content on the last minute before it's handed off.
  7. Fokke Zandbergen 2015-10-02

    Well if we remove it we break parity with the native API. *The* example for handoff is starting an email on your iPhone and continue it on your iPad or Mac. For this typical use case it *is* very important that you can update the activity before it is continued on the other device. I really need we do need to fix this. And until we do we should inform people about the workaround, which is to update the activity when they set needsSave:true. I will update the blog post that will go out today with that and link to this issue.
  8. Fokke Zandbergen 2015-10-03

    [~ben.bahrenburg@gmail.com] since you implemented this (right?) any ideas on this issue?
  9. Ben Bahrenburg 2015-10-03

    [~fokkezb] what you have as the workaround is the actual behavior. Please reference http://stackoverflow.com/questions/26715531/nsuseractivity-handoff-not-working-for-custom-data If you made any chances you need to update the needsSave property. Sounds like this is just a documentation update. Alternatively we could break with the native API and set the property for the developer. Would suggest the first strategy.
  10. Fokke Zandbergen 2015-10-03

    [~ben.bahrenburg@gmail.com] I know you need to set needsSave:true when you have changes that need to be saved before another device continues the activity. That's not the point. The point is that Apple is (somewhat) [clear](https://developer.apple.com/library/mac/documentation/UserExperience/Conceptual/Handoff/AdoptingHandoff/AdoptingHandoff.html#//apple_ref/doc/uid/TP40014338-CH2-SW14) that you shouldn't actually update the activity until the useractivitywillsave event. {quote}To update the activity object’s userInfo dictionary efficiently, configure its delegate and set its needsSave property to YES whenever the userInfo needs updating. At appropriate times, Handoff invokes the delegate’s userActivityWillSave: callback, and the delegate can update the activity state.{quote} And *that* is not (always) working in our Titanium implementation because the Obj-C thread doesn't wait for the JS event to be finished before allowing the other device to continue. Which causes the other device to not always get the updated information but rather the previous state.
  11. Ben Bahrenburg 2015-10-03

    [~fokkezb] you are not going to be able to have the Obj-C wait for the JS event to finish. This is just a delegate event that is fired in native, that then raises the event in JS. Truthfully you should be re-building your activity on the other side of your application if there is concern over latency.
  12. Fokke Zandbergen 2015-10-05

    I don't get what you mean with re-building. Surely we need to be able to get this working as Apple has documented it should work?
  13. Ben Bahrenburg 2015-10-05

    [~fokkezb] see [~cng] comment he sums up your options well. Even read your second to last statement with some thought, i.e. you want ObjC to wait for JS in a delegate call.... think about what you just said. I think the key is how do you work around this in your example. Personally when I coded the hand-off part of my application, using this code, I never had to use this event.
  14. Fokke Zandbergen 2015-10-06

    I know it works fine if you update the activity before/when you set needsSave:true it would just be better if we can make our implementation follow Apple's best practices. If that is technically impossible then we should indeed remove the useractivitywillsave event and tell people that in contrast to what Apple says they need to update the activity where they set needsSave:true.
  15. Chee Kiat Ng 2015-10-28

    PR here: https://github.com/appcelerator/titanium_mobile/pull/7357 to deprecate useractivitywillsave to avoid confusion.
  16. Fokke Zandbergen 2015-10-28

    [~cng] Added a few comments to the PR. If we don't expect to ever be able to restore useractivitywillsave it would be good to look into if it wouldn't be better if we call needsSave automatically whenever the user makes a change to the user activity.
  17. Chee Kiat Ng 2015-10-28

    [~fokkezb] it sure sounds reasonable. maybe new a ticket for that?
  18. Fokke Zandbergen 2015-10-29

    Done: TIMOB-19567
  19. Hans Knöchel 2015-10-29

    PR approved!
  20. Harry Bryant 2016-02-22

    Verified as fixed, This was tested using two iOS9 devices, and an iOS9 to iOS8 device. info made by useractivitywillsave returns correct values on multiple tries, and the information received by Device B is reflected correctly. Tested on: iPhone 6Plus device (9.0.2) , iPhone 6S Plus (9.2.1) & iOS8.4 Device Mac OSX El Capitan 10.11.3 (15D21) Ti SDK: 5.2.0.v20160220080449 Appc Studio: 4.5.0.201602170821 Appc NPM: 4.2.3-2 App CLI: 5.2.0-269 Xcode 7.2 Node v4.2.6 production *Closing ticket.*
  21. Fokke Zandbergen 2016-02-23

    [~htbryant] not sure I get your test report. The event should give a deprecation message now because it does _not_ (always) work as expected.

JSON Source