Monday, August 19, 2013

Android - WebView - JavaScript - onPageFinish() and stuff in between

OK... so let start by saying I'm working with some very talented people...

I've been digging in Android sources for days searching for the cause for this, I've even offered a bounty for 100 points which no one claimed.

I've seen many similar issues such as this, across StackOverFlow...

The main issue is that the loading of the page is completed, and then the onPageFinished event is called with a seemly random delay that can range from 0.1 - 40 sec, only after the <GATE-M>DEV_ACTION_COMPLETED</GATE-M> is printed to the log.

Here is the code snippet:

webView = (WebView) view.findViewById(R.id.WebView);

webView.setWebViewClient(new WebViewClient() {

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        logDebug("Loading URL: " + url);
        super.onPageStarted(view, url, favicon);
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        return WrappingClass.this.shouldOverrideUrlLoading(view, url);
    }

    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        logInfo("Injecting JavaScript to webview.");
        webView.loadUrl("full-js-here");
    }

    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        logError("error code:" + errorCode);
        super.onReceivedError(view, errorCode, description, failingUrl);
    }
});

WebSettings webSettings = webView.getSettings();
webSettings.setSavePassword(false);
webSettings.setSaveFormData(false);
webSettings.setJavaScriptEnabled(true);
webView.requestFocus(View.FOCUS_DOWN);
webView.loadUrl("url");

So the solution is quite a hack but it is wonderful... Check it out:

Somewhere in your class declare the following:
final class ObjectExtension {

    @JavascriptInterface
public void onLoad() { logInfo("onLoadCompleted"); WrappingClass.this.onLoadCompleted(); } }
public void onLoadCompleted() {
    webView.loadUrl("full-js-here");
}


And before the URL loading add the following:
webView.addJavascriptInterface(new ObjectExtension(), "webviewScriptAPI");
String fulljs = "javascript:(\n    function() { \n";
fulljs += "        window.onload = function() {\n";
fulljs += "            webviewScriptAPI.onLoad();\n";
fulljs += "        };\n";
fulljs += "    })()\n";
webView.loadUrl(fulljs);
webView.loadUrl("url");

This registers a callback for the onLoad event of the WebView window, which is loaded long time before the onPageFinished is called, because of that Android WebView issue.

So the trick is that we inject the onLoad callback before loading the url, later (next line) we load the url, once the onLoad callback is called, in our onLoad implementation we call to the Java API, which in its turn inject the Javascript into the loaded page, and sometime long after that the onPageFinished is called.

End of story...
---- UPDATE ----

It has been a very long while since that post... I've wrote SocialApp(link at the top Left), which is entirely a WebView application, a multi WebView applications, which runs Javascripts on all of the WebViews and monitor the beginning and ends of scripts, and runs Javascript on a WebViews in the background....

What I'm trying to say is, if you have any questions, I'm pretty sure I can answer them, so ask away...


---- UPDATE ----

It has been a very long while since I posted that update, I really hoped to release it as an open source, and was struggling with it for a long time, but as it was pretty much forced on me due to the architecture I've been decided to append this "CyborgWebView" to Cyborg, which is a license based SDK, You can find it here.

Also, if the post is not clear enough, and you prefer a sample project, let me know...







Monday, August 12, 2013

Multi-Language Android application

I'm going to add localization to our application...

I know Android to its core, but I haven't used localization before, (never needed to).

So I'm so thrilled to start this new journey, and be able to share my insights...

Given the fact that I'm a critic, this should be joyful :)

So here I go:

If I've got the basic correct, then:

  • If you want to target a specific locale, your values folder containing the strings.xml should be 'values-xx-rYY', note that the locale region referred in the specs is with underscore, while the values folder MUST NOT have underscores in them.
  • If you want to target a group of locales, and by group I mean locales which fits 'xx_*', then your values folder should be 'values-xx'.
