Sunday, September 28, 2014

Gradle - The next generation ANT (Build management tool adopted by Google)

All those who've worked on automating the builds must have worked on ANT. ANT was a simple tool but extremely effective in making the builds automated. It had a simple XML based structure, allowed you to break building pieces into tasks and easily use external JARS. You could also create your own tasks.

However, I think Ant has not evolved as we would have wanted. The XML syntax, despite being easy to use is tedious. Time to analyze the other players on the block.

A few popular ones out today are:
  • Maven
  • Gulp
  • Grunt
  • NodeJS
  • Gradle
Among the ones listed above, Maven has been around for long and is quite close to Ant and has similar limitations. Among others, I found Gradle to be the most impressive. The best part about Gradle is that it doesn't reinvent the wheel, it makes use of the existing systems and provides a simple language, similar to Groovy to write your build scripts. Gradle readily adds support for or has plugins available for the following:
  • Ant Tasks
  • RequireJS
  • Google Closure Compiler
  • GZipJs
  • ...and many more

The biggest endorsement for Gradle comes from Google itself who've based their new Android IDE, Android studio completely on Gradle, need I say more? The flexibility that Gradle provides is amazing, you can use Gradle to:
  • Build JS Projects
  • Build Android projects
  • .. And also build XCode Projects
This is amazing as I can rely on one tool for all my build management. In this post I will share the approach to automate the builds of a JS project and share some basic info about Android Studio and XCode support.

To understand the examples better, you can read basics about gradle here:
Gradle Homepage
Gradle JS Plugin

Download Gradle at: Gradle 2.1

Let's get started! First create an empty text file and name it "build.gradle". Second thing we will do is define the plugins we are going to use for this
buildscript {
  repositories {
      jcenter()
  }
  dependencies {
      classpath "com.eriwen:gradle-js-plugin:1.12.1"
  }
}
That's all we need, Gradle will itself download and install all plugins defined when build script is run. As we move ahead, some of the things will seem strikingly similar to Ant. Gradle leverages on ANT and the terminology it uses is also quite close. The methods are called tasks and variables are accessed using $(var). Just like Ant, Gradle allows defining configurations in a properties file. The name of the default properties file is gradle.properties. Any property defined in the properties file can be used using the varName syntax. Let's see an example:
Create a new text file "gradle.properties" and add a variable as follows:
message=Hello World

And this is how we use it in the script:
println "Just want to say ${message}";

Simple, ain't it! That's not all. Gradle also provides a powerful mechanism of creating custom properties files and using both the default one and the custom one. This is particularly useful when you want to customize build according to themes but don't want to specify the common properties repeatedly. We will see it in action in the tutorial that follows:

In this tutorial, I will create a Task, which will first combine a list of JS files, combine them and then Obfuscate the same using Google Closure compiler.

First thing we will do is load a properties file
Properties props = new Properties() props.load(new FileInputStream("${theme}.properties"));

Here, we loaded a custom properties file based on the theme parameter. Now, I will create the tasks to Combine the JS files. We will create 2 tasks, just to demonstrate use of custom properties.

task combineCommonJS(type: com.eriwen.gradle.js.tasks.CombineJsTask)  {
    List list = new ArrayList();
    if(flavor == "mobile")
    {
        mobile_common_scripts.split(",").each{String f -> list.add(f)}
    }
    else
    {
        common_scripts.split(",").each{String f -> list.add(f)}
    }
    source = list;
    dest = file("${buildDir}/common.js")
}

task combineCustomJS(type: com.eriwen.gradle.js.tasks.CombineJsTask) {
    List list = new ArrayList();
    if(flavor == "mobile")
    {
        props.mobile_custom_scripts.split(",").each{String f -> list.add(f)}
    }
    else
    {
         props.custom_scripts.split(",").each{String f -> list.add(f)}
    }
    source = list;
    dest = file("${buildDir}/custom.js")
}

So, here, we created 2 tasks, both of type CombineJsTask. The idea here is to fetch a list of JS files from properties and combine them. In the first task, the list of JS files was picked from the default properties and in the second one, it was picked from the custom properties. In the second one, we also used a condition to check whether we are making a build for mobile flavor or not. Here flavor is just another variable which can be anything but when working with Android studio, flavor is special but will talk about that later. Variables can also be provided as input parameters, to a gradle script, but that for later.

