Android App Development - Human Element

Silly faces and top hats make for good app development.

This is part 2 of a series on Mobile app development using Webview. Want to read more? Here ya go:

Android App Development: Introduction

In the first part of this article, we discussed the design for a pretty standard app. In this article, we will discuss how you can implement an easy Android App using Webview for Development. Hopefully, this article will help you with your own mobile app.

  • What this article covers: Basic functionality that most all WebView apps may implement around the WebView. This is the boilerplate code written in a device’s native language.
  • Not covered: Anything related to your content, design, or concept. Anything that appears inside the WebView, coded with Javascript, HTML, and CSS. That’s up to you.

This article will focus on Java code for an Android WebView application.

We will create an app that uses a WebView to “frame” in the Human Element website. Remember, this is just a “boiler-plate” template for any app; you could substitute the Human Element url with ANY url to create an entirely different app. For example, if your app needs to present internal page content instead of any old url out there on the internet, then YES, you can do that instead (I discuss where your web files can live in the first part of this article). The example in this article uses centralized hosting, so all the web files live at human-element.com.

Giving away secrets?

In a golden age of owning “Intellectual Property,” why would we give away “secrets?” It took me a long time to put together this code and get it to work properly, but now, you can copy and paste it in just a few minutes. What’s in it for me?

There will always be the next barrier that must be overcome, ALWAYS. But when solutions are shared, we all progress FASTER because we can all benefit from someone else’s progress. Teach a man how to fish and one day he may teach you how to fish better… or discover the next, new, better alternative. So by helping you, essentially I’m helping myself.

This philosophy probably seems moronic when you’re reclining in your personal jet air-liner, miles above reality. But whether you like it or not, eventually, you have to depend on someone else. You can’t help but live in the same world with everyone. I prefer to live with educated, happy people like me. I’ll share these “secrets” so maybe we can progress together. That’s up to you. But here’s one of those opportunities.

On this page

Complete code examples (5 files)

And now, the main attraction. If you are trying to build your own app, then this is what you came here for. Who cares about this paragraph, just give me the code samples. I know how it is. That’s why I included COMPLETE examples, instead of just bits and pieces.

If you copy the following code into your own application, then you should have a working app (so you can focus more on building your content inside the WebView). I tested this code on a real android phone, and it works… for me, at least.

MainActivity.java

This is where most of your code logic will go. This is the only Java file in this example. The other files are all XML configuration files:

Events

Standard Android events: You can write logic into these events to override what happens when they get triggered.

  • onCreate Entry point for the application
  • onBackPressed Fires when the user hits the back button on their device
  • onConfigurationChanged Fires when the screen orientation changes, eg: vertical to horizontal…
  • onSaveInstanceState Fires before the current activity is placed in a background state
  • onCreateOptionsMenu Fires when the options menu is created
  • onOptionsItemSelected Fires when the user presses one of the menu options

Methods

Here are some methods that I defined for this boiler-plate app:

  • isFileUrl(String url, String extension) Returns true if url ends with extension. Otherwise, returns false
  • handleExternalDeviceActivity(String url) Opens the url OUTSIDE of the app, in a separate activity. For example, inside a separate browser app.
  • hasInternetConnection(Context context) Returns true if an internet connection is available
  • noInternetMessage() Displays an alert popup to tell the user there is no internet connection
  • buildView(final Bundle savedInstanceState) Setup the main WebView to display the primary app url
  • buildExternalView(String externalUrl) Setup the external WebView to display some url, outside of the app’s domain
  • closeExternalView() Close the external WebView so the user sees the primary WebView again (return to the main app)

Fields