First you would like to know which are the available languages, I've got it from here:
(I have no idea how credible the data is, but it is a start)

Arabic, Egypt (ar_EG)           
Arabic, Israel (ar_IL)          
Bulgarian, Bulgaria (bg_BG)     
Catalan, Spain (ca_ES)          
Czech, Czech Republic (cs_CZ)   
Danish, Denmark(da_DK)          
German, Austria (de_AT)         
German, Switzerland (de_CH)     
German, Germany (de_DE)         
German, Liechtenstein (de_LI)   
Greek, Greece (el_GR)           
English, Australia (en_AU)      
English, Canada (en_CA)         
English, Britain (en_GB)        
English, Ireland (en_IE)        
English, India (en_IN)          
English, New Zealand (en_NZ)    
English, Singapore(en_SG)       
English, US (en_US)             
English, South Africa (en_ZA)   
Spanish (es_ES)                 
Spanish, US (es_US)             
Finnish, Finland (fi_FI)        
French, Belgium (fr_BE)         
French, Canada (fr_CA)          
French, Switzerland (fr_CH)     
French, France (fr_FR)          
Hebrew, Israel (he_IL)          
Hindi, India (hi_IN)            
Croatian, Croatia (hr_HR)       
Hungarian, Hungary (hu_HU)      
Indonesian, Indonesia (id_ID)   
Italian, Switzerland (it_CH)    
Italian, Italy (it_IT)          
Japanese (ja_JP)                
Korean (ko_KR)                  
Lithuanian, Lithuania (lt_LT)   
Latvian, Latvia (lv_LV)         
Norwegian-Bokmol, Norway(nb_NO) 
Dutch, Belgium (nl_BE)          
Dutch, Netherlands (nl_NL)      
Polish (pl_PL)                  
Portuguese, Brazil (pt_BR)      
Portuguese, Portugal (pt_PT)    
Romanian, Romania (ro_RO)       
Russian (ru_RU)                 
Slovak, Slovakia (sk_SK)        
Slovenian, Slovenia (sl_SI)     
Serbian (sr_RS)                 
Swedish, Sweden (sv_SE)         
Thai, Thailand (th_TH)          
Tagalog, Philippines (tl_PH)    
Turkish, Turkey (tr_TR)         
Ukrainian, Ukraine (uk_UA)      
Vietnamese, Vietnam (vi_VN)     
Chinese, PRC (zh_CN)            
Chinese, Taiwan (zh_rTW)

Next thing you would like to do, is be able to switch the languages dynamically.

Why? Simply because it would take far less time to evaluate each screen while switching languages, and been able to see the twigging each causes.
To do that you will first need to distinct whether the application runs in debug mode, or in production...

What I've done, is added a menu while running in debug, then launched a dialog for choosing the language I want to display.

A code snippet for changing the Locale:

 Resources res = getApplicationContext().getResources();
 DisplayMetrics dm = res.getDisplayMetrics();
 android.content.res.Configuration conf = res.getConfiguration();
 conf.locale = newLocale;
 res.updateConfiguration(conf, dm);

Afterwards I've tried to dismiss the dialog which for some weird reason it did not work... but I've found the solution for it.

The last part is rendering the UI... That was one of the biggest hacks I faced!
I'll start of and say that in order to render the UI without writing 10 TONs of code, you should(I think MUST) have a parenting layer of, Application, BaseActivity, BaseFragment, BaseDialogFragment, and so on...

Since the only way (I could find) to check if the locale had change within an activity, in its onResume you need to compare the Context.getResources().getConfiguration().locale, with a value which you save on each onResume, in your BaseActivity, this value I believe can be static.