By now, we have written code to combine all JS files into 2 files common.js and custom.js. Now, we'd create tasks to minify these using Google Closure Compiler.
task minifyCommonJS(type: com.eriwen.gradle.js.tasks.MinifyJsTask) {
    source = file("${buildDir}/common.js")
    dest = file("${buildDir}/common.min.js")
    closure {
        warningLevel = 'QUIET'
    }
}

task minifyCustomJS(type: com.eriwen.gradle.js.tasks.MinifyJsTask) {
    source = file("${buildDir}/custom.js")
    dest = file("${buildDir}/custom.min.js")
    closure {
        warningLevel = 'QUIET'
    }
}

So, now we have all the tasks ready but we need to define the dependencies so that they execute in the correct order. For that we'd add a few more lines to the gradle script:

minifyCommonJS.dependsOn combineCommonJS minifyCustomJS.dependsOn combineCustomJS combineCommonJS.dependsOn minifyReaderJS

We have defined dependencies in a way that when we call minifyCommonJS task, it will execute all other tasks due to the defined dependencies. That's it, we have the script ready. Before we execute, let us see how to define JS files in the properties file.

gradle.properties:
common_scripts=lib/a.js,lib/b.js,lib/c.js
mobile_common_scripts=lib/a.js,lib/b.js,lib/mobile.js

defaulttheme.properties:
custom_scripts=defaulttheme/1.js,defaulttheme/2.js

customtheme.properties:
custom_scripts=customtheme/1.js,custom=theme/2.js

Here, we have created 3 properties files, one default one and 2 for the themes. Take special care that there is no space between the file names and no trailing spaces. Now let us see how to execute this gradle script.

Assuming you've added gradle to the Path. Open command line, locate to the folder containing the build.gradle file and type:
gradle minifyCommonJS -Ptheme=defaulttheme -Pflavor=mobile

Here we have instructed gradle to run the task minifyCommonJS and provide 2 properties as input, theme and flavor. The properties need to be defined using the -P syntax. Note there is no space after -P.

When this task runs, the defaulttheme.properties file will be loaded and from the scripts, mobile_common_scripts will be compiled in the combineCommonJS task. We can change the input properties to change the behavior as follows:
gradle minifyCommonJS -Ptheme=customtheme -Pflavor=web

Once the task executes, we will have the minimized and obfuscated output for the JS files. That's it! We have successfully created and executed a gradle build.

This concludes the tutorial but we have just about touched Gradle. The possibilities are endless. Gradle for Android brings an interesting concept of flavor. With the help of flavor, you can maintain different assets per flavor and at the time of compilation, it will only copy the chosen flavor. Makes life easier. To explore the full powers of Gradle on Android, use Android Studio.

Gradle for XCODE is something I am yet to touch but you can read about it here: Gradle XCode Plugin

I hope this post helps you in getting familiar with Gradle. For any queries, feel free to email me

Sunday, September 21, 2014

SQLite Full Text Search on Mobile devices (FTS3)

When we talk about Full Text Search, the libraries that come to mind are Solr, ElasticSearch etc. However, all of these are back end libraries and require the indexes of the files to be created first.

When we talk about searching inside the mobile apps (without hitting the server), either these libraries do not have the client side implementations or are too heavy to be included in an app.

This is where Full Text Search engine of SQLite (FTS3) comes to rescue. FTS3 provides a lot of methods to perform full text search on the database using SQL statements. Although, both iOS and Android support SQLite out of the box, however, even using the same with libraries like SQLCipher is quite easy and doesn't require embedding huge libraries into the app either. I am not going to cover the differences between FTS3 and FTS4 as all that information is available on the link I share below.

All the methods provided by FTS3 with examples are listed here http://www.sqlite.org/fts3.html
. I recommend that you do keep referring to the link if anything is not clear as I will not cover the definition in detail but only the basics of FTS3 and share a code snippet for Android.

The FTS3 and FTS4 extension modules allows users to create special tables with a built-in full-text index, "FTS tables". The full-text index allows the user to efficiently query the database for all rows that contain one or more words also known as "tokens", even if the table contains many large documents.

