Tuesday, June 14, 2016

Setting up a Google Play Expansion File (.obb) for easy Android WebView reference

Scoping the problem

A PhoneGap app I'm working on has a lot of image and sound files.  We have the iOS version working fine, but the Android version is roughly 200MB, a full 100MB over the maximum binary size allowed by Google Play.

Google Play tells us that we can create a separate ".obb" (Opaque Binary Blob) expansion file where we can offload some of that content.  The OBB is uploaded along with the APK,

Ok, sounds great.  But before creating the OBB, I know I'm going to have to access the files in the OBB from HTML passed to an Android WebView.  Ideally,  we want simple file references without writing a lot of code.  For example:

<img src="file://SomePathToOBB/photos/photo1.png">

I didn't want to have to unpack the OBB, so after doing some Stack Overflowing, here and here, I decided to pursue this SO suggestion:
It is a good alternative to use JOBB tools to pack expansion file and use StorageManager to mount it. After that the files inside expansion file should be accessible as regular files in the file system. The unpacking of expansion file is not needed in this case.
An OBB file can be of any type (.pdf, .zip, etc), but a file created with the JOBB utility can be mounted using StorageManager and requires no extra work at runtime (like extracting from a zip file).

Creating the .obb using JOBB & loading on a test device

To create the .obb, I moved directories of image files into a directory named materials, and then ran the following* from the command line on my Mac:
/Users/mikem/Library/Android/sdk/tools/jobb -d materials -o my.obb -pn com.gwhizmobile.example -pv 1
* com.gwhizmobile.example represents the app package name

Easy!  The OBB was created as my.obb.  According to my reading of the docs, when I upload it to Google Play, it will automagically rename the file into something like main.1.com.gwhizmobile.example.obb.  However, I wanted to be able to test it on my Nexus 5 test device, so I renamed it into the proper format, and put it into a directory structure with the following path:
./obb/com.gwhizmobile.example/main.1.com.gwhizmobile.example.obb
This structure allowed me to use the Android File Transfer utility for Mac to copy the obb directory onto my device (in the Android folder)

So at this stage, I have the .obb file built and residing on my Nexus 5 where it is supposed to be.

Using Storage Manager to mount the .obb and get contents path

Next, we need to add some code that allows us to mount the obb.  Between here and here, I generated this little snippet of code**:
    
  String pathReference = null;

  final StorageManager storageManager = (StorageManager) context.getSystemService(context.STORAGE_SERVICE);
 
  final String packageName = "com.gwhizmobile.example";
  final String packageVersion = "1";

  final File mainFile = new 
 File(Environment.getExternalStorageDirectory() +  "/Android/obb/" + packageName + "/"+ "main." + packageVersion + "." + packageName + ".obb");

  storageManager.mountObb(mainFile.getAbsolutePath(), null, new  OnObbStateChangeListener() {

     @Override

     public void onObbStateChange(String path, int state) {
       super.onObbStateChange(path, state);
       if (state == OnObbStateChangeListener.MOUNTED) {
         pathReference = storageManager.getMountedObbPath(path);
       }
    }
  });

** Since this is a PhoneGap app, this code was encapsulated within a simple homegrown Cordova plugin.

Final integration steps

The generated pathReference value looks something like file:///mnt/obb/123e187987192397831.  This is the prefix we can use in the HTML, as in:
 <img src="file:///mnt/obb/123e187987192397831/photos/photo1.png">
Works. Like. A. Champ.

A few other notes

  1. Although we created the .obb by pointing it at the materials directory, that directory name is not included in the filename path.
  2. GooglePlay will not allow you to upload the OBB file with your first APK!?  However, after doing another build process the OBB can be loaded.
  3. Note that the packageVersion in the code above will need to change since GooglePlay will rename the OBB file to the version of the APK loaded at the same time as the OBB.



Thursday, March 24, 2016

A Guide for Engaging in Political Discussions and Not Looking Like an Idiot

During this 2016 political season, there's been a distressing outbreak of bad grammar, misspellings, mangled abbreviations, and poor reasoning on political websites and Twitter.   Although the offenders are largely from Team Trump, there's a fair amount from other camps as well.
With that in mind, I consider it my patriotic duty to improve the political discourse in these United States, and I'll address that obligation in the form of this blog post.  :)  I welcome comments, suggestions, and corrections.

Disclaimer:  I don't pretend to be a grammarian, and some of this writing probably violates at least a few rules in The Little Brown Handbook - but somebody must take the first step - it might as well be me!

Here goes, and may God bless America (and keep her literate).

GRAMMAR

You're is the shortened form of you are, not  yourYour is a possessive, as in: Your lack of basic grammar skills makes it seem like you should be in summer school instead of commenting on presidential politics.

They're is the shortened form of they areTheir is a possessive - for example, they are wasting their vote if they vote for your guy.  Additionally, there refers to a place, as in, there they go again 

SPELLING

States do not succeed from the union, they secede.   Perhaps they successfully seceded?

