Wednesday, November 23, 2011

AIR iOS- Solving [Apps must follow the iOS Data Storage Guidelines]

There are some AIR for iOS applications which got rejected due to following error.
Rejection: 2.23 Apps must follow the iOS Data Storage Guidelines or they will be rejected
Recently with the release of iOS 5, Apple updated the Data Storage Guidelines which are mentioned here(Only for the registered developers). The rejection details are similar to the one mentioned at this post. Just to keep you guys reading this article I am quoting the details from the post.

We found that your app does not follow the iOS Data Storage Guidelines, which is not in compliance with the App Store Review Guidelines.

In particular, we found magazine downloads are not cached appropriately.

The iOS Data Store Guidelines specify:

"1. Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the /Documents directory and will be automatically backed up by iCloud.

2. Data that can be downloaded again or regenerated should be stored in the /Library/Caches directory. Examples of files you should put in the Caches directory include database cache files and downloadable content, such as that used by magazine, newspaper, and map applications.

3. Data that is used only temporarily should be stored in the /tmp directory. Although these files are not backed up to iCloud, remember to delete those files when you are done with them so that they do not continue to consume space on the user’s device."

For example, only content that the user creates using your app, e.g., documents, new files, edits, etc., may be stored in the/Documents directory - and backed up by iCloud. Other content that the user may use within the app cannot be stored in this directory; such content, e.g., preference files, database files, plists, etc., must be stored in the /Library/Caches directory.

Temporary files used by your app should only be stored in the /tmp directory; please remember to delete the files stored in this location when the user exits the app.

It would be appropriate to revise your app so that you store data as specified in the iOS Data Storage Guidelines.
So, to summarize AIR developers may face this problem if they are using Local Shared Objects or AIR File.applicationStorageDirectory API to write/save the data for their application.

UPDATE: I think above mentioned APIs are used on iOS for varied use-cases ranging from using them as a disk cache to using them for storing App preferences, configuration to saving user's work (Like XMLs, Game state etc.) So before you think that following solution is for you, follow this thumb rule. 


"If you are using above mentioned APIs to store any kind of file that 

  • Can not be re-downloaded or regenerated by your application 
  •  Presence of this stored file is essential for correct functioning your application(For eg. Progress a player has made in your game). 
In such case DO  NOT to use following solution, instead provide additional explanation about the nature of files and align their purposes with the reasons mentioned above."


Let me take one more para to explain why. Caches directory is supposed to be used to store files that are actually used as caches and hence those files, if deleted, should not have any impact on the functioning of your application. iOS takes liberty to

  • Empty the caches directory in case it finds that device is under low disk space.
  • If a user updates to new version of your application. Caches directory is not retained.
All these changes are made so that only the essential and necessary files are backed up to iCloud or iTunes backup. Hence, if current version of your application was uploaded before iOS 5 release. You may face a rejection in your next release (Even if you upload almost the same application.)

There is a quick and clean workaround available for this problem if you were writing to applicationStorageDirectory. Before starting you need to decide the place where the files should go as per the guidelines. I recommend scrolling up again and reading the 3 points in Apple Rejection note, they precisely define the directory you need to use based on the kind of file you are creating.

If you have used Local Shored Objects. The solution is little trickier, because we can not control the location where LSOs are stored. Hence the only way we currently have is to "Not to use LSO" or you may want to create some class like MyLSO which provides the similar functionality using File APIs(which in turn use the following solution)

Following is the code segment that will help you solve the problem. I will, as always, post the code first and then try to explain.
        
 // If the File falls under point 2 of rejection note
 public var cacheDir:File= null;
 // If the new File falls under point 3 of rejection note
 public var tempDir:File=null;
 
 //Initialize the Objects with proper paths.
 var str:String = File.applicationDirectory.nativePath;
 cacheDir= new File(str +"/\.\./Library/Caches");
 tempDir = new File(str +"/\.\./tmp");
Here, instead of using the static Directory paths populated by File class' properties, I am creating global File objects that point to the corresponding directories as per the Apple Data Storage guidelines.Now to create/write to any file you can use code that looks similar to following.

 var fr:FileStream=new FileStream();
 fr.open(cacheDir.resolvePath("myCache.txt"),FileMode.WRITE);
 fr.writeUTFBytes("works");
 fr.close();