To create a virtual table, the following statement can be used.
    CREATE VIRTUAL TABLE enrondata1 USING fts3(content TEXT);
This makes the table eligible for Full text query

When the WHERE clause of the SELECT statement contains a sub-clause of the form " MATCH ?", FTS is able to use the built-in full-text index to restrict the search to those documents that match the full-text query string specified as the right-hand operand of the MATCH clause.

The fast full text query looks as follows:     SELECT * FROM mail WHERE subject match 'database';

FTS3 and FTS4 provides three special auxiliary functions that are very useful to the developers:"snippet", "offsets" and "matchinfo". As the SQLite portal states: "The purpose of the "snippet" and "offsets" functions is to allow the user to identify the location of queried terms in the returned documents. The "matchinfo" function provides the user with metrics that may be useful for filtering or sorting query results according to relevance."

In this post, I am going to talk about the Offsets method only as that is the one I found to be most effective if you have to perform a full text search and fetch an excerpt. Although, the snippets function seems to be the one to fetch excerpts, it actually doesn't work as per its name.
The offsets() function returns a text value containing a series of space-separated integers. For each term in each phrase match of the current row, there are four integers in the returned list. Each set of four integers is interpreted as follows:
Integer Interpretation
0 - The column number that the term instance occurs in (0 for the leftmost column of the FTS table, 1 for the next leftmost, etc.).
1 - The term number of the matching term within the full-text query expression. Terms within a query expression are numbered starting from 0 in the order that they occur.
2 - The *byte offset* of the matching term within the column.
3 - The *size* of the matching term in bytes.
Important thing to note here is that the offset is a byte offset and not a character offset.
More details on the same can be read here: http://www.sqlite.org/fts3.html#section_4_1

Let us see an example of Offsets function being used in Android.

First we will create a new virtual table:
database.execSQL("CREATE VIRTUAL TABLE mail USING fts3(subject, body);");
ContentValues contentValues = new ContentValues();
contentValues.put("subject", "Subject");
contentValues.put("body", textContent);
database.insert("mail", null, contentValues);

Now that the table is ready and has one record, we will write the FTS query as follows:
Cursor myCursor = database.rawQuery("SELECT offsets(mail), body FROM mail WHERE mail MATCH 'Republic';", null);

Here along with the offsets, I am also fetching the full text so that I can extract excerpts from the same.
myCursor.moveToFirst(); // Move cursor to first location
String[] offsets = myCursor.getString(0).split(" "); // Split to " " to read integers
String text = myCursor.getString(1); //Store complete body in a variable
byte[] textBytes = text.getBytes(); // Convert text to bytes
ByteArrayInputStream ba = new ByteArrayInputStream(textBytes);
int i = 0;
ArrayList results = new ArrayList();
int textLength = textBytes.length;
ExcerptFinder excerptFinder = new ExcerptFinder(ba); // Provide the stream containing text bytes to ExcerptFinder
while (i < offsets.length)
{
  //Term and column index are ignored because we've searched for a single term only.
  int startOffset = Integer.parseInt(offsets[i + 2]);// Find the start index of searched term
  int endOffset = startOffset + Integer.parseInt(offsets[i + 3]);// Find the end index of searched term

if(startOffset < 0)
{
    startOffset = 0;
}

if(endOffset >= textLength - 1)
{
     endOffset = textLength - 1;
}
  String excerpt = excerptFinder.readFullWords(startOffset, endOffset);
  results.add(excerpt);
  i += 4;
}

So, here we extract excerpts for the searched term by first identifying its start index and end index in bytes. The same process continues for all the results for the row. In this example, we are working with a single row of data, otherwise there would be one more loop for the rows. Since we need to run forwards and backwards in the ByteArray Stream, the ExcerptFinder class I created extends the RandomAccessStream class provided at: RandomAccessStream.java