I defined some “module-level” fields. “module-level” is a an old text book term that means the variable can be accessed anywhere within the current class. That “m” before the field name stands for “module-level” and helps me keep track of these types of variables throughout the project.

  • mViewUrl The primary url where your main app’s web files are located
  • mViewQs Any query string parameters you want to attach to your mViewUrl. I set “d=android” so that I can send the device type to the web files. For example, “d=android” or “d=ios”.
  • mNochaceQs When the app starts, a “nocache” value is appended to the mViewUrl. This helps prevent the app content from being cached if dynamic content changes. I have the “nocache” value change every hour.
  • mWebView The primary WebView object
  • mExternalView The secondary (external) WebView object
  • mActionBar The action bar object. The action bar only appears when the external WebView is open. Then main WebView doesn’t have a visible action bar
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
package com.example.helloworld;
import java.util.Calendar;
import java.util.Date;
import android.support.v7.app.ActionBarActivity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.ColorDrawable;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebBackForwardList;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class MainActivity extends ActionBarActivity {
    //fields
    private final String mViewUrl = "https://www.human-element.com";
    private final String mViewQs = "?d=android";
    private final String mNochaceQs = "&nocache=";
    
    protected WebView mWebView;
    protected WebView mExternalView;
    protected android.support.v7.app.ActionBar mActionBar;
    //properties
    public String getViewUrl(){return mViewUrl;}
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //hide the action bar at the top of the screen
        mActionBar = getSupportActionBar();
        mActionBar.hide();
        mActionBar.setHomeButtonEnabled(true);
        //define action bar color
        mActionBar.setBackgroundDrawable(new ColorDrawable(0xff2d5b8e)); //2d5b8e
        //define action bar icon
        mActionBar.setIcon(R.drawable.ic_launcher);
        //set the main view
        setContentView(R.layout.activity_main);
        //build the main view
        buildView(savedInstanceState);
    }
    //find out if this URL is a link to a file (eg: PDF) by checking the extension at the end of the url
    private boolean isFileUrl(String url, String extension){
        boolean isFile=false;
        url=url.toLowerCase();extension=extension.toLowerCase();
        //if contains extension
        if(url.contains(extension)){
            //if also contains ? query string
            if(url.contains("?")){
                //strip off query string
                url=url.substring(0,url.indexOf("?"));
            }
            //if ends in file extension
            if(url.lastIndexOf(extension)==url.length()-extension.length()){
                //then yep. This is a file
                isFile=true;
            }
        }
        return isFile;
    }
    //pass any url to this function to auto-handle certain activities (eg: opening PDF documents or making phone calls)
    protected void handleExternalDeviceActivity(String url){
        //pass this special URL to an external activity (outside of the app)
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        startActivity(intent);
    }
    //verify that an internet connection is available
    protected boolean hasInternetConnection(Context context) {
        boolean isConnected=false;
        //if connectivity object is available
        ConnectivityManager con_manager=(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if (con_manager.getActiveNetworkInfo() != null){
            //if network is available
            if(con_manager.getActiveNetworkInfo().isAvailable()){
                //if connected
                if(con_manager.getActiveNetworkInfo().isConnected()){
                    //yep... there is connectivity
                    isConnected=true;
                }
            }
        }
        return isConnected;
    }
    //show something if there is no internet connectivity
    protected void noInternetMessage(){
        //build an alert box
        AlertDialog.Builder alertbox = new AlertDialog.Builder(this);
        // Set the message to display
        alertbox.setMessage(R.string.err_connection_summary);
        // Add a neutral button to the alert box and assign a click listener
        alertbox.setNeutralButton(R.string.btn_ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface arg0, int arg1) {
                //quit the app
                finish();
            }
        });
        alertbox.setOnCancelListener(new OnCancelListener() {
            @Override
            public void onCancel(DialogInterface arg0) {
                //quit the app
                finish();
            }
        });
         // show the alert box
        alertbox.show();
    }
    protected void buildView(final Bundle savedInstanceState){
        //if there is still internet connectivity
        if(hasInternetConnection(this)){
            //get the webview object
            mWebView = ((WebView)findViewById(R.id.webview));
            //if web view is NOT already initialized (don't re-init just because orientation changed)
            if(savedInstanceState==null){
                //ALLOW JAVASCRIPT TO RUN INSIDE THE WEBVIEW
                //==========================================
                WebSettings webSettings = mWebView.getSettings();
                webSettings.setJavaScriptEnabled(true);
                //INIT LOADING INDICATOR OBJECT
                //=============================
                final ProgressDialog pd = ProgressDialog.show(this, "", "Loading...",true);
                //KEY WEBVIEW EVENTS (OVERRIDES)
                //==============================
                mWebView.setWebViewClient(new WebViewClient() {
                    /*@Override
                    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
                    }*/
                    @Override
                    public boolean shouldOverrideUrlLoading(WebView view, String url) {
                        //override webview url loading (distinguish between internal and external urls)
                        boolean ret = true;
                        //if there is still internet connectivity
                        if(hasInternetConnection(view.getContext())){
                            //if the new url is on the same webview page
                            if (url.indexOf(mViewUrl)==0) {
                                //load the new page inside the webview NOT another browser activity
                                ret = false;
                            }else{
                                //the new url is NOT on a webview page...
                                
                                //if the url ends in .pdf... (PDF files don't open inside webviews)
                                if(isFileUrl(url,".pdf")){
                                    //launch an external Activity that handles URLs, such as a browser application
                                    handleExternalDeviceActivity(url);
                                }else{
                                    //not a PDF url...
                                    
                                    //if this is a telephone number url (starts with "tel:")
                                    if(url.indexOf("tel:")==0){
                                        //launch an external Activity that handles telephone numbers to make a call
                                        handleExternalDeviceActivity(url);
                                    }else{
                                        //if this is a mailto link
                                        if(url.indexOf("mailto:")==0){
                                            //launch an external Activity that handles email mailto links
                                            handleExternalDeviceActivity(url);
                                        }else{
                                            //try to load the external url in the alternate webview
                                            buildExternalView(url);
                                        }
                                    }
                                }
                            }
                        }else{
                            //no internet connectivity...
                            noInternetMessage();
                        }
                        return ret;
                    }
                    @Override
                    public void onPageFinished(WebView view, String url) {
                        //hide the loading indicator when the webview is fully loaded
                        if(pd.isShowing()&&pd!=null){
                            pd.dismiss();
                            //clear the webview cache
                            super.onPageFinished(view, url);
                            view.clearCache(true);
                        }
                    }
                });
                //LOAD THE APP PAGE INTO THE WEB VIEW
                //===================================
                Calendar now = Calendar.getInstance(); 
                Date date = new Date();
                now.setTime(date);
                String nocache = now.get(Calendar.YEAR)  + "_" + now.get(Calendar.MONTH) + "_" + now.get(Calendar.DAY_OF_MONTH) + "_"
                        + now.get(Calendar.HOUR_OF_DAY) + "_" + now.get(Calendar.MINUTE) + "_" + now.get(Calendar.SECOND);
                mWebView.loadUrl(mViewUrl + mViewQs + mNochaceQs + nocache);
            }else{
                //RESTORE THE INSTANCE STATE
                //==========================
                mWebView.restoreState(savedInstanceState);
            }
        }else{
            //no internet connection...
            noInternetMessage();
        }
    }
    protected void buildExternalView(String externalUrl){
        //if web view is NOT already initialized (don't re-init just because orientation changed)
        if(mExternalView==null){
            //get the webview object
            mExternalView = ((WebView)findViewById(R.id.externalView));
            //ALLOW JAVASCRIPT TO RUN INSIDE THE WEBVIEW
            //==========================================
            WebSettings webSettings = mExternalView.getSettings();
            webSettings.setJavaScriptEnabled(true);
            //INIT LOADING INDICATOR OBJECT
            //=============================
            final ProgressDialog pd = ProgressDialog.show(this, "", "Loading More...",true);
            //KEY WEBVIEW EVENTS (OVERRIDES)
            //==============================
            mExternalView.setWebViewClient(new WebViewClient() {
                @Override
                public boolean shouldOverrideUrlLoading(WebView view, String url) {
                    //override webview url loading (distinguish between internal and external urls)
                    boolean ret = true;
                    //if the url ends in .pdf... (PDF files don't open inside webviews)
                    if(isFileUrl(url,".pdf")){
                        //launch an external Activity that handles URLs, such as a browser application
                        handleExternalDeviceActivity(url);
                    }else{
                        
                        //if this is a telephone number url (starts with "tel:")
                        if(url.indexOf("tel:")==0){
                            //launch an external Activity that handles telephone numbers to make a call
                            handleExternalDeviceActivity(url);
                        }else{
                            //if this is a mailto link
                            if(url.indexOf("mailto:")==0){
                                //launch an external Activity that handles email mailto links
                                handleExternalDeviceActivity(url);
                            }else{
                                //just handle this url inside this same web view
                                ret=false;
                            }
                        }
                    }
                    return ret;
                }
                @Override
                public void onPageFinished(WebView view, String url) {
                    //hide the loading indicator when the webview is fully loaded
                    if(pd.isShowing()&&pd!=null){
                        pd.dismiss();
                        //clear the webview cache
                        super.onPageFinished(view, url);
                        view.clearCache(true);
                        //try to make this webview visible
                        view.setVisibility(View.VISIBLE);
                        view.bringToFront();
                        //show the action bar at the top of the screen
                        mActionBar.show();
                    }
                    //set the new page title
                    setTitle(view.getTitle());
                }
            });
        }else{
            //mExternalView already created...
        }
        //LOAD THE EXTERNAL PAGE INTO THE WEB VIEW
        //========================================
        mExternalView.loadUrl(externalUrl);
    }
    //close the external webview and go back to the main app webview
    private void closeExternalView(){
        //show the main webview
        mWebView.bringToFront();
        //clear browse history from external web view
        mExternalView.loadUrl("about:blank");
        mExternalView.clearHistory();
        mExternalView.clearCache(true);
        //hide the externalView and the action bar
        mExternalView.setVisibility(View.GONE);
        mExternalView = null;
        mActionBar.hide();
    }
    @Override
    public void onBackPressed()
    {
        //if NOT in the external view mode
        if(mExternalView==null){
            if(mWebView.canGoBack()){
                //the back button should go to previous webview page instead of close the app
                mWebView.goBack();
            }
        }else{
            //in the external view mode...
            
            //if there is a previous external page
            if(mExternalView.canGoBack()){
                //get the last url
                WebBackForwardList backForwardList = mExternalView.copyBackForwardList();
                String historyUrl = backForwardList.getItemAtIndex(backForwardList.getCurrentIndex()-1).getUrl();
                //if NOT going back to about:blank (about:blank was inserted into the list at the point at which the external view was last closed)
                if(!historyUrl.equals("about:blank")){
                    //the back button should go to previous page instead of doing nothing
                    mExternalView.goBack();
                }else{
                    //instead of going back to about:blank, go back to the main view
                    closeExternalView();
                }
            }else{
                //there is no previous external page...
                
                //close the external view and return to the main app view
                closeExternalView();
            }
        }
    }
    @Override
    public void onConfigurationChanged(Configuration newConfig){
        super.onConfigurationChanged(newConfig);
    }
    @Override
    protected void onSaveInstanceState(Bundle outState){
      //save the state of the WebView
      mWebView.saveState(outState);
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        //handle action bar event options
        switch (item.getItemId())
        {       
            //if the main app icon was clicked in the action bar (while viewing an external webview page)
            case android.R.id.home:           
                //switch back to the main view and exit the external view...
                closeExternalView();
                return true
            //if the drop down "Return" item was clicked
            case R.id.action_returnapp:           
                //switch back to the main view and exit the external view...
                closeExternalView();
                return true;
            //if the user clicked "Open in Browser"
            case R.id.action_openinbrowser: 
                //open the external view in a real browser outside of the app
                handleExternalDeviceActivity(mExternalView.getUrl());
                //close the external view and return to the main app view
                closeExternalView();
                return true;
            default:           
                return super.onOptionsItemSelected(item);   
        }
    }
}

