WebView.loadUrl(url) does nothing - android

I recently wrote up a simple Twitter app for Android to learn the ropes of the Twitter API and OAuth.
The app's main activity simply asks for a username to follow. It then calls another activity which handles the OAuth & Twitter API calls. It redirects the user to an authorization page, which then returns to the app after the user finishes.
It used to work just fine, but now for some reason when I call webview.loadUrl(authorizationURL), NOTHING happens. I never changed anything that would affect the WebView stuff though... Here's my code:
#Override
public void onResume() {
super.onResume();
try {
if(weNeedCredentials()) {
obtainCredentials();
}
follow(mUsername);
} catch (OAuthException oae) {
// omitted
}
}
private boolean weNeedCredentials() {
// assume the method returns true
}
private void obtainCredentials() {
final Token requestToken = mOauthService.getRequestToken();
String authUrl = mOauthService.getAuthorizationUrl(requestToken);
// I verified that authUrl is the correct url (and != null)
final WebView oauthView = (WebView) findViewById(R.id.oauthview);
oauthView.setWebViewClient(new WebViewClient() {
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if(someCondition) {
oauthView.setVisibility(View.GONE);
doOtherStuff();
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
});
oauthView.getSettings().setJavaScriptEnabled(true);
oauthView.loadUrl(authUrl);
}
Here's my layout xml file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<WebView
android:id="#+id/oauthview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>
and also I included the proper Internet permissions in the Manifest.
A WebViewCoreThread is running for the life of the app, and a WebViewWorkerThread pops in later, but no WebView ever comes up (not even a white screen). The app never blocks either. It continues running as if the loadUrl() line were simply commented out.
I've tested on my phone (Droid X2) as well as an emulator, both with the same results. Any help would be greatly appreciated.
Thanks in advance!

It continues running as if the loadUrl() line were simply commented out.
It will always "continue running as if the loadUrl() line were simply commented out". loadUrl() is asynchronous and does not block.
Off the cuff, either:
weNeedCredentials() is returning false, or
follow() is replacing the UI, or
Twitter is doing a redirect, and someCondition is true, so you are making the WebView be GONE right away
there are issues with the URL that you are loading

Related

Get ANR dialog when open links from WebView in Chrome custom tabs. How do I debug this?

I want to use Chrome custom tabs to properly handle URLs out of my domain.
Here is the code
webView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request){
String url = request.getUrl().toString();
if(url.startWith("http://my.domain.name"))
return false;
else{
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor(getResources().getColor(R.color.colorPrimary));
builder.setStartAnimations(getActivity(), R.anim.slide_in_right, R.anim.slide_out_left);
builder.setExitAnimations(getActivity(), R.anim.slide_in_left, R.anim.slide_out_right);
Intent actionIntent = new Intent(
getApplicationContext(), ActionBroadcastReceiver.class);
actionIntent.setData(Uri.parse(url));
PendingIntent menuItemPendingIntent =
PendingIntent.getBroadcast(getApplicationContext(), 0, actionIntent, 0);
builder.addMenuItem(getString(R.string.action_share), menuItemPendingIntent);
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl(getActivity(), Uri.parse(url));
return true;
}
}
});
However when I click on the out-of-domain URLs, occasionally I get ANR dialog and UI of the app is freezing.
I try to debug the anr traces but I found no suspect thread.
The ANR traces file is quite long, so I posted it here:
https://gist.github.com/hoavt-54/42f1109c0619eed81e82a9a8d1128a6d
If you have any suggestion on how to debug the app or how to understand the trace file, I would really appreciate that.
The 5 seconds rule is not relevant - Even 1 second blocking the main thread will appear to the user as a stuck app.
The amount of work the system does in the background and in the foreground for your app all the time is huge, and it's all being done on the main thread. So when you don't return immediately from any callback (onXyz()) - the whole world is blocking waiting for you, nothing will be drawn, no touch event will arrive, etc.
So never do any network call on the main thread. It will always cause ANR at least on some devices some of the time, for example when their network is off.
Never do any of these on the main thread:
read/write to the local file system, including properties and database
heavy calculations
network
long running/busy loops of any kind
All the above will cause ANR's randomly for users.
I would suggest you take an advantage of StrictMode atleast in Debug mode. Use below code to get logs of any issue which slows down your App on main thread.
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
You can set different penalties -
penaltyLog() // to print log
penaltyDeath() // This will crash you App(so costly penalty)
penaltyDialog() // Show alert when something went lazy on Main thread
There is so much about https://developer.android.com/reference/android/os/StrictMode.html
As per your code you are overriding Webview url to move to custom chrome tabs.
As per my understanding of webview it gives a callback in chrome thread, so ideally you should run your code in activity using callback and handler with activity main thread.
#Override
public void retainOldWebView() {
if (mmtWebViewPop != null) {
mHandler.postDelayed(new Runnable() {
#Override
public void run() {
if (mmtWebViewPop != null) {
webViewFrameLayout.removeView(mmtWebViewPop);
mmtWebViewPop = null;
}
//TODO: write your own implementation here
}
}, 100);
}
}
To debug webview I would suggest to verify logs with chromium tags or while doing such experiments enable remote debugging
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
I as a developer would not want to mix up the implementation of both in one single activity, even if I want to I would rather create separate fragments for customChromeTabs and WebView and replace fragments whenever necessary. Because both of them work as a separate entity and shouldn't mix up the implementation for both.
Please share the code or implement something like below :-
WebViewActivity intercept requests and if url pattern doesn't match give a callback and start new ChromeTabActivity with url passed in intent bundle.
ChromeTabActivity where your code for customChromeTab should be present and work independently of each other and finish to resume webview from where you left of.
But I never used ChromeTabActivity for the similar cases, I have a use cases where fb and google login weren't working for me so I opened the same in another webview in frameLayout, which is working fine for my usecases. As soon as the pageload is completed I call this function.
if (null != mmtWebView) {
mmtWebView.setWebChromeClient(new WebChromeClientImpl(getApplicationContext()));
}
class WebChromeClientImpl extends WebChromeClient {
private final Context appContext;
public WebChromeClientImpl(Context appContext) {
this.appContext = appContext;
}
#Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
mmtWebViewPop = new WebView(view.getContext());
mmtWebViewPop.setVerticalScrollBarEnabled(false);
mmtWebViewPop.setHorizontalScrollBarEnabled(false);
mmtWebViewPop.setWebViewClient(new WebViewClientImpl(appContext, null));
mmtWebViewPop.getSettings().setJavaScriptEnabled(true);
mmtWebViewPop.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
webViewFrameLayout.addView(mmtWebViewPop);
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(mmtWebViewPop);
resultMsg.sendToTarget();
return true;
}
#Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
return super.onConsoleMessage(consoleMessage);
}
#Override
public void onCloseWindow(WebView window) {
super.onCloseWindow(window);
retainOldWebView();
}
}

