How to find which AIR version was used to build an APK?

After I updated my game using the newest AIR version, I’ve got the ANR increase. I want to send a report to Harman, but I don’t remember, which AIR version was used to publish the previous version. Is it possible to retrieve this information from the apk?

Yes.
Unzip the APK (I use 7zip). Then navigate to \assets\META-INF\AIR\ and open application.xml.
The version is at the top in the <application> tag.

Greg.

Referring to https://help.adobe.com/en_US/air/build/WSfffb011ac560372f2fea1812938a6e463-8000.html

xmlns — the XML namespace attribute determines the required AIR runtime version of the application.

The namespace changes with each major release of AIR (but not with minor patches). The last segment of the namespace, such as “3.0,” indicates the runtime version required by the application.

Please mind, you will not be able to see the exact AIR version by looking in the AIR application descriptor.

If you unzip the APK and look in the ‘lib’ folder, you’ll see a subfolder that tells us which ABI you’re using (ARM, ARM-64 etc). If you then open “libCore.so” in a hex editor, and search for a text string “33,1”, you should see the embedded version code.

2 Likes

Thank you! So, the version, packaged with 33.1.1.190 is showing 1.11% ANR


The earlier version was showing only 0.13%ANR

To find, which AIR version was used to build this, is tricky… libCore.so has only the line “33.0” (for the newer version libCore.so contains the line “33.1.1.190”) I guess, it might be 33.0.2.330

Besides AIR version change, I also added IAP. Can this be the reason? For example, when check, which IAP are already available right when the application starts?

ANR graph look like this:

This is the list of the most common ANRs during the last 60 days:

Wait, indeed, this should be connected with the IAPs, because here’s how the log of the most widespread ANR looks like:

Input dispatching timed out (air.com.airapport.steampunkidlespinner/air.com.airapport.steampunkidlespinner.AppEntry, Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago. Wait queue length: 2. Wait queue head age: 8942.9ms.)

"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x794de1e8 self=0x7cda814c00
  | sysTid=16477 nice=-10 cgrp=default sched=0/0 handle=0x7d606fa548
  | state=S schedstat=( 61345949316 4831147288 23439 ) utm=5673 stm=461 core=5 HZ=100
  | stack=0x7fc921c000-0x7fc921e000 stackSize=8MB
  | held mutexes=
  #00  pc 000000000007afc4  /system/lib64/libc.so (__ioctl+4)
  #01  pc 000000000002ae00  /system/lib64/libc.so (ioctl+132)
  #02  pc 000000000005ccb0  /system/lib64/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+244)
  #03  pc 000000000005da5c  /system/lib64/libbinder.so (android::IPCThreadState::waitForResponse(android::Parcel*, int*)+60)
  #04  pc 000000000005d8b0  /system/lib64/libbinder.so (android::IPCThreadState::transact(int, unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+176)
  #05  pc 00000000000518c8  /system/lib64/libbinder.so (android::BpBinder::transact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+72)
  #06  pc 00000000001380e8  /system/lib64/libandroid_runtime.so (android_os_BinderProxy_transact(_JNIEnv*, _jobject*, int, _jobject*, _jobject*, int)+332)
  at android.os.BinderProxy.transactNative (Native method)
  at android.os.BinderProxy.transact (BinderProxy.java:473)
  at com.android.vending.billing.IInAppBillingService$Stub$Proxy.getSkuDetails (IInAppBillingService.java:241)
  at com.fgl.thirdparty.inapppayment.GoogleInAppPayment.loadIAPSkusDetails (GoogleInAppPayment.java:198)
  at com.fgl.thirdparty.inapppayment.GoogleInAppPayment.updateAndCacheAllProducts (GoogleInAppPayment.java:137)
  at com.fgl.thirdparty.inapppayment.GoogleInAppPayment.onIAPServiceConnected (GoogleInAppPayment.java:127)
  at com.fgl.thirdparty.inapppayment.GoogleInAppPayment.access$200 (GoogleInAppPayment.java:38)
  at com.fgl.thirdparty.inapppayment.GoogleInAppPayment$1.onServiceConnected (GoogleInAppPayment.java:64)
  at android.app.LoadedApk$ServiceDispatcher.doConnected (LoadedApk.java:1738)
  at android.app.LoadedApk$ServiceDispatcher$RunConnection.run (LoadedApk.java:1770)
  at android.os.Handler.handleCallback (Handler.java:873)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loop (Looper.java:201)
  at android.app.ActivityThread.main (ActivityThread.java:6864)
  at java.lang.reflect.Method.invoke (Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:547)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:873)