The relevant code in the Excerpt Finder class goes as follows:
public final String readFullWords(int start, int end) throws IOException
{
  int startOffset = start;
   int endOffset = end;
  byte[] singleByte = new byte[1];
  int currentCharacter = 0;
  int i = 0;
  int spaceCount = 0;
  while(currentCharacter != RETURN_DELIMITER && spaceCount < NUMBER_OF_WORDS)
  {
    startOffset = start - i;
    seek(startOffset);
    if(startOffset <= 0)
    {
      startOffset = -1;
      break;
    }
    read(singleByte, 0, 1);
    currentCharacter = singleByte[0];
    if(currentCharacter == 32)
    {
      spaceCount++;
    }
    i++;
  }
  currentCharacter = 0;
  i = 0;
  spaceCount = 0;
  int readBytes = 0;
  while(currentCharacter != RETURN_DELIMITER && spaceCount < NUMBER_OF_WORDS)
  {
    endOffset = end + i;
    seek(endOffset);
    readBytes = read(singleByte, 0, 1);
    if(readBytes < 0)
    { //
      endOffset = endOffset - 1;
      break;
    }
  currentCharacter = singleByte[0];
  if(currentCharacter == 32)
  {
    spaceCount++;
  }
  i++;
    }
  seek(startOffset + 1);
  byte[] result = new byte[endOffset - startOffset - 1];
  readFully(result, result.length);
  return new String(result);
}

In the code above, the rule for an excerpt is defined by a RETURN delimiter or NUMBER_OF_WORDS before and after the term. The logic can be tweaked to anything that you want. The key here is to play with the bytes offsets returned by the FTS3 query. Although, we are performing operations at a byte level, the code executes much faster then a RegEx performed on plain text. The search can be made faster by simply fetching a predefined number of bytes before and after the search term without worrying about the number of words.

FTS can be very handy when it comes to providing offline search to mobile apps. You can create the DB at the backend and simply download it on the app to run the FTS query on it. This concludes the post and I look forward to the comments.

If you need the source for the Android app to help you get started, feel free to email me.

iFrame & iOS 7 Safari (Issues, workarounds & limitations)

As a developer, I must admit that iFrame does make developer's life easy but they are not browsers' best friends. We are using iFrame in one of the ebook reader products we are creating and really struggled to get it working on iOS 7. There are multiple ways to work with iFrame on iOS 7:
  1. Setting the iScroll's scrollable property to "yes" doesn't make the iFrame scrollable but actually increases the height of the iFrame to match it's content. So, if you specified the height of iFrame to be "600px" but the content is 12000 px, the iFrame's height actually becomes 12000px which means the parent document's body scrolls and not the iFrame itself, which may defeat the purpose of using an iFrame.

  2. Another approach that I discovered by reading blogs was to wrap the iFrame inside a container div with a fixed height and set the iFrame's height to match it's content and set it up as follows:

    <div style="overflow:scroll; position:absolute; -webkit-overflow-scrolling: touch; width:100%; height:100%;">
        <iframe scrolling="no" style="width:1000px; height:6000px;" src="my_page_url.html"></iframe>
    </div>
    As you can notice, the container div has a fixed height where as the iFrame is larger to match its content. The most important thing here is the style applied on the parent div i.e. "style="overflow:scroll; -webkit-overflow-scrolling: touch;". This is what informs the webkit browser that the div is supposed to scroll and should scroll on touch. Unless you specify this style on the container div, the div will NOT scroll on iOS. You can follow the same approach on all WebKit compatible browsers. This approach will work and make the container div only scroll, however, this approach has it's caveats and we will talk about it later in the article.

  3. The third and final approach I discovered is only feasible when you've control over the document that is loading inside the iFrame. This involves modifying iFrame document's DOM which is possible only when loading documents from the same domain as the parent. In this approach we'd turn the iFrame's scrolling to "no" and also keep it to a fixed size so that the containing div or the parent body doesn't scroll. To achieve the scroll we'd need to inject a container div into the iFrame body and make it overflow. The div can be inject using a code snippet like this:

    iframeDocumentBody.innerHTML = "<div style="overflow:scroll; position:absolute; -webkit-overflow-scrolling: touch; width:100%; height:100%;"> + iframeDocumentBody.innerHTML + </div>

    This will ensure that you actually have a scrolling iFrame.