I think this is it. You can use the code snippets at proper places in your applications.and I you should be good to go.

Just for completeness, here you can find the Apple Documentation about Data Handling Categories.  

Monday, October 24, 2011

Removing unwanted languages from AIR for iOS apps

IMPORTANT: This article is now obsolete. After the release of AIR 3.2 a new tag in Application descriptor, called <supportedLanguages>, is introduced. To read more about this visit this great article.

As many of the AIR developers have observed on iOS platform that their application when shown on iTunes store lists around 15 different languages. This is how it is and as of now there doesn't seem to be any solution to this. Well, you can work around this problem if you have Mac and Xcode installed.

I recently posted this work around while answering this post on Adobe Forums. So I will just re-organize the solution so that its easy to follow.

Before we actually start doing this we will create one plist file which will be used in the process given below(Only if you want to create a binary that will go on App Store through Application Loader). I will first tell you what are the contents of that file. Then I will take some time to describe its significance.

Create a file called Entitlements.plist with the contents given below.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>application-identifier</key>
    <string>XXXXXXXXX.com.your.id</string>
    <key>keychain-access-groups</key>
    <array>
        <string>XXXXXXXX.com.your.id</string>
    </array>
</dict>
</plist>

In this file
  • XXXXXXX is the AppIdentifierPrefix which you can see by opening mobileprovision file in textEdit look for 'ApplicationIdentifierPrefix'. If you are not sure what mobileprovision file is you should read this first
  • com.your.id is the same id of your app which is used for mobileprovision and is given as id in app descriptor 
What is this file about?

When apple codesigns the binary it can optionally combine the signature with an XML chunk which is specified in a property list file. Now, when I say optionally I mean this is optional operation when you just want to put your re-codesigned binary on your device and test. But absolutely not when you want to upload it for the app store review. There are some rules around this and also purposes of those tags which I wont discuss here for the sake of simplicity. So now I assume you have this file created somewhere on your Mac and will start the process of creating an ipa which enlists only the languages that you support.
  1. Generate the final ipa(preferably the one which is to be put on app store) with your favorite tool. Make sure you use distribution certificate and mobileprovision if you want to upload this binary through application loader. If you just want to test you can use development profiles also.
  2. Rename the generated binary from *.ipa to *.zip and extract it. This will create a folder called 'Payload' in the directory you extracted the archive.The contents of Payload is a folder called binaryname.app. (Mac gives this a special meaning and doesnt show it as a normal folder in finder. But its a actually a normal folder/directory)
  3. Right click on the .app in finder application and select show package contents. This will actually open the .app folder to show all the contents and assets of your application.
  4. Now, Inside .app delete all such x.lproj where x is not the language that you support. This is most probably sufficient for you. but if your application happens to support a language whose lproj did not already exist you can simply create <ISO-639-Code>.lproj folder for that language.
    Cool!! So, now you have a .app that supports only the languages that you actually support. But wait... We just modified the contents of .app which in turn invalidated the signature on .app package hence in the next step we will re codesign the .app so that we have valid signature applied to the packge with modified contents.
  5. Execute the following command. (I have assumed that I have unarchived the ipa in my current working directory)
    codesign -f -s "iPhone Distribution: Your Name" 
    "--resource-rules=./Payload/binary.app/ResourceRules.plist" 
    --entitlements "/path/to/Entitlements.plist" 
    "./Payload/binary.app"
    
    above command is pretty straight-forward. The string "iPhone Distribution: Your Name" comes from your "Keychain" You must have installed the distribution certificate in your keychain. If you are using development certificate(in step 1). Your string will become "iPhone Developer: Your Name (XXXXXXXXX)" process is same, you should have installed your development certificate in your keychain to see this entry.
    You can see the we are providing the --entitlements here whose value is the path to the file we just created at the beginning. Note that this switch is optional for the command to work correctly and hence can be omitted if you are using development certificates. But If you are planning to upload this binary to app store and using the distribution certificates you must provide this switch.
  6. Now, We are done. If you want to upload this binary to app store for review, just right click on .app folder and compress it to .zip and upload this .zip through Application Loader.
    If you want to install it on device and test, compress the parent 'Payload' folder and rename the compressed .zip to .ipa and put it on your device