Conceded does not mean the same thing as conceited.  All of us cool kids know this already.

If you use loosing when you should have used losing then you've already lost.

Lieing is not a word and I ain't lying.  Same with lier.

The problem with illegal immigration is not boarder security, but border security.  (Unless you're referring to a lack of rooms in your house).

Traders and Traitors are not the same.  Traders trade;  Traitors don't trait.


ABBREVIATED WORD MEANING
  
There is no such thing as a Super Pack unless you're talking about these.  You are probably referring to a super PAC, where PAC stands for Political Action Committee.

A Rhino isn't the same as a RINO (Republican In Name Only).  That thing on my face is a nose, not a horn. (although I can understand the confusion)

i.e. and e.g. are not the same.  i.e. (id est) translates to "that is", and e.g. (exempli gratia) means "for example".


REASONING & ARGUMENTS

Starting a sentence with The fact is... implies that it will be followed by verifiable facts, not opinions. 

Never use never and always avoid always - Using these adverbs weakens your argument since they are usually logically false.    For example, saying something like "Conservatives never treat poor people fairly" or "Liberals always hate America" is demonstrably untrue.

Don't use strawman arguments (setting up a false proposition and then knocking it down).   "Even though liberals want to take all of our guns, we need those weapons for self-defense!".   "Even though conservatives want poor people to die early, we still need universal health care!"

Calling someone a dumb-ass, liar, or other school yard taunt will not help your cause.  It may make you feel better, and may even be true, but it will shut down any hope of educating your opponent to your point of view.   What is the point of arguing if not to make your opponent see the light (or perhaps learn something)?

NOT A WORD

Hugely



Thursday, February 18, 2016

Facebook Audience Network Ad Approval

(Feb/2016)

I've been working on integrating Facebook Audience Network ads.    Before going "live", Facebook says that they have to approve the ad placement - they say this approval is initiated automatically when you've had 5 successful test ads and that the approval takes 2-3 business days.

In my case, I displayed some test ads on the simulator, and also on a device.  They tell you to use the API and register the device with a hash code they provide.

After almost a week, I hadn't heard anything from Facebook and was wondering what was going on.  After doing some online research, I found a discussion that suggested that the number of impressions was important.

I reran the app - this time with it running on a local device connected to Xcode.  On each refresh, it would write  [FBAudienceNetworkLog/FBAdUtility:396] Impression is logged (displayed for test ads only)  to the console. 

I let it go for a few minutes until there were at least five of these impressions.

Then, to my surprise, less than a minute or two later, I received a Facebook alert saying that the app had been approved.

I had a second app that I repeated the process with.  Again, after just a few minutes, the app was approved for ads.

*** Update 3/28 ***

Okay, after doing a few more apps, it seems that Facebook has changed the process such that they just automatically review apps after a few days.    I'm guessing that the number of impressions still matters, but it seems disassociated from when they actually review. 

Thursday, October 22, 2015

iOS 9 Issues

This is just a list for keeping track of iOS 9 related issues I've run into.


1) If you get a list of font families, some of those families do not contain any actual fonts - only seems to affect newer devices.  (reported to apple - marked as dupe)

2) Weird issues with UIView hit test not propagating.   Oddball things where it works everywhere but on an iPhone 6s.  Generally, once recompiled, it works fine.  Actually, I've found that recompiling or changing the hit test code does not work.  In fact, any old style touchesBegan type tap handling does not work on the iPhone 6s.

3) CSS Sibling selectors do not work in certain conditions, for example:

input:checked + label ~ div {
    z-index: 1;
}


changing to:

input:checked + label + div {
    z-index: 1;
}


does work, but doesn't work in iOS 8.   Adding both rules seems to do the trick.

4) Using willRotateToInterfaceOrientation to force a UISplitViewController to refresh the layout no longer works.  Should use  setPreferredDisplayMode instead, as in:

    [sv setPreferredDisplayMode:(hideMaster)?UISplitViewControllerDisplayModePrimaryHidden : UISplitViewControllerDisplayModeAllVisible];

for example.

5) External http calls not working (after iOS 9 recompile).  Need to add

NSAppTransportSecurity
   NSAllowsArbitraryLoads  : YES

to info plist.




Tuesday, October 6, 2015

How to use the UIFocusGuide for navigation

I really didn't quite understand how UIFocusGuides were supposed to work until after several hours of banging my head against the wall.  I think I got it now....

The focus guide, like the layout guide it extends, can be thought of as an invisible view - in this case, an invisible focusable view.

A UIFocusGuide functions as a bridge between different areas of the screen since focus navigation only works up/down or left/right.

Consider a screen that has two objects A and B.  

     AAAA

               BBBB

With the default focus behaviors, there is no way to transition from A to B because B is not directly to the right or bottom.

Thus, to make this work, you'd need a focus guide in between, like


     AAAA

FFFFFFFFFFFFFFFFFFFFF

               BBBB


and we would set the focus guide's preferredFocusedView = B to start.  