Now that we have seen 3 different approaches to get the iFrame to work, let us understand the caveats that each one of the above possesses:

  • Approach #1 has the biggest caveat that the iFrame itself doesn't scroll and the container document scrolls.
  • Both approach #1 and #2 present memory issues on iOS Safari. As the iFrame goes longer, the iOS Safari consumes more virtual memory to load the same. So, if you have a 20000 px long iFrame and it contains an HTML video or audio element, the page will CRASH on load. Not certain what happens here but it seems that iOS tries to create a huge place holder for the iFrame and crashes while allocating memory. The memory management works better in case of approach #3 and the page doesn't crash with the same content.
  • Although, following approach #3 solves the problem with crashing on load, however, with all 3 approaches, the iFrame with huge overflow and a media element (audio / video) will crash when you scroll at a very high speed. So audio or video inside the iFrame are not liked by Safari and should be avoided. Best if they can be added to the main document instead.
  • Text Selection: The selection of text works fine with all the three approaches, but with approach #2 and #3 when you scroll the page, the selection doesn't move along with the scroll. This is a bug with iOS safari, which means that it is unable to move selection with any overflowing element doesn't move with the scroll. Sadly, I haven't found any workaround for the same yet. If anyone knows about any work around, please let me know too :D

  • So, as I covered above, there are several issues with the iFrame on iOS 7. I have not got my hands dirty on iOS 8 so can't say what will change. Till that time, I would recommend NOT using iFrame till it is absolutely important. If you still do, I hope the tricks above will help you. If you have anything to add or want to suggest a better approach, please comment and I will modify the post.

Friday, February 7, 2014

Android: Overriding the default contextual action bar for text selection in WebView (Android 4.1+)

We had to create an Android app where user annotates using the text selected in an HTML page. We used the standard Webview component of Android to render the web page.

By default Android's Webview displays the standard ActionBar with Copy, Paste and other buttons when the text is selected. I wanted to either disable the Contextual Action Bar (CAB) or display a CAB with my own set of buttons. Android Webview doesn't provide ability to do either of these.

People had commented on various StackOverflow posts to either use LongPress listener or use registerContextMenu. If we setup the LongClick listener, the selection stops working and registerContextMenu doesn't work in the desired manner either.

After delving deep into the Webview code, I found out that WebViewClassic class was invoking startActionMode method of the Webview. This is all we needed. I created a new class by subclassing the Webview class and overrided the startActionMode method. After that, I just used the extended Webview instead of the default one and that was it.

The overridden method looks like this:

        public ActionMode startActionMode(ActionMode.Callback callback)
          {
                 actionModeCallback = new SelectActionModeCallback();
                return super.startActionMode(actionModeCallback);
          }



As you can note in the code above, instead of sending the ActionMode.Callback sent by Webview classic, I simply created a custom ActionMode.Callback with desired items and sent it to the subclass. This ensured that the selection worked perfectly fine and my custom CAB showed up in place of Android's default menu, each time text was selected. Works like a charm.

However, there is a small caveat in this solution. The CAB doesn't get closed by itself as the Android's default CAB and I had to write code to close it at appropriate places. Hope this helps someone.

Friday, January 25, 2013

Android: Invoking a URL Scheme Intent from a WebView

A few days back, I was working on a project where one of our PhoneGap Android apps needed to invoke another application using the custom URI scheme.

We were able to invoke the other app, when clicking on a link from the mobile browser, but the same did not work from our app.

We figured out that the issue was with the Webview as it was not able to recognize the custom URI.

Solution: After a lot of research, I was able to get to a fix.

The Android Webview component allows using a WebView client. The Webview client exposes a method and I just overrided it as follows: public boolean shouldOverrideUrlLoading(WebView view, String url)
{
                if(url.toLowerCase().startsWith("http") || url.toLowerCase().startsWith("https") || url.toLowerCase().startsWith("file"))
                {
                                view.loadUrl(url);
                }
                else
                {
                                try
                                {
                                                Uri uri = Uri.parse(url);
                                                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                                                                startActivity(intent);
                                }
                                catch (Exception e)
                                {
                                                Log.d("JSLogs", "Webview Error:" + e.getMessage());;
                                }
                }
    return (true);
}

That's it, once you configure the code as above, any URL scheme inside the HTML page would work like a charm. What I simply did was to tell the Webview that if the URL is HTTP, HTTPS or FILE, you handle it, otherwise fire an intent and let the OS handle the same. Simple, yet no one blog has this solution clearly articulated, hence this post.

AIR Android Native Extensions: Part 7: Launching AIR's main activity from other Android activities and passing information