Hope this article helps you guys to eliminate the unsupported languages from your apps to avoid bad comments and money refunds. cheers!!

Saturday, October 22, 2011

Facebook Single Sign-On for AIR iOS Apps

With AIR 3 on the door many developers are willing to build their Mobile Applications with Adobe AIR Runtime. I need not mention separately that Social Networking remains one the biggest use cases for mobile applications which means nothing but integration with Facebook.So, today we have a great library(SWC) build for Facebook API that can be used with any Actionscript based application it can be a Web app running in browser or a desktop application built with AIR or A mobile application on Android/iOS/Playbook. One swc serves all. This can be found here.
But this article addresses a very specific problem. Facebook has provided a feature for Mobile Devices called "Single Sign-On".
A quote from Facebook developer page which best describes what this feature is all about.
One of the most compelling features of the iOS SDK is Single-Sign-On (SSO). SSO lets users sign into your app using their Facebook identity. If they are already signed into the Facebook iOS app on their device they do not have to even type a username and password. Further, because they are signing to your app with their Facebook identity, you can get permission from the user to access to their profile information and social graph.
Just to explain better. If I have 10 Facebook applications on my iPhone I will need to enter my credential in each app as and when I use them. SSO solves this problem for once and for all. You need to provide your credentials only once in the "Facebook" application (Facebook app by Facebook) and all other applications will come to Facebook app for authorizations.It also has a fallback mechanism, if the end user doesn't have "Facebook" app installed we can use Safari browser to serve the same purpose.
Ok. So lets start coding!! but before we actually start lets summarize the 5 easy steps involved and we will walk through each step.
  1. Define Custom URL for your application
  2. Set-up and use a simple native extension, which can tell us if a particular URI can be opened.
  3. Implement the Authorize method in Actionscript
  4. Handle the INVOKE EVENT of the application and Pass on the access_token and expiry_date to the Actionscript API for Facebook.
  5. Make Facebook API Calls with Actionscript API.