AndroidManifest.xml

This is where you define your app’s key package identifier. I have the com.example.helloworld package name. But obviously, you will have something different.

Your app may be on its first release. If so, then your versionCode will probably be “1” and your versionName could be something like “alpha“, “lolly pop“, “1“, or some other name that you choose.

Also, you must tell the applications what permissions get used. At minimum, you will need two:

  • INTERNET If you are accessing ANY external urls, then you need to enable this permission.
  • ACCESS_NETWORK_STATE If you need to detect if the device HAS access to internet, then you also need this permission. My hasInternetConnection method depends on being able to detect connectivity.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="utf-8"?>
    package="com.example.helloworld"
    android:versionCode="8"
    android:versionName="1.7" >
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="21" />
    
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:configChanges="orientation|screenSize" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

res/menu/main.xml

Two menu items are defined in this file. These menu items appear in the mActionBar.

Remember, this action bar is only visible IF the external web view is open.

The purpose of these two menu items is:

  • Return Go back to the main app and hide the external web view.
  • Open in Browser Open the current external view into a browser (outside of the Hello World app).

You can see how the app detects when these menu items get pressed AND carries out their actions: Check out the onOptionsItemSelected event, that handles option item presses in MainActivity.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.helloworld.MainActivity" >
     
    <item
        android:id="@+id/action_openinbrowser"
        android:orderInCategory="100"
        android:title="@string/action_openinbrowser"
        app:showAsAction="never" /> 
 
    <item
        android:id="@+id/action_returnapp"
        android:orderInCategory="200"
        android:title="@string/action_returnapp"
        app:showAsAction="never" />