WebView not working consistently

Edit: Strangely works when duplicating the loadUrl() line
I'm working with WebViews to gather data from a webpage. Sometimes, the webview just does nothing. I tried many proposes, but actually, none of them works....
Strange: webview.loadUrl("url...") doesn't work, but as soon as I call it twice, it works...
checkLogin()
void checkLogin(final Context context) {
Log.d("checkLogin()", "Begin of checkLogin()");
WebView webview = returnNewWebView(context, false);
webview.setWebViewClient(new WebViewClient(){
#Override
public void onPageFinished(WebView view, String url) {
Log.d("url", url);
// GOOGLE.COM ACTUALLY WORKS!
if(url.equals("https://www.google.com/"))
return;
Log.d("checkLogin()", "Loading of checkpage finished");
if(url.contains("loginto.php")) {
Log.d("checkLogin()", "User is logged out");
OnLoginCheckListener.onLoginCheckLoggedOut();
} else {
Log.d("checkLogin()", "User is logged in");
OnLoginCheckListener.onLoginCheckLoggedIn();
}
}
});
String id = sharedPref.getString("id", null);
String transid = sharedPref.getString("transid", null);
if(TextUtils.isEmpty("id") || TextUtils.isEmpty("transid")) {
Log.d("checkLogin()", "No login found, starting LoginActivity()");
Intent myIntent = new Intent(context, LoginActivity.class);
context.startActivity(myIntent);
} else {
Log.d("checkLogin()", "Probably logged in, checking by loading startpage");
webview.loadUrl("https://www.google.com");
// google actually works -.-
webview.loadUrl("https://my.login-page.com/index.php?pageid=1&id=" + id + "&transid=" + transid);
}
}
returnNewWebView()
private WebView returnNewWebView(Context context, Boolean JSInterface) {
// Prepare a webview
WebView WebView = new WebView(context);
if(JSInterface) {
WebView.getSettings().setJavaScriptEnabled(true);
WebView.addJavascriptInterface(new JavaScriptInterface(), "AndroidInterface");
}
// We don't need images for data scraping...
WebView.getSettings().setLoadsImagesAutomatically(false);
WebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
// Allow cookies
CookieManager.getInstance().acceptCookie();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().acceptThirdPartyCookies(WebView);
}
// Prevent webview not loading
// or try it...
WebView.clearCache(true);
WebView.destroyDrawingCache();
return WebView;
}
As you see, I already tried some stuff in order to prevent it but still, it doesn't work. In that cases, as soon I'm going to put the loadUrl() / postUrl()twice, it works.
What can I do that it works in every single case?
I would appreciate any tip!
Thank you very much in advance
logcat (non-filtered)
I actually see this message then..
03-09 07:36:19.061 4111-24251/? E/ctxmgr: [ProducerActiveIntervalImpl]closeActiveInterval: Error: ongoing, trying to close
and this one:
03-09 07:36:28.864 29862-3249/? E/accs.GcmPush: initializeApp occur error!
java.lang.IllegalStateException: FirebaseApp name [DEFAULT] already exists!
at iv.a(Unknown Source)
at com.google.firebase.FirebaseApp.a(Unknown Source)
at com.google.firebase.FirebaseApp.a(Unknown Source)
at org.android.agoo.gcm.GcmRegister$1.run(GcmRegister.java:32)
and this one too:
E/libEGL: validate_display:99 error 3008 (EGL_BAD_DISPLAY)
But I can't tell you if they are fired by the app....
I have gone through the full code for so long and pointed out this :
You are using this -
webview.loadUrl("https://www.google.com");
// google actually works -.-
webview.loadUrl("https://my.login-page.com/index.php?pageid=1&id=" + id + "&transid=" + transid);
in your checkLogin() method which is simply wrong as because one webview can not display the two urls at same time (as you mentioned it you want to achieve that).
Now according to your code the loading of the first url is overridden by the second url.
In order to achieve the loading of two urls in same webview try to implement this using :
Threads or some sessionTimeout methods.
OR
You can achieve this by using okhttp package.
Hope this helps!
onPageFinished is called when the webpage finished loading, but not after all the javascript on the page finished running. Depending on what the page is doing and what you are trying to achieve, it might be that your code is sometimes being executed before some essential javascript is executed on that page.
It's not a pretty solution but try adding some delay (like a Handler's postDelayed() or SystemClock.sleep() - but never on the UI thread!) to your code to see if that is indeed your problem.