So, what I understand from it, right when the game starts (ZygoteInit.java), the IAP payment ANE starts retrieving all the details of SKUs, and right there the application stops responding. Am I right?
What can be done to solve this? Maybe suggest the ANE provider to call the updateAndCacheAllProducts on times after 1-2 seconds delay, not at the start of the application?

let me guess, your class AppEntry is a huge class doing a lot of different stuff?

if that’s the case then simply divide stuff in smaller package
and then execute then in sequential order over a longer period of time

so for example, I doubt you absolutely need the in-app billing initialised right the first second in your app, it probably can wait the 5th or later second

It’s hard to say more because I don’t see how your code is organised and structured but basically it seems the app is trying to do too much too fast

and I know what an answer could be: “but it was working before”
to which I would say, but before you were doing a little less, and then now you’re doing a little more and that make the app pass over a threshold

let’s try to give an example
when you initialize/start an AIR app you want tto do different actions or steps
let’s say: register for global error, configure the stage dimension, register fonts, draw the UI, etc.

you could imagine all that as a boot sequence in an operating system
each step need to execute in a specific order one after another

for that you could think of a BootSequence class which is just here to store BootTask
and all those boot tasks could be their own independent task

configuring the stage for example could be in your main class
the classic

this.stage.align        = StageAlign.TOP_LEFT;
this.stage.scaleMode    = StageScaleMode.NO_SCALE;
this.stage.quality      = StageQuality.BEST;

in order for this to go well, the stage need to be available
so let’s sy you would need to execute this kind of task after an Event.ADDED_TO_STAGE

and to simplify everything, let’s just initialize the strict minimum
and only execute the tasks after this particular event ADDED_TO_STAGE