The basic mechanism by which this feature works on iOS is using Custom URI. On iOS any application can register a custom URI type that a perticular application wants to handle. For eg. If I have an application called "Image Viewer" I might want to tell iOS to pass any URI that start with img://... should be passed on to my application so that the application can be used to open an Image. Same is the case with facebook. If you want to try out. go to Safari browser on iOS and in address bar type "fbauth://" and say go. You can see that "Facebook" application opens up.
  1. So lets start and define one such URI for our application. It doesn't matter what IDE you are using to develop your app. So according to your directory structure and IDE locate the application descriptor xml of your AIR iOS app. and add the following XML chunk as a direct child of top level Application tag.
    <iPhone>
            <InfoAdditions><![CDATA[
       <key>UIDeviceFamily</key>
       <array>
        <string>1</string>
        <string>2</string>
       </array>
       <key>CFBundleURLTypes</key>
       <array>
        <dict>
         <key>CFBundleURLSchemes</key>
         <array>
          <string>fbYOUR_APP_ID</string>
         </array>
        </dict>
       </array>
      ]]></InfoAdditions>
            <requestedDisplayResolution>high</requestedDisplayResolution>
        </iPhone>
    
    The above chunk will tell iOS that whenever any application on device wants to open a url of the form fbYOUR_APP_ID://... iOS should open your application to handle that URL. Similar to saying that whenever we open url that starts with http:// it is opened with safari. The facebook app will use fbYOUR_APP_ID:// url to open our application back on successful login and pass all the required data along with this URL which our application can use.
    you can test this right away. just create an ipa install it on your device and then in safari try typing fbYOUR_APP_ID:// and hit go. It should launch your application.
  2. Now, lets move to the next problem. What if the user doesn't happen to have the Facebook application installed? To understand the problem in more detail, Just the way our app registers for a custom URI, The facebook app also registers for a custom URI scheme "fbauth://..." essentially this is how we will transfer control to facebook app for authrization, By opening a fbauth:// based URI, but if the end user does not have facebook app installed on his device there will eventually be none who understands the meaning of fbauth://. What do we do then? In such case we fall back to safari browser and use that for authorization. Currently in AIR we do not have in way of knowing if the iOS device has an application which can handle a particular URI scheme hence we need to a write a small native extension which provides a function called canOpenUrl(string) . This tutorial will not focus on how do we write native extensions you can get the one I have written from here and follow the steps to use it in your project.
    • Add the following XML chunk in your application descriptor as a child of top level application tag.
      <extensions>
          <extensionID>com.sbhave.openUrl</extensionID>
      </extensions>
      
      This will tell ADT that your application uses a native extension.Identified by the given extensionID. Note that Native extension is a new feature added in AIR 3. You must use AIR 3 SDK in order to use native extensions.
    • In order to get the Actionscript functions(which actually call the native obj-c code) recognized by your AIR iOS application. You need to externally link to the swc(Provided in zip). To this in Flash Builder go to project properties ->Flex Build path and Add the SWC. Once added double click on linkage type and make it "external" this step is very necessary.If you are using Flash Develop provide the swc path in external-library parameter
    •  Ok, good to go. Now the only thing to keep in mind that the currently released Flash Builder is not aware of packaging an application which uses Native extension and hence you need to package the application from command-line (help) and at the very end provide a new AIR 3 switch -extdir <Path to folder where .ane file is kept.>
    • We will use the functionality provided by this ANE in next steps. For now everything is set-up.
  3. Now we will implement the authorize method which does all the work of authentication. I will first share the code and then try to explain what it actually does.
    protected function authorize(appId:String,extPerm:Array,forceBrowser:Boolean=false)
    {
     var url:String = "fbauth://authorize";
     var isFbAppAvailable:Boolean = URLUtils.instance.canOpenUrl(url);
     var ops:URLVariables = new URLVariables();
     ops["client_id"] = appId;
     ops["type"] = "user_agent";
     ops["redirect_uri"] = "fbconnect://success";
     ops["display"] = "touch";
     ops["sdk"] = "ios";
     if(extPerm != null && extPerm.length > 0)
      ops["scope"] = extPerm.join(",");
     if(!isFbAppAvailable || forceBrowser){
      url = "https://m.facebook.com/dialog/oauth";
      ops["redirect_uri"] = "fb"+appId+"://authorize";
     }
     var req:URLRequest = new URLRequest(url);
     req.data = ops;
     req.method = URLRequestMethod.GET;
     navigateToURL(req);
    }
    
    This is a very simple method which just sets the data and constructs the url. navigateToUrl actually tries to open the Url which indeed opens the application which has registered to handle the particular type of url scheme. This function expects two parameters. first one is the application ID that you get when you first create your application on facebook developer portal. extParams is a string array which enlists the permissions required by you application.The last parameter allows you to force the use of browser and avoid using the native FB app for authentication. We will see its use in next step when we handle the invoke event. an example call is shown below.(Please read step 4 before you actually write the following call in your code. You may want to check the LSO. See the sample code at the end of tutorial.)
    authorize(APP_ID,["read_stream"]);
    
    You can call this whenever you want the user to authenticate and authorize your application.For various permissions that you can specify see this . It is assumed for the sake of simplicity that APP_ID is the global level constant in your application which set to your facebook application ID.
  4. Till this point, If you can test you application, whenever you call the authorize method you will be redirected to the FB app or Safari depending on availability and the user can get himself authenticated there and once done he will be redirected to your app. Now lets see how we can handle the data we get from facebook. We do this with the help of INVOKE event given by AIR. Whenever the application is invoked as a result of custom URL. we get the link that triggered our app in InvokeEvent's arguments array. This is link is where we have all the data that we need. In order to do this. Add a handler for INVOKE event in your application, earlier is better but must be added before calling the authorize() method. Eg. is shown below.
    NativeApplication.nativeApplication.addEventListener(InvokeEvent.INVOKE,onInvoke);
    
    onInvoke is the function where we will process all the data received as a result of authentication.I will again repeat my habit of first scaring the reader with the code and then trying to explain.
    protected function onInvoke(e:InvokeEvent):void{
    
    var str:String = e.arguments[0];
    if(str && str.indexOf("fb"+APP_ID+"://") != -1 )
    {
     var vars:URLVariables = FacebookDataUtils.getURLVariables(e.arguments[0]);
     var accessToken:String = vars.access_token;
     if(!accessToken || accessToken == ""){
      var err:String = vars.error;
          
      if(err && err == "service_disabled_use_browser"){
       authorize(APP_ID,["read_stream"],true);
      }
          
      else if (err && err == "service_disabled"){
       // We cant use SSO at all use the old FacebookMobile.init() and FacebookMobile.login()
      }
       
      var errCode:String = vars.error_code;
      var userDidCancel:Boolean = !errCode && (!err || err == "access_denied"); 
          
      if(userDidCancel){
       // User cancelled the login and authentication
      } 
     }
         
     else{ // Login was successful
      var expiresIn:int = int(vars.expires_in);
          
      if(expiresIn != 0){ // Everything went just well
       var expDate:Date = new Date();
       expDate.seconds += expiresIn;
       FacebookMobile.init(APP_ID,startUsingApi,accessToken);
       
                            // Store FB details in LSO
              var diskCache:SharedObject = SharedObject.getLocal("diskCache");
       diskCache.data["token"] = accessToken;
       diskCache.data["expDate"] = expDate;
       diskCache.flush();
       diskCache.close();
       diskCache = null;
      }
     }
    }
    }
    
    Lets go through the code bit by bit In the first line we store the first argument in a variable. As there can be various other methods of opening(invoking) the app. This can very well be null. That's what we do in the if statement that follows. We also check that the first argument is actually the result of triggering our fbUrl. If you go back and see the step 1. URLSchemes are actually an array and hence your app can register for more than one url scheme. We do absolutely nothing if any of this tests fail
    As our ultimate goal is to hook up this authentication with Actionscript API for facebook.I can safely assume that you have already added a reference to the AS FB API swc file.FacebookDataUtils.getURLVariables is a function in the SWC that decodes the URL and return an object, using which we can access various parameters passed along with URL by making use of normal dor(.) operator.for eg. if I pass to the function a url like abcd.com?a=b&c=d I can access the data as result.a and result.c, and this is exactly how we will use vars.
    The very first this that we do is to try getting an access token this is the key for API. If this access token happens to be null or empty we have hit some problems. which we handle in the if block. Generally, if we dont get the token we get a variable called error which describes the error that may have occurred
    • service_disabled_use_browser: The SSO serve is disabled from FB app and hence we need to try with browser. This is where we use the third parameter of the authorize() which forces the use of browser.
    • service_disabled: SSO service is disabled completely we need to fall back to the old method of using SWC init and login methods.
    • The next few lines decides if user purposely canceled the process of authentication. and
    If we enter the else block we have made it and we have an access token every time we get the access token we also get with it an integer called expires_in these are the number seconds for which the token is valid. If we have a non zero value we have received a valid usable token. In such case we will call the FB API SWC's init method, but with the third parameter as we already have a valid access token now.
    But to make developers and end users' life even easier. We store the access token and expiryDate(Pay attention to how the expiryDate is generated from expiresIn value) to the local object. WHY? Reason being: What if user closes the app soon after authentication, and opens it back in just few ours? though he need not enter his credentials again (This is what SSO is all about), He will get a round trip to FB app(Showing that he has already authorized the application) and then back to your app. Instead what we can do is that in step 3. before we call the authorize method we can check the same local shared object and check if we already have a non-expired and valid access token. In such case we do not need to call authorize at all.
  5. Now the last and most fun part that is to actually use the FB API to make calls and get the real data from facebook. In the earlier step we have passed a callback name called startUsingApi this will be the function that will get called when Facebook API completes the initialization. An example emplementation is shown below.
    protected function startUsingApi(session:Object, error:Object){
     var p:Object = new Object();
     p.limit = 0;
     FacebookMobile.api("/me",function(result:Object, fail:Object):void{
      if(null == fail){
       trace(result.name);
      }
     },p);
    }
    
    
    Just to trace the user's name.
That is all. Actually it doesn't seem so big when you actually do it. You can find the complete implementation here.