Android SSO Okta Integration Example

Don't know a lot about Okta and Android. Does anyone know of a good tutorial which shows how to wire an android application into the Okta framework. Or do I implement a SAML SSO implementation and then Okta is associated to that? Any code examples appreciated - especially one showing Android implementation of generic SSO, if such a thing exists.
OK, alot of ground to cover here and some of the work I did not do. But the basic idea is that on the server side (we use .Net) we created a SAML communication layer using "kentor". I did not work with this, but idea is software communicating the the client's Identity Provider (IDP) for SSO (Okta for example). The IDP client usually has to provide XML meta data which has security info and ultimately a URL, and you provide them with your SSO xml meta data (sorry, I did not work on that part!).
Basically from there it is pretty straight forward on the Android side. The bottom line is that the above interaction results in a url that the SSO client provides that you will use on the Android side to create a webview, which will allow them to enter their login information for verification.
We have the URL hardcoded since we create a white label product specifically for the customer (you will see that as Constants.SINGLE_SIGNON_URL below) but there is nothing that stops you from passing the URL back after a customer passes a Organization Code for SSO in (we are working on that now). In other words you store the URL or generate the URL based on which customer and then return that when the device passes you an Organizatioal Code. The URL is actually to your server, which redirects to the IDP (Okta) login page for SSO. This is because the response from OKTA needs to go to your server where ultimately it will be sent back by redirect to your webview. We used the cookies to then store the resulting username to allow the normal login process. Probably a lot of different ways to do that, and Okta even provides a native mobile device capability, but the customer has to support that.
Here is a diagram that hopefully will spell out some high level pieces of this:
The code only covers 1), 2) and 5) in the above diagram. 1) Is pretty obvious the call to the WebView. 2) is really the call to the Constants.SINGLE_SIGNON_URLwhich hits your server, which should redirect to the IDP page. When the user logs in there, it get sent back to your Service (SP) and is redirected back to your WebView. Again, we stored something in the cookie to pull out to continue our normal login.
One key is to realize that the WebView's shouldOverrideUrlLoading() is called several times. Ignore all of those except the one that sends back your server's URL, at which point you pull out the data you need (in our case the login info that the server had verified). This is seen in the call GlobalState.getInstance().currentUserName = getCookieValue("_username" ,cookies);
Probably not explaining this very well (and it has been a month or so!). Here is a sample of the SSOActivity where most of the work is done:
public class SSOActivity extends Activity {
WebView webView;
private Button mCancel;
private Button mReset;
/**
* Grabs the specified variables out of the list of cookies
*
* #param fieldName
* #param cookies
* #return
*/
public String getCookieValue(String fieldName, final String cookies){
String CookieValue = null;
String[] cookiessplit = cookies.split(";");
for (String str : cookiessplit ) {
if(str.contains(fieldName)) {
String[] value=str.split("=");
CookieValue = value[1];
break;
}
}
return CookieValue;
}
public void clearCookies() {
try {
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
}
catch (Exception ex)
{
Utilities.logException(ex);
Utilities.logError("SSOActivity", "clearCookies() : " + ex.getMessage() );
}
}
/**
* Cancels the SSO request in Webview
*
* #param view
*/
public void cancelSSOClick (View view) {
Utilities.logInfo("cancelSSOClick", "Cancel SSO click");
setResult(Activity.RESULT_CANCELED, null);
SSOActivity.this.finish();
}
/**
* Resets and deletes cookies and SSOUrl if one exists
*
* #param view
*/
public void resetSSOClick (View view) {
Utilities.logInfo("resetSSOClick", "Cancel SSO click");
setResult(Activity.RESULT_CANCELED, null);
clearCookies();
SSOActivity.this.finish();
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(Activity.RESULT_OK, null);
// Setup the web view. It will redirect to SSO site for login
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_sso);
mCancel = (Button)findViewById(R.id.cancelSSO);
mCancel.setTextColor(Color.WHITE);
mReset = (Button)findViewById(R.id.resetSSO);
mReset.setTextColor(Color.WHITE);
webView = (WebView) findViewById(R.id.ssoViewer);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setSupportZoom(false);
webView.setWebViewClient(new WebViewClient() {
#Override
public boolean shouldOverrideUrlLoading (WebView view, String url) {
try {
// If coming from our system, then we need to check the cookie for username password, for
// some SSO this might be different than the base url. Check for both
if (url.equals(Constants.getBaseUrl()) || url.equals(Constants.SSO_RETURN_URL)) {
CookieManager cookieManager = CookieManager.getInstance();
final String cookies = cookieManager.getCookie(url);
GlobalState.getInstance().currentUserName = getCookieValue("_username" ,cookies);
SSOActivity.this.finish();
return true;
}
}
catch (Exception ex) {
GlobalState.getInstance().currentUserName = "";
GlobalState.getInstance().currentPassword = "";
setResult(Activity.RESULT_CANCELED, null);
SSOActivity.this.finish();
}
return false;
}
});
try {
webView.loadUrl(Constants.SINGLE_SIGNON_URL);
}
catch (Exception ex) {
Utilities.logException(ex);
Utilities.logError("SSOActivity", "onCreate(), webView.loadUrl(ssoUrl) : " + ex.getMessage() );
}
}
}
Here is an example of the XML supporting the Activity:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/ssoViewerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/button_layout"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:gravity="center|bottom"
android:layout_alignParentBottom="true">
<Button
android:id="#+id/cancelSSO"
android:layout_marginTop="16dp"
android:layout_width="125dp"
android:layout_height="55dp"
android:layout_margin="5dp"
android:onClick="cancelSSOClick"
android:text="Cancel Login"
android:background="#drawable/button_login" />
<Button
android:id="#+id/resetSSO"
android:layout_marginTop="16dp"
android:layout_width="125dp"
android:layout_height="55dp"
android:layout_margin="5dp"
android:onClick="resetSSOClick"
android:text="Reset SSO"
android:background="#drawable/button_login"/>
</LinearLayout>
<WebView
android:id="#+id/ssoViewer"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_above="#id/button_layout" />
</RelativeLayout>
Calling it else where in the code looks like this:
Intent viewIntent = new Intent(getActivity(), SSOActivity.class);
(getActivity()).startActivityForResult(viewIntent, Constants.SINGLE_SIGN_ON);
And finally what you should see:
Hope this helps!