In my previous post, I covered, Preventing Android activity from being recreated on orientation change and relaunch.

In this post, the last of this series I would explain how to invoke your AIR application from other activities and passing data to the same. This is not about using custom URI, but about using Intent to invoke the AIR activity.

The first question that you may have is "What is AIR activity?". When you compile your Flex project into an APK, AIR automatically generates a new activity named AppEntry. This activity class is the single Android activity for all your Flex App. What I am trying to say here is that unlike Android native apps where the application is divided into multiple activities, AIR apps have just one activity i.e. AppEntry.

The package for this AppEntry class is your application's id, that you define in yourproject-app.xml. However, please note that whenever you compile your AIR app for Android, AIR prepends an "air." to the application id. So, if you have defined your app's id as "com.techie.pulkit", APK would get the id as "air.com.techie.pulkit". So, this is what the package for AppEntry class is, so the fully qualified classname for the AppEntry class, in this case, becomes "air.com.techie.pulkit.AppEntry".

Now that we know that our Air application also has an activity, how do I invoke it from my extension and pass data to it? Obviously, you could use FREContext.dispatchStatusEventAsync to dispatch events, but here we would use the Android Intent class to invoke the AIR activity and send data. Let's see how.

Activity myAIRActivity = myFREContext.getActivity();
notificationIntent = new Intent(myAIRActivity, myAIRActivity.getClass());
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
notificationIntent.setData(Uri.parse("any_data_you_want_to_send"));
startActivity(notificationIntent);

The code above would allow you to invoke your AIR activity and pass the data to it. Let's go over each statement to understand better.
Activity myAIRActivity = myFREContext.getActivity();
Here, we get the reference to the AppEntry activity with the help of our extension's FREContext.
notificationIntent = new Intent(myAIRActivity, myAIRActivity.getClass());
Here, we create a new Intent using the activity's context and the activity's class.
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
This one is important, read it carefully. Android Intents by default create a new instance of the activity to be started, however, we don't want that. So, we add a flag, which tells the intent not to create a new instance of the activity, but look for an existing instance and simply reorder it to front. Now, to the next step.
notificationIntent.setData(Uri.parse("any_data_you_want_to_send"))
This is where we send the data to the AIR activity. Simply parse the data as a URI and send it across. Later in the post, we'd see how to receive this data in AIR.
startActivity(notificationIntent);
Finally, simply start the activity.

Now that we are done with the Android part, let's see how to receive this data in your Flex project.

First, add a listener to the event:
NativeApplication.nativeApplication.addEventListener(InvokeEvent.INVOKE, appInvokeHandler);
Now, listen to the event like this:
private function appInvokeHandler(event:InvokeEvent):void
  {
    trace("appInvokeHandler ::: ::::: "+event.arguments.length);
  }
That's it, if the event's arguments' length > 0, you have received the data.

So, with this, I conclude this post and the series. Hope you learnt something in the process and resolved some of the issues with AIR and Android ANEs. If you'd like to go through the other posts, this is the bast place to start:
AIR Android Native Extensions - Issues and Solutions

I would try to keep sharing my findings in the field of technology as I keep getting my feet wetter. You can drop in a note to me: pulkit.gupta@gmail.com

AIR Android Native Extensions: Part 6: Preventing Android activity from being recreated on orientation change and relaunch.

In my previous post, I explained how to package XHDPI and XLarge resources with the AIR app. In this post, I would cover how to prevent Android activities from being recreated on orientation change or resumption of app.

In the process, we'd see how to compile an AIR android app with a different Android SDK.

First, let's understand what the issue is. There are a lot of times, where you need to create native android activities. For example, if you want to use the Android WebView component to launch the web pages in your app, you'd need to create a new activity for the same.

By default, Android recreates the activity on various configuration changes, such as SoftKeyBoard open or Orientation change. To prevent the recreation of activity, we used to add the android:configChanges property to activity's declaration in the manifest:

<activity android:name="com.webview.Webview"
android:configChanges="keyboardHidden|orientation" >
</activity>

As you can notice above, we have added an attribute android:configChanges and the values for it are keyboardHidden|orientation. This much was sufficient to prevent the activity from getting recreated in Android below 3.2. However, these values alone are unable to prevent the activity from being killed in Android 3.2 and above.