So, given this example layout, if view A has the focus, and the user navigates down, it will find the focus guide (F), and the focus guide will then redirect the focus to the guide's  preferredFocusedView, in this case B.

Of course, if B has the focus, you'd want to update the focus guide so its preferredFocusedView is now A, so that when the user navigates up, it goes from B to A.

Okay, now that you get the concept, what's the code look like?  Something like this (Warning: I have not compiled/run this segment):


UIFocusGuide *F;

- (void)viewDidLoad
{
   [super viewDidLoad];
   
   F = [UIFocusGuide new];
   [self.view addLayoutGuide:F];

   [F.topAnchor constraintEqualToAnchor:A.bottomAnchor].active = YES;
   [F.heightAnchor constraintEqualToConstant:1].active = YES;
   [F.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES;
   [F.rightAnchor constraintEqualToAnchor:self.view.rightAnchor].active = YES;

   [F setPreferredFocusedView:B];
}
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{

   if (context.nextFocusedView == A){
       [F setPreferredFocusedView:B];
   } else if (context.nextFocusedView == B){
       [F setPreferredFocusedView:A];
   }
}


Apple has a simple example in the UIKitCatalog demo that demonstrates the concept in swift.  The layout in that example case is more like:

AAAAA    BBBBB

CCCCC    FFFFF

and it shows the navigation between B and C via F.

Monday, August 31, 2015

Upload a file to Google Drive via PHP using REST

Disclaimer: I am (at best) a novice PHP coder.  There may well be better ways to accomplish this task, but this is what worked for me.

First off, Google does provide a nice-looking PHP library for working with Drive, but I was reluctant to introduce more libraries into our already complex and fragile server environment.

Using REST, Google's  documentation explains that there are three ways to upload files to Drive - using a media upload, a multipart upload, and a resumable upload. 

For our task, we were working with small spreadsheets so the resumable upload was overkill.

We really wanted to use multipart but gave up after many hours of trying but getting only 400 - Bad Request errors.  We tried using the exact type message that Google detailed in its documentation, but it still didn't work.   (Had I been a better PHP programmer, perhaps I could have figured out what was wrong).

Anyways, we moved on and used a combination of a media POST, to upload the file contents followed by a PATCH, to update the title.  Here's the code:


POSTing the file to upload.  (Assumes you have an authorization token).  The result of the file_get_contents call will be a JSON structure that represents a file resource.


$authstr = 'Bearer '.$token;

$fileToUpload = "datafiles/".$_FILES['uploadedFile']['name'].".csv";

$url = 'https://www.googleapis.com/upload/drive/v2/files?uploadType=media&convert=true'; 

$options = array(
        'http' => array(
            'header'  => "Content-type: application/vnd.ms-excel\r\nAuthorization: ".$authstr."\r\n",
            'method'  => 'POST',
            'content' => file_get_contents($fileToUpload),
        ),
 );
$context  = stream_context_create($options);
$result = file_get_contents($url, false, $context);

$json = json_decode($result);


PATCHing the file to change the title.  We use a different URL here and identify the file using the File Id returned from the media upload POST.  We tell it that we're passing a JSON structure and send along the attributes to change.


$url = 'https://www.googleapis.com/drive/v2/files/'.$json->id;
 

$options = array(
        'http' => array(
            'header'  => "Content-type: application/json\r\nAuthorization: ".$authstr."\r\n",
            'method'  => 'PATCH',
            'content' => '{"title" : "My New Filename"}',
        ),
 );

$context  = stream_context_create($options);

$result = file_get_contents($url, false, $context);
$json = json_decode($result);



Have a better solution?  I'd be interested to hear about it....




Thursday, May 28, 2015

Backing up Cordova databases, local storage, and CDVLocalStorage on iOS

On iOS, Cordova has a complex class, CDVLocalStorage which manages backup behavior via the BackupWebStorage flag.   It was a little hard to understand the gist of what was going on, so I'm describing the general process of how it works here.... Of course, please note that this information has a limited shelf life due to new releases of Cordova and iOS.

Another note is that if you're building with Xcode, the  BackupWebStorage flag is in the config.xml in the Staging area.

Anyways, there are three available values, none, local, and cloud.

When set to none, Cordova does very little and defers to the default iOS behavior which is to store local storage and WebSQL databases in Library/Caches.  This is an area which will not be backed up using iCloud. 

local has the most complex activity.  When set to local, Cordova adds a listener so that when the user exits the app, the databases and local storage will be copied to the Documents/backups directory.  This allows a user plugging their device into their computer and running iTunes backup to save those databases/local storage.  However, to prevent the iCloud backup from copying these files, the Documents/backups directory is marked with the "do not backup" flag.

When BackupWebStorage is set to cloud, the following is set:

[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"WebKitStoreWebDataForBackup"];

This flag causes iOS to move the databases/local storage files from Library/Caches to Library/WebKit.  Once there, the databases/local storage will be picked up by the iCloud backup.


CDVLocalStorage also creates various Cloud/NoCloud directories but it appears that none of these are ever used.