Android onOverrideUrlLoading based on javascript return value

I am working on a hybrid app and trying to return true or false in onOverrideUrlLoading of webview based on returned value from javascript function executed in webview
Example code I have so far.
//Have a boolean variable isExternalDomain;
//Added JavascriptInterface webView.addJavascriptInterface(this, "android");
public boolean onOverrideUrlLoading(final String url) {
WebView.loadUrl("javascript:android.onData('true')");
//I Tried inserting sleep, delay EG: Thread.sleep(200);
//I see the delay but still javascript executes last.
if(isExternalDomain) {
return true;
} else {
return false;
}
}
#JavascriptInterface public void onData(String value)
{
isExternalDomain = true;
}
So the Issue I am having is javascript execution happens after onOverrideUrlLoading completed executing all lines with isExternalDomain as false. I would like to have onOverrideUrlLoading returning true or false based on javascript returned value.
Unfortunately, running JavaScript code from inside onOverrideUrlLoading() isn't possible. You must return from onOverrideUrlLoading() before WebView can do anything else. When you call WebView.loadUrl() from inside onOverrideUrlLoading(), what really happens is an asynchronous task gets posted onto the WebView's message loop. It only gets processed after you leave onOverrideUrlLoading(). Thus, no amount of delay will make WebView to process your request while your code is inside onOverrideUrlLoading().
If you want to prevent navigation from happening based on the decision made by JavaScript code, it's more natural to do that on the JavaScript side by using window.onbeforeunload event handler. If you return non-null value from it, an attempt to navigate away by clicking a link will be cancelled.
Below is a sample of JavaScript code:
window.onbeforeunload = function() {
if (navigationDisallowed()) {
return true; // Prevent navigating away from the page.
} else {
return null; // Allow navigating away.
}
}