Problem: Android introduced a new configuration property "screenSize" in Android 3.2, hence you need to add this as well as in the android:configChanges attribute like this:

<activity android:name="com.webview.Webview"
android:configChanges="keyboardHidden|orientation|screenSize" >
</activity>

This would tell Android to not recreate the activity on screenSize changes. However, you won't be able to use this property with the default android SDK AIR uses as it would not recognize this property and throw an error.

Solution: Updating Android SDK. No, the platformsdk compiler argument, as explained in my previous post, would not help here.

So, we'd need to replace the Android SDK in the AIR SDK's library folder. The old Android SDK JAR is at the following location:
{AIR_SDK_FOLDER}\lib\android\lib\resources\android-res.jar


You'd need to copy the JAR file from the android SDK folder, rename it as "android-res.jar" and paste it at the location above, replacing the old one. The Android SDK JAR, for say, Android 4.0, would be present at "platforms/android-15/android.jar". You can download the SDK for a particular version from the android website.

Once the above is done, you'd be able to successfully package the application with screenSize attribute added to the activity declaration.

Unfortunately, even after doing this, the Android activity may get recreated on resume on certain devices. Fortunately, adding a few lines to your manifest can fix this, see below:

<android>
<colorDepth>16bit</colorDepth>
<manifestAdditions><![CDATA[
<manifest android:installLocation="auto">
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15" />
<application android:enabled="true">
<activity android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity> </application>
</manifest>
]]>
</manifestAdditions>
</android>

As you'd notice above, I've added an activity node under the application node and assigned it a property android:launchMode="singleTop". This is what does the trick. The main thing is the launchMode, that we have set to singleTop. But, to set that we'd need to add the lines emboldened as well.

Well, this would ensure that your activity would never recreate unless you want it to. Hope this helps someone banging head against the wall, trying to figure out what to do. Like what I was doing sometime back, till I figured this out.

Do keep watching this space for my last article in this series: Launching AIR's main activity from other Android activities and passing information.

AIR Android Native Extensions: Part 5: Exporting xLarge and xHDPI resources with the AIR application

In my previous post, I explained how to use 3rd party JARs in your app. In this post, I am going to cover how to bundle xLarge resources and xHDPI assets with the AIR app.

On the higher version of Android tablets, you may not be able to get the correct UI by using standard sizes and may need to use xLarge and xHDPI values. However, when you put a xHDPI asset or xLarge resource in your res folder, you'd be able to package the ANE but when you compile the final AIR application, it would throw errors saying that these are not supported.

Reason: AIR, by default uses Android 2.2 SDK and xLarge and xHDPI were introduced in Android SDK 3.2. So, the Android SDK 2.2 doesn't recognize them.

Solution: We'd need to compile the app with an Android SDK on and above 3.2. Question is How?

There are 2 ways to update the Android SDK uses to compile AIR:
  • Using the -platformsdk compiler argument
  • Replacing the Andoid JAR in the AIR SDK

Well, sadly it is not an either/or solution. You may need to follow both the steps for different problems. For packaging xHDPI and xLarge resources, you'd need to follow step 1.

Firstly, make sure you have the Android SDK folder on your machine in the following structure:

  - android_sdk_folder
      - platform
      - platform-tools
      - tools
      - add-ons
      - AVD Manager
      - SDK Manager

Now ensure that inside the android_sdk_folder/platforms folder, you have the correct platform available i.e. if you want to use Android 4.0, there should be a folder named android-15 inside the platforms folder. You can Google for the Android SDK to API version mapping.

So, now that you have the correct SDK, what do you specify in the platformsdk? Suppose the android_sdk_folder, I spoke about is in root of d:/. The path that would go in platformsdk is d:/android_sdk_folder. Read this carefully:, you would not give the path to d:/android_sdk_folder/platform/android-15, but only till the d:/android_sdk_folder. Now, you must be wondering, how AIR would know which Android SDK to use, since there may be multiple SDKs inside the platforms folder. This is where your application's app.xml would come into play.

Open your application's app XML i.e. myproject-app.xml. You'd need to add the following line to this file:
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15" />

