Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-23955] Hyperloop - not possible to start a native activity and get its result

GitHub Issuen/a
TypeBug
PriorityCritical
StatusClosed
ResolutionDuplicate
Resolution Date2016-10-06T15:16:34.000+0000
Affected Version/shyperloop 1.2.7
Fix Version/sn/a
ComponentsHyperloop
LabelsAndroid, Hyperloop
ReporterBrian Knorr
AssigneeChristopher Williams
Created2016-09-27T19:17:40.000+0000
Updated2017-03-20T22:26:30.000+0000

Description

Starting an activity and getting its result is fundamental to Android development. Hyperloop does not provide a way to do this...at least to my knowledge. I worked with several folks on TiSlack and no one seems to have a working solution. Here was the best attempt using what is available in HyperLoop:
var Activity = require('android.app.Activity');
var Intent = Alloy.require('android.content.Intent');
var CardIOActivity = Alloy.require('io.card.payment.CardIOActivity');

var MyActivity = Activity.extend({
            onActivityResult: function(requestCode, resultCode, data) {
                console.log('!!!!!!onActivityResult');
            }
        });

//Cast the current activity to your overridden native one
var windowActivity = new MyActivity(window.getActivity());

var scanIntent = new Intent(windowActivity, CardIOActivity.class);

windowActivity.startActivityForResult(scanIntent, 100); //Fails
Here is the error that is thrown:
[ERROR] :  HyperloopProxy: (main) [6645,21912] Exception thrown during invocation of method: public void Activity_Proxy.startActivityForResult(android.content.Intent,int), args: [Intent { cmp=/io.card.payment.CardIOActivity (has extras) }, 100]
[ERROR] :  HyperloopProxy: java.lang.NullPointerException: Attempt to invoke virtual method 'android.app.ActivityThread$ApplicationThread android.app.ActivityThread.getApplicationThread()' on a null object reference
[ERROR] :  HyperloopProxy: 	at android.app.Activity.startActivityForResult(Activity.java:4026)
[ERROR] :  HyperloopProxy: 	at Activity_Proxy.super$startActivityForResult$void(Activity_Proxy.generated)
[ERROR] :  HyperloopProxy: 	at java.lang.reflect.Method.invoke(Native Method)
[ERROR] :  HyperloopProxy: 	at java.lang.reflect.Method.invoke(Method.java:372)
[ERROR] :  HyperloopProxy: 	at com.android.dx.stock.ProxyBuilder.callSuper(ProxyBuilder.java:546)
[ERROR] :  HyperloopProxy: 	at hyperloop.DynamicSubclassInvocationHandler.invoke(DynamicSubclassInvocationHandler.java:33)
[ERROR] :  HyperloopProxy: 	at Activity_Proxy.startActivityForResult(Activity_Proxy.generated)
[ERROR] :  HyperloopProxy: 	at android.app.Activity.startActivityForResult(Activity.java:3973)
[ERROR] :  HyperloopProxy: 	at Activity_Proxy.super$startActivityForResult$void(Activity_Proxy.generated)
[ERROR] :  HyperloopProxy: 	at java.lang.reflect.Method.invoke(Native Method)
[ERROR] :  HyperloopProxy: 	at java.lang.reflect.Method.invoke(Method.java:372)
[ERROR] :  HyperloopProxy: 	at com.android.dx.stock.ProxyBuilder.callSuper(ProxyBuilder.java:546)
[ERROR] :  HyperloopProxy: 	at hyperloop.DynamicSubclassInvocationHandler.invoke(DynamicSubclassInvocationHandler.java:33)
[ERROR] :  HyperloopProxy: 	at Activity_Proxy.startActivityForResult(Activity_Proxy.generated)
[ERROR] :  HyperloopProxy: 	at java.lang.reflect.Method.invoke(Native Method)
[ERROR] :  HyperloopProxy: 	at java.lang.reflect.Method.invoke(Method.java:372)
[ERROR] :  HyperloopProxy: 	at hyperloop.BaseProxy.invokeMethod(BaseProxy.java:145)
[ERROR] :  HyperloopProxy: 	at hyperloop.InstanceProxy.invokeMethod(InstanceProxy.java:183)
[ERROR] :  HyperloopProxy: 	at hyperloop.BaseProxy.callNativeFunction(BaseProxy.java:127)
[ERROR] :  HyperloopProxy: 	at org.appcelerator.kroll.runtime.v8.V8Object.nativeFireEvent(Native Method)
[ERROR] :  HyperloopProxy: 	at org.appcelerator.kroll.runtime.v8.V8Object.fireEvent(V8Object.java:62)
[ERROR] :  HyperloopProxy: 	at org.appcelerator.kroll.KrollProxy.doFireEvent(KrollProxy.java:918)
[ERROR] :  HyperloopProxy: 	at org.appcelerator.kroll.KrollProxy.handleMessage(KrollProxy.java:1141)
[ERROR] :  HyperloopProxy: 	at org.appcelerator.titanium.proxy.TiViewProxy.handleMessage(TiViewProxy.java:357)
[ERROR] :  HyperloopProxy: 	at android.os.Handler.dispatchMessage(Handler.java:98)
[ERROR] :  HyperloopProxy: 	at android.os.Looper.loop(Looper.java:145)
[ERROR] :  HyperloopProxy: 	at android.app.ActivityThread.main(ActivityThread.java:6843)
[ERROR] :  HyperloopProxy: 	at java.lang.reflect.Method.invoke(Native Method)
[ERROR] :  HyperloopProxy: 	at java.lang.reflect.Method.invoke(Method.java:372)
[ERROR] :  HyperloopProxy: 	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1404)
[ERROR] :  HyperloopProxy: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1199)

Comments

  1. Brian Knorr 2016-09-27

    Also if you replace the line:
       var windowActivity = new MyActivity(window.getActivity());
       
    with:
       var windowActivity = new Activity(window.getActivity());
       
    the Activity will start just fine, but there is no way to get the result.
  2. Brian Knorr 2016-09-27

    I cannot find a way to update the ticket's description...but I made a copy/paste mistake. The lines:
       var Intent = Alloy.require('android.content.Intent');
       var CardIOActivity = Alloy.require('io.card.payment.CardIOActivity');
       
    should be this instead
       var Intent = require('android.content.Intent');
       var CardIOActivity = require('io.card.payment.CardIOActivity');
       
  3. Christopher Williams 2016-09-28

    Well, first off that "cast" will never work - the active/current activity is going to be whatever it already is, you can't just pass it into the dynamically generated subclass to turn it into that subclass. This isn't a use case I've tried yet, so it's likely they'll need to be some mix of standard Titanium API usage and some custom hyperloop code meshing nicely together - and I probably didn't add the silent conversions for Intents (between Titanium.Android.Intent and android.content.Intent) into the hyperloop code yet. My best guess right now would be an approach like so:
       var Intent = require('android.content.Intent'),
           CardIOActivity = require('io.card.payment.CardIOActivity');
       
       var scanIntent = new Intent(Ti.Android.currentActivity, CardIOActivity.class);
       
       Ti.Android.currentActivity.startActivityForResult(scanIntent, function (result) {
           Ti.API.info(result.requestCode);
           Ti.API.info(result.resultCode);
       });
       
    Here, the startActivityForResult call is actually the one from our Titanium API at http://docs.appcelerator.com/platform/latest/#!/api/Titanium.Android.Activity-method-startActivityForResult Again, I still need to test if this works, but this seems like a workable approach.
  4. Brian Knorr 2016-09-28

  5. Christopher Williams 2016-09-28

    OK, I just tested my code above and yeah it doesn't work. I'm working on special casing conversions between native Activity and the Titanium ActivityProxy, as well as Intent and Titanium's IntentProxy. For now, you can "cast" a Titanium Activity to a native one, but can't with Intents. But additionally, if you try and pass a Titanium Activity or Intent to a method expecting the native Activity or Intent (or vice-versa, passing in native ones where we expect Ti proxies) that currently fails. I'm going to try and address all these cases now and see if we can't get this to work.
  6. Christopher Williams 2016-09-28

    Yuck, so I can fix TIMOB-23953 pretty easily. However, to allow Titanium API methods to accept hyperloop proxies as you would need in my snippet above, I'll likely need to make SDK changes. I'll investigate more, but I'm guessing I'd need to add some code in the SDK and hyperloop to handle that.
  7. Christopher Williams 2016-09-28

    So I _think_ might be able to hack this eventually with changes only to hyperloop, but it'd end up in some clunky syntax that I'm not sure we'd want to support. Basically you'd need to create a new native/hyperloop Intent, then cast it to a Titanium IntentProxy, then call a special method to unwrap the hyperloop wrapper down to the actual native IntentProxy and pass that into the Titanium API call. Something like:
       var Intent = require('android.content.Intent'),
           IntentProxy = require('org.appcelerator.titanium.proxy.IntentProxy'),
           CardIOActivity = require('io.card.payment.CardIOActivity');
       
       var scanIntent = new Intent(Ti.Android.currentActivity, CardIOActivity.class);
       var proxy = new IntentProxy(scanIntent);
       
       Ti.Android.currentActivity.startActivityForResult(proxy.getNativeObject(), function (result) {
           Ti.API.info(result.requestCode);
           Ti.API.info(result.resultCode);
       });
       
    That's less than ideal, and exposes the Titanium proxy classes and a new accessor method to unwrap hyperloop's wrapper (which is really intended to be transparent). I'd prefer to introduce some interface/mechanism into the SDK itself to be able to support hyperloop's wrappers better when they match up properly in terms of the wrapped type. But right now, given the architecture of hyper loop's wrapper classes (specifically InstanceProxy), I don't think I could do so in a type-safe manner at all, we'd basically just have to have some marking interface that we can query to see if the implemented object can convert/adapt to the expect class we want to manipulate.
  8. Brian Knorr 2016-09-28

    How about something like the following....would this work?
       var Activity = require('android.app.Activity'),
           Intent = require('android.content.Intent'),
           IntentProxy = require('org.appcelerator.titanium.proxy.IntentProxy'),
           CardIOActivity = require('io.card.payment.CardIOActivity');
       
       var scanIntent = new Intent(new Activity(Ti.Android.currentActivity), CardIOActivity.class);
       var proxy = new IntentProxy(scanIntent);
       
       Ti.Android.currentActivity.startActivityForResult(proxy, function (result) {
           Ti.API.info(result.requestCode);
           Ti.API.info(result.resultCode);
           
           var resultIntent = new Intent(result.intent);
       });
       
  9. Christopher Williams 2016-09-28

    Unfortunately not, no. I tried this as well. You ultimately get a crash because the Ti.Android.currentActivity.startActivityForResult() call expects an IntenTproxy as the first argument and while we try to pretend that the proxy is an IntentProxy, under the hood it's not really. When you do new IntentProxy it's *actually* creating a hyperloop wrapper whose wrapped Java type is InstanceProxy (basically a generic proxy that holds an instance of some object and uses reflection to access methods and fields on the object it wraps). You can unwrap the hyperloop proxy (proxy variable above in JS world) to the "native"/Java InstanceProxy instance it holds in Java-land, but can't "unwrap" that to the actual IntentProxy it's holding - unless I add some new method/property to do that (the getNativeObject() call in my example above).
  10. Brian Knorr 2016-09-29

    What if there was a general way to cast a native object to a Ti one...just like you do when going from Ti to native? Probably would require some work but would handle all cases.
        var Activity = require('android.app.Activity');
        var Intent = require('android.content.Intent');
        var CardIOActivity = require('io.card.payment.CardIOActivity');
        
        //create native intent
        var nativeIntent = new Intent(new Activity(Ti.Android.currentActivity), CardIOActivity.class);
        
        //a few different ideas for casting to Ti
        var tiIntent1 = new Ti.Android.Intent(nativeIntent); 
        var tiIntent2 = Ti.Android.createIntent(nativeIntent); 
        var tiIntent3 = Ti.Android.Intent.cast(nativeIntent); 
        
  11. Christopher Williams 2016-09-29

    Ok, managed to get this working locally with no changes beyond the fix for TIMOB-23953. Here's what I did: - Grabbed the aar for the library: http://search.maven.org/remotecontent?filepath=io/card/android-sdk/5.4.2/android-sdk-5.4.2.aar - Dropped it into app/platform/android - Modified my tiapp.xml to add the activities to the manifest for android:
          <android
            xmlns:android="http://schemas.android.com/apk/res/android">
            <manifest>
              <application android:theme="@style/appcelerator">
            <activity android:name="io.card.payment.CardIOActivity"
                android:configChanges="keyboardHidden|orientation" />
            <activity android:name="io.card.payment.DataEntryActivity" />
        </application>
            </manifest>
          </android>
        
    - Then used this code:
        	var Intent = require('android.content.Intent'),
        		CardIOActivity = require('io.card.payment.CardIOActivity'),
        		CreditCard = require('io.card.payment.CreditCard');
        
        	$.button.addEventListener('click', function () {
        		var scanIntent = Titanium.Android.createIntent({
        			className: 'io.card.payment.CardIOActivity'
        		});
        
        		// customize these values to suit your needs.
        		scanIntent.putExtra(CardIOActivity.EXTRA_REQUIRE_EXPIRY, true); // default: false
        		scanIntent.putExtra(CardIOActivity.EXTRA_REQUIRE_CVV, false); // default: false
        		scanIntent.putExtra(CardIOActivity.EXTRA_REQUIRE_POSTAL_CODE, false); // default: false
        		scanIntent.putExtra(CardIOActivity.EXTRA_RESTRICT_POSTAL_CODE_TO_NUMERIC_ONLY, false); // default: false
        		scanIntent.putExtra(CardIOActivity.EXTRA_REQUIRE_CARDHOLDER_NAME, false); // default: false
        
        		// hides the manual entry button
        		// if set, developers should provide their own manual entry mechanism in the app
        		scanIntent.putExtra(CardIOActivity.EXTRA_SUPPRESS_MANUAL_ENTRY, false); // default: false
        
        		// matches the theme of your application
        		scanIntent.putExtra(CardIOActivity.EXTRA_KEEP_APPLICATION_THEME, false); // default: false
        
        		Ti.Android.currentActivity.startActivityForResult(scanIntent, function (result) {
        			Ti.API.info(result.requestCode);
        			Ti.API.info(result.resultCode);
        			var nativeIntent = new Intent(result.intent);
        			Ti.API.info(nativeIntent);
        			var card = nativeIntent.getParcelableExtra(CardIOActivity.EXTRA_SCAN_RESULT);
        			Ti.API.info(card);
        			var scanResult = CreditCard.cast(card);
        			Ti.API.info("Card Number: " + scanResult.getRedactedCardNumber() + "\n");
        
        			// Do something with the raw number, e.g.:
        			// myService.setCardNumber( scanResult.cardNumber );
        
        			if (scanResult.isExpiryValid()) {
        				Ti.API.info("Expiration Date: " + scanResult.expiryMonth + "/" + scanResult.expiryYear + "\n");
        			}
        
        			if (scanResult.cvv != null) {
        				// Never log or display a CVV
        				Ti.API.info("CVV has " + scanResult.cvv.length() + " digits.\n");
        			}
        
        			if (scanResult.postalCode != null) {
        				Ti.API.info("Postal Code: " + scanResult.postalCode + "\n");
        			}
        
        			if (scanResult.cardholderName != null) {
        				Ti.API.info("Cardholder Name : " + scanResult.cardholderName + "\n");
        			}
        		});
        	});
        
    This works for me, but there's on weird behavior here. Specifically after the user enters their CC info and clicks Done, the UI disappears, but it doesn't seem like the activity "finishes". I have to hit the back button on the Android emulator before the Ti.Android.currentActivity.startActivityForResult callback fires.
  12. Brian Knorr 2016-09-30

    Ya with the ability to cast a Ti Intent to a native one, this is a great workaround. I would like to try it out, but I don't have access to hyperloop 1.2.8, only 1.2.7.
  13. Christopher Williams 2016-10-06

    Ok, So I'm going to mark this as "duplicate" for now since the other fix seems to enable this to work. [~btknorr] Please feel free to re-open if the workaround doesn't work for you once you've tried on Hyperloop 1.2.8+. I don't have insight as to when exactly that gets released or if support is able to release a build in advance to you.
  14. Lee Morris 2017-03-20

    Closing ticket as duplicate.

JSON Source