package some.stuff.here
{
    
    public class MyApp extends Sprite
    {
        protected var boot:BootSequence;
        
        public function MyApp()
        {
            super();
            _ctor();
        }
        
        private function _ctor():void
        {
            /* we first instantiates any non-UI objects */
            boot = new BootSequence();
            
            /* we then wait for the UI */
            if( stage )
            {
                onAddedToStage(); // --> _main()
            }
            else
            {
                addEventListener( Event.ADDED_TO_STAGE, onAddedToStage );
            }
        }
        
        private function _main():void
        {
            // do main stuff here
        }
        
        private function onAddedToStage( event:Event = null ):void
        {
            removeEventListener( Event.ADDED_TO_STAGE, onAddedToStage );
            _main();
        }

        //...

}

few obvious things first, but still let mention them

  • do not put too much code in the class constructor
    in fact try to put as less as possible code there
    if you can delegate everything in another method, for ex _ctor()
  • absolutely watch your consumption of events
    if you addEventListener() you HAVE TO removeEventListener()
    eg. learn to clean up everything you don’t need anymore

Now, for some boot sequence, you would want to make it work like that

package some.stuff.here
{
    
    public class MyApp extends Sprite
    {
        protected var boot:BootSequence;
        
        private function _main():void
        {
            //connect to boot callbacks
            boot.onStart    = onBootStart;
            boot.onProgress = onBootProgress;
            boot.onFinish   = onBootEnd;
            
            if( !boot.running )
            {
                //we always call the before() function first
                before();
                
                //start the boot sequence
                boot.run();
                
                // we delete the boot sequence
                boot = null;
                
                //we always call the after() function last
                after();
            }
            else
            {
                throw new Error( "Boot is already running!" );
            }
        }

        //...

}

execute some before() method, then run your boot sequence with boot.run() and finally execute some after() method

this part

boot.onStart    = onBootStart;
boot.onProgress = onBootProgress;
boot.onFinish   = onBootEnd;

is to hook some function, to call each time a boot task start, progress and finish

you could do it like those

package some.stuff.here
{
    
    public class MyApp extends Sprite
    {

        //---- our Boot API ----
        
        protected function before():void
        {
            //override in Application implementation
            trace( ".before()" );
        }
        
        protected function onBootStart( task:BootTask ):void
        {
            //override in Application implementation
            trace( ".onBootStart()" );
        }
        
        protected function onBootProgress( task:BootTask ):void
        {
            //override in Application implementation
            trace( ".onBootProgress()" );
        }
        
        protected function onBootEnd( task:BootTask ):void
        {
            //override in Application implementation
            trace( ".onBootEnd()" );
        }
        
        protected function after():void
        {
            //override in Application implementation
            trace( ".after()" );
        }

        //...

}

and you would initialize those tasks in the before() method

        protected override function before():void
        {
            trace( "MyApp.before()" );
            /* Note:
            before() always execute
            before the boot sequence
            */
            
            /* Note:
            Here an example of default boot sequence
            Define your boot sequence in your own Application class
            */
            boot.add( new RegisterGlobalErrorsTask( this ) );
            boot.add( new ConfigureStageTask( stage, this ) );
            boot.add( new RegisterFontsTask() );
            boot.add( new DrawGUITask( this.draw ) );
            
        }

you simply add tasks to your boot sequence, and the little thing to notic is you can pass around variables in the constructor of your task, here the this is the instance of the application itself for ex

let’s see in details the ConfigureStageTask class

package some.stuff.here.tasks
{
    import flash.display.Stage;
    import flash.display.StageAlign;
    import flash.display.StageQuality;
    import flash.display.StageScaleMode;

    //...
    
    public class ConfigureStageTask extends BootTask
    {
        private var _stage:Stage;
        private var _app:*;
        
        public function ConfigureStageTask( stage:Stage, app:* )
        {
            super( "ConfigureStageTask" );
            
            _stage = stage;
            _app   = app;
        }
        
        public override function run():void
        {
            if( !_stage )
            {
                throw new Error( "Stage is not ready" );
            }
            
            //we configure the stage
            _stage.align        = StageAlign.TOP_LEFT;
            _stage.scaleMode    = StageScaleMode.NO_SCALE;
            _stage.quality      = StageQuality.BEST;
        }
    }
}

and that’s the trick, instead of having a method on your main class, for ex: private function -configureStage():void

  • you delegate the whole logic in its own “task” class
  • you use the constructor of this task class to pass what is needed
  • then your “run()” method is here to execute this specific task

So, there are off course thousands of different way to architecture a main class in an AIR app
but even without looking at your code (even if unfair) I can make those few comments

  • usually dev pack a hell lot of stuff in the main class
    there is always one moment where they go overboard, where it is simply too much
  • by isolating block of logic and inititialization in their own separate class
    the first effect it will have is clear up your main class, and you will be able to see the flow
    of execution and inititialization more clearly
    the second effect it will have is to force you to think in which order things need to be inititialized
    for ex: you probably need the stage before iinititializing the in-app billing
  • by having those inititialization in sequences you will be able to on/off some of them
    “hu ho when I don’t inititialize at all this thing, no more crash”
    also by isolating those sequences in their own class you will track down more easily where the problem happen (and in which order)
  • you will be also be able to change the order of sequences
    there are some case where iinititializing A before B works better (than the opposite)
    there are some case where you want to iinititialize C only after a certain amount of time
    etc.

Anyway those are suggestions that might help if the assumption of the start is indeed doing too much into the main class.

2 Likes

Yes, in my Main constructor I create a timer, which after a delay calls the creation of Starling object.
Starling the launches assets loading, and when the assets are loaded, the creation of game objects starts.

The problem with the IAP is they I didn not use any ANE for it, they injected into the byte-code of the compiled APK with Enhace. So, I’ve written to the support to find the possibility to have the purchases initiated somewhen later during the game flow.

well… I think you just pointed at a huge flaw with this enhance stuff
eg. you can not control how and where the things is instanced in your code

you might get away with that by using multiple frames
see Adobe Air android with 2 swfs questions

if and only if enhance can inject their code in say the 3rd frame of your AIR SWF
then you can initialize all your code before that on frame 1
then let the playhead run, and wait till it hit the 3rd frame
and from your AppEntry code (in 1st frame) then launch the app

I find that not very stable I would rather manage the ANE myself
but in your specific case it might work

that, or have the option to let your app control when the ANE code is run
eg. enhance could add their bytecode, but just give the info on how to run it by code dynamically (with them off course not running it automatically)

Yes, like in good old flash times, where all the MovieClips were in the 3rd frame of the swf, and they were initiated during preloading.