Once you've recognize a change in the locale, there are a couple of approaches you can and need to take:
  • You can refresh your activity with its intent. (Re-launch the activity and close the current one)
  • You can refresh your activity manually. (Rener the views one by one)
  • If you are using a ViewPager, make sure you are setting its adapter via Handler.post(...), otherwise you might get a 'Recursive entry to executePendingTransactions' error.
  • Since I'm loading all my fragment dynamically, after an activity has gone to background, e.g. the onSaveInstanceState was called on the fragments, I could not commit the new changes to the FragmentTransaction, I had to use the commitAllowingStateLoss.
And I was done...

All took less then a day, there is no greater satisfaction then to see the languages changes live in front of your eyes!

Good luck...

(I'm going to number these as I think there are going to be more then one... prof is in the comments :))
NOTES:

  1. Since Android 4.2 the API to change the Device's Locale has been disabled for non-os-signature applications, so all the Locale changing apps are DOOMED...

-- UPDATE --

I've released Cyborg not too long ago, and Language Swapping is build in and optimized feature in, You can find it here.

Wednesday, August 7, 2013

Custom view will not render

If you have ever created a custom view, one which inherit from ViewGoup, and by chance had no Children... actual View children, and then tried to override its onDraw(...), or draw(...), and you have called invalidate in vain, debugged for far too long only to find out that view will not render...

Well the reason actually makes sense, it is a container... there are no children SO... DO NOT RENDER!

But still, it might have a background... ?

In any case to solve this, you are required to add the following line to all of your custom view constructors:

super(...);
setWillNotDraw(false);

This would force the view group to render... although it has no children.

Monday, August 5, 2013

Android - APK Signature check programmatically in runtime

I've been at this for some time... I've tried to make my application distinct between debug version, release version, in some cases staging version, free, paid premium... and the list goes on and on...

So... I've asked the pro's, it was hard for me to describe the question, but I think it is right. I got no answer so I've went and searched a bit, and found an article explaining how this things work, I'm not going to go by all the explanation, I'll just lay it down.

First we define the certificates:

private enum SignedCertificate {
 Debug("...", null, true, true),
 Staging("...", Environments.Staging, false, true),
 Production("...", Environments.Production, false, false),
 Unknown("There is no such a certificate", Environments.Production, false, false), ;

 private final String md5;

 private final Environments environment;

 private final boolean debugMode;

 private final boolean showsLogs;

 private SignedCertificate(String md5, Environments environment, boolean debugMode, boolean showsLogs) {
  this.md5 = md5;
  this.environment = environment;
  this.debugMode = debugMode;
  this.showsLogs = showsLogs;
 }

 public static final SignedCertificate getCertificate(String md5) {
  md5 = md5.toLowerCase();
  for (SignedCertificate certificate : values()) {
   if (certificate.md5.toLowerCase().equals(md5))
    return certificate;
  }
  return Unknown;
 
}

The MD5 for each certificate can be copied from the export dialog in the final step of exporting a signed application in Eclipse, for debug md5 sign it with you debug keystore.

In order to retrieve the current certificate in runtime:

private void checkCertificate() {
 try {
  PackageManager pm = application.getPackageManager();
  Signature sig = pm.getPackageInfo(application.getPackageName(), PackageManager.GET_SIGNATURES).signatures[0];
  String md5Fingerprint = doFingerprint(sig.toByteArray(), "MD5");
  certificate = SignedCertificate.getCertificate(md5Fingerprint);
 } catch (Exception e) {
  logInfo("Error getting certificate, assuming release version...", e);
  certificate = SignedCertificate.Production;
 } finally {
  logInfo("Found Certificate: " + certificate.name());
  Log.setShowLogs(certificate.showsLogs);
 }
}

protected static String doFingerprint(byte[] certificateBytes, String algorithm)
  throws Exception {
 MessageDigest md = MessageDigest.getInstance(algorithm);
 md.update(certificateBytes);
 byte[] digest = md.digest();
 
 String toRet = "";
 for (int i = 0; i < digest.length; i++) {
  if (i != 0)
   toRet += ":";
  int b = digest[i] & 0xff;
  String hex = Integer.toHexString(b);
  if (hex.length() == 1)
   toRet += "0";
  toRet += hex;
 }
 return toRet;
}

After retrieving the certificate, implement any logic you may want according to the certificate, I've used the enum to define logs or debug mode, working environment and so on...

The nice thing about this implementation, is that it does not care about the application package name Google uses... if you have shared code between free and paid version of the application, and you would like to distinct them programmatically, I think this is a very nice solution...

Leave your comments below... :)

-- UPDATE --

I've released Cyborg not too long ago, and this Certificate Validation is build in and optimized further in terms of API and how much code you need to write to get it to work, You can find it here.

Dynamic DialogFragment - Android

This is frustrating... I've been playing with this stupid DialogFragment for half a day, trying all sort of pieces of code, Questions from StackOverFlow, just to realize, that they are all useless!!

It is as simple as the fact that when using the DialogFragment the LayoutParams of the parent view are not been set, thus renders the dialog badly.

After some thinking about it I've came up with this:

In res/values/stylex.xml:

<resources>
    ...
    <style name="AlertDialogStyle" >
        <item name="android:cacheColorHint">@android:color/transparent</item>
        <item name="android:windowBackground">@android:color/transparent</item>
    </style>
   ...
</resources>

In res/layout/alert_dialog_body.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/tools"
    android:id="@+id/xx"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@android:drawable/alert_dark_frame"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/Title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15dp"
            android:layout_marginTop="10dp"
            android:text="Large Text"
            android:textAppearance="?android:attr/textAppearanceLarge" />

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="2dp"
            android:layout_marginRight="2dp"
            android:layout_marginTop="5dp"
            android:contentDescription="Separator"
            android:scaleType="fitXY"
            android:src="@drawable/settings_separator"
            app:ignore="HardcodedText" />

        <TextView
            android:id="@+id/AlertBody"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginBottom="5dp"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_marginTop="5dp"
            android:text="TextView"
            android:textAppearance="?android:attr/textAppearanceMedium" />

        <Button
            android:id="@+id/DialogButton"
            style="?android:attr/buttonStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginBottom="10dp"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:layout_marginTop="15dp"
            android:text="Button" />
    </LinearLayout>

</RelativeLayout>
(The RelativeLayout is the trick...)

Separator Image
In res/drawable/... filename.png





and the Dialog fragment:

public class MyDialogFragment
  extends DialogFragment {
 
 protected final String TAG = getClass().getSimpleName();
 
 private int titleId;
 
 private int bodyId;
 
 private int buttonId;
 
 public MyDialogFragment(int titleId, int bodyId, int buttonId) {
  super();
  this.titleId = titleId;
  this.bodyId = bodyId;
  this.buttonId = buttonId;
 }
 
 @Override
 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  
  View view = inflater.inflate(R.layout.alert_dialog_body, container);
  TextView title = (TextView) view.findViewById(R.id.Title);
  TextView body = (TextView) view.findViewById(R.id.AlertBody);
  Button button = (Button) view.findViewById(R.id.DialogButton);
  
  button.setOnClickListener(new OnClickListener() {
   
   @Override
   public void onClick(View v) {
    getDialog().dismiss();
   }
  });

  button.setText(buttonId);
  title.setText(titleId);
  body.setText(bodyId);
  return view;
 }
 
 @Override
 public void onAttach(android.app.Activity activity) {
  super.onAttach(activity);
 }
 
 @Override
 public void onCreate(android.os.Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setStyle(DialogFragment.STYLE_NO_FRAME, R.style.AlertDialogStyle);
 }
 
}

And Just in case you would actually want to use this:

MyDialogFragment dialogFragment = new MyDialogFragment(R.string.title,R.string.body,R.string.button_label);
dialogFragment .setCancelable(false);
dialogFragment .show(getSupportFragmentManager(), "Dialog Fragment");

You can check out the code here.

Leave your comments below... :)