This implies that the minimum API version that the app supports is "8" i.e. Android 2.2 but you want to use API version 15 i.e. Android 4 to package the build. Even after adding this line, you can be rest assured that your app would work in the older android versions as well.

Your App XML's section would look something like follows:

<android>
<colorDepth>16bit</colorDepth>
<manifestAdditions><![CDATA[
<manifest android:installLocation="auto">
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15" />
<application android:enabled="true"></application>
</manifest>
]]>
</manifestAdditions>
</android>

Now, this is how, Android identifies which version of android SDK to use. So, that's it, now you'd be successfully be able to compile your AIR app with xLarge and xDPI resources.

We did not discuss the usage of Step 2 i.e. Replacing the Andoid JAR in the AIR SDK. I have covered it in the next post of the series: Preventing Android activity from being recreated on orientation change and relaunch.

AIR Android Native Extensions: Part 4: Using Third Party Libraries (Jar) with your extensions

In my previous post, I explained how to speed up access of resources in an Android ANE.

In this post, I would discuss how to bundle 3rd party JAR library code along with your ANE. There are numerous instances where we need to use JAR files in our code, for example to support an older platform compatible version for an android component, we'd need to use android-support JAR. However, when you export the JAR for your ANE, the code of the other JAR files doesn't go along. And, AIR's ADT command supports only one JAR per ANE? You are stuck because the code will not run without the other JARs.

You can use the source code instead of the JAR. However, not all 3rd party libraries would provide the source code.

Solution: Merge all your JARs into one and package the merged JAR with the ANE. Simple, you can use the following ANT script to achieve it:

<?xml version="1.0" encoding="UTF-8"?>
<project name="OpenPageMobile" default="combine-jars" basedir=".">
<target name="combine-jars">
<mkdir dir="output"/>
<unzip dest="output">
<fileset dir="jars">
<include name="**/*.jar"/>
</fileset>
</unzip>
<delete dir="output/META-INF"/>
<jar destfile="final.jar" basedir="output" />
<delete dir="output"/> 
<!--<delete dir="output"/> -->
</target>
</project>

So, what does this ANT script do, it takes up all the JARS in the jars directory, extract them, removes the META-INF file and repackage them as a single JAR in the output folder. It's important to remove the META-INF file as it corrupts the merged JAR.

You can now package this merged JAR with your ANE and all would work fine.

Quick solution, but may save you some valuable time.

The next in the blog series is: Exporting xLarge assets with the APK.

AIR Android Native Extensions: Part 3: Speeding up access of resources in Native extensions

In my previous post of this series, I explained how to access resources inside an native extension for Android. In this post, I am going to explain ways to speed up access of resources. In most scenarios, you may not need it at all, but if you are using really heavy Android UI components in your extension, like the Calendar component, you are at the right place.

I had an issue where I used a calendar component in one of my native extensions. When I ran that activity natively, it took 1 second to show up, but running the same thing in native took around 9 seconds. That's huge!!!

After a lot of research, I figured out that the issue was with Context.getResourceId calls. AIR was internally running a long loop, which used to take time accessing each resource. Calendar component had 31 cells (item renderers) and each item renderer had 9 calls to Context.getResourceId. AIR doesn't maintain a dictionary of the resources once fetched, which causes it to run the loop again and again.

As a quick fix, I maintained a dictionary of resources once fetched and it brought the load time of the activity down to 3 seconds. On loading the activity again, the time was down to 1 second only.

It was still not the ideal situation. This is where a colleague of mine, a Java bond, came to rescue. The next few lines of code are sheer magic and bring the activity load time exactly the same as native, even when launched first time. Checkout the magic:

public static int getResourceId(String resourceString)
{
    packageName = getActivity().getPackageName()+".R$";
    String[] arr = new String[2];
    arr = resourceString.split("\\.");
    Class someObject = Class.forName(packageName+arr[0]);
    Field someField = someObject.getField(arr[1]);
    return someField.getInt(new Integer(0));
}

That's it done. Magic, ain't it? Just call this function to get resource id instead of using extensionContext.

Someone please pass this code on to Adobe guys, I'd give you the name of my colleague for the credits section ;)

Hope this helps and saves your life at a time where you feel completely helpless. The next one in the series, that I have posted is: Using Third party libraries (JARs) in your android code.