Load websites in offline mode

I want to load websites from cache when internet is not connected. Here's how I setup the webview in onCreate:
browse.getSettings().setJavaScriptEnabled(true);
browse.getSettings().setPluginsEnabled(true);
browse.getSettings().setLoadWithOverviewMode(true);
browse.getSettings().setUseWideViewPort(true);
browse.getSettings().setBuiltInZoomControls(true);
browse.getSettings().setSupportZoom(true);
browse.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);
browse.setScrollbarFadingEnabled(true);
browse.getSettings().setLoadsImagesAutomatically(true);
browse.getSettings().setAppCacheMaxSize(1024 * 1024 * 100);
browse.getSettings().setAppCacheEnabled(true);
if (cm.getActiveNetworkInfo().isConnected())
browse.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
else
browse.getSettings().setCacheMode(
WebSettings.LOAD_CACHE_ELSE_NETWORK);
browse.setWebViewClient(new WebViewClient() {
#Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
// TODO Auto-generated method stub
super.onPageStarted(view, url, favicon);
t.setVisibility(TextView.VISIBLE);
}
#Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
t.setVisibility(TextView.GONE);
}
});
try {
browse.loadUrl(address);
} catch (Exception e) {
e.printStackTrace();
}
Here's the tutorial I'm following and I'm trying to load the previously visited webpages even if the browser is restarted. Doesn't work! Please help me.
P.S: I have searched all the related codes on SO, none of them works.
Declare the following permission in your manifest file:
android.permission.ACCESS_NETWORK_STATE
I think you are missing the call to WebSettings.setAppCacheEnabled(true) :)
Also, I think it's probably better to check the connectivity and call setCacheMode() before you call loadUrl() instead of from within your onPageStarted() method (seems like it would be too late).
Lastly, I am not sure whether your override of getCacheDir() is necessary, since I believe you are simply calling the default implementation anyway in your override.
UPDATE:
After looking at the updated code in the question, I can confirm that I have similar code that worked fine. The only notable difference is I have the following code when checking for connectivity:
final NetworkInfo activeNetworkInfo = cm.getActiveNetworkInfo();
if (activeNetworkInfo != null && activeNetworkInfo.isConnected()) {
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}
compared to your version:
if (cm.getActiveNetworkInfo().isConnected())
browse.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
else
browse.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
I tested by running in the emulator and toggling airplane mode on and off.
I did however read the tutorial that you linked more closely (and I understand now why you override getCacheDir() in your original code) and am wondering if maybe you are still calling WebView.clearCache(true) in the onDestroy() of your Activity. If you still have that part from the tutorial unmodified, it will definitely cause the cache appear not to be working :)
Have you tried nanoHttpd webserver, it is simple solution to get access webpages in sdcard or local memory.. all you want to do is give path to page as ,http://localhost:8081/[your cache directry path to offline webpage] in webview
https://gist.github.com/1893396
I tried this on pages in sdcard and phone memory, hope this will work for you

Categories

Resources