</menu>

res/values/strings.xml

Are you familiar with the concept of localization? This is a strategy for defining all of your text values in one file so that this file can be substituted with alternate language files. Android localizes some of its string values inside a file aptly named, strings.xml. You can see string definitions for some of my app’s strings here.

01
02
03
04
05
06
07
08
09
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Hello World</string>
    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>
    <string name="action_returnapp">Return</string>
    <string name="btn_ok">Ok</string>
    <string name="action_openinbrowser">Open in Browser</string>
    <string name="err_connection_summary">Having trouble reaching Hello World. Please check your internet connection then restart the app.</string>
</resources>

res/layout/activity_main.xml

Here, I define both of my WebViews, primary and external. I also set the dimensions of the WebViews. I want them both to fill the screen ENTIRELY, so I set them both to fill_parent. I also define their ID’s which I use to grab them inside the MainActivity.java file.

Note: If you work inside Eclipse, you may see the xmlns:android attribute cause XML syntax errors. But you can easily remove the error if you you go to Project > Clean in Eclipse.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.helloworld.MainActivity" >
    
    <WebView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/externalView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
    />
    <WebView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/webview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
    />
</RelativeLayout>

That’s all for now. Hopefully, this article was a good launching point for your Android app. Stay tuned for a similar article for iOS apps.

< Back to Menu of Code