Complete example of using a WebView

Fragment with a WebView and fall-back UI in case there’s no active data connection.

Note: if you plan to open external websites, we suggest to use the Chrome Custom Tabs instead. The snippets below tie in more with your own app, and are perfect for, e.g. showing terms & conditions and privacy policies.

To open a url in a custom tab, use the Kotlin function here.

Features:

  • Compatible with Android SDK 9 and up.
  • Sets the UserAgent string (e.g: AppName/1.0-5 (Android 6.0-23) - LGE Nexus 5).
  • Configures caching and localStorage.
  • Adds a progressbar to the top of the screen.
  • Shows a placeholder UI when there is no connection, with buttons to launch the settings or to try again.

Usage:

  • Copy both files (WebFragment.java and fragment_web.xml) to the right folders.
  • Simply place the Fragment in the desired container, or use the FragmentActivity.
  • A url is expected in R.string.web_url.
  • Disables Javascript by default, turn on by modifying the ENABLE_JAVASCRIPT constant.
  • Requires adding several strings and icons to the resources – just follow the errors.

WebFragment.java

package com.pixplicity.example.ui;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

import com.pixplicity.example.R;

/**
 * Fragment with a WebView and fall-back UI in case there's no active data connection.
 * A url is expected in R.string.web_url.
 * <p/>
 * Compatible with Android SDK 9 and up.
 */
public class WebFragment extends Fragment {

    // Only set to true if you know it will be safe
    private static final boolean ENABLE_JAVASCRIPT = false;
    // Max app cache size. Ignored on SDK 18+
    private static final long APP_CACHE_SIZE = 5 * 1024 * 1024;
    // Set to false to open all links in the webview as well.
    // Otherwise only links within the same domain as the starting url will open in the webview.
    private static final boolean OPEN_LINKS_IN_BROWSER = true;
    private static final boolean ALLOW_LOCAL_STORAGE = true;
    /**
     * User-Agent string. E.g. "AppName/1.0-5 (Android 6.0-23) - LGE Nexus 5"
     */
    private static final String USER_AGENT = "%1$s/%2$s-%3$s (Android %4$s-%5$s) - %6$s %7$s";

    private static final String TAG = WebFragment.class.getSimpleName();

    private boolean mIsWebViewAvailable;
    private boolean mHasFailed = false;
    private String mWebUrl;
    private String mWebDomain;
    private WebView mWebView;
    private ViewGroup mRootView;
    private ProgressBar mProgress;

    public WebFragment() {
    }

    public static WebFragment getInstance() {
        return new WebFragment();
    }

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Inflate
        mRootView = (ViewGroup) inflater.inflate(
                R.layout.fragment_web, container, false);
        mWebView = (WebView) mRootView.findViewById(R.id.webview);
        mProgress = (ProgressBar) mRootView.findViewById(R.id.pb_web);
        mWebUrl = getString(R.string.web_url);
        mWebDomain = getDomain(mWebUrl);

        // Retry and Settings buttons
        mRootView.findViewById(R.id.bt_refresh).setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                mHasFailed = false;
                mWebView.loadUrl(mWebUrl);
            }
        });
        mRootView.findViewById(R.id.bt_settings).setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                final Intent intent = new Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS);
                startActivity(intent);
            }
        });

        // Allow local storage if needed
        if (ALLOW_LOCAL_STORAGE) {
            String databasePath = getActivity().getDir("databases", Context.MODE_PRIVATE).getPath();
            mWebView.getSettings().setDatabasePath(databasePath);
            mWebView.getSettings().setDatabaseEnabled(true);
        }

        mIsWebViewAvailable = true;

        // Custom User Agent
        WebSettings settings = mWebView.getSettings();
        settings.setUserAgentString(getUserAgent());

        // Local caching for offline use
        settings.setAppCacheMaxSize(APP_CACHE_SIZE);
        settings.setAppCachePath(getActivity().getApplicationContext().getCacheDir().getAbsolutePath());
        settings.setAllowFileAccess(true);
        settings.setAppCacheEnabled(true);
        settings.setJavaScriptEnabled(ENABLE_JAVASCRIPT);
        settings.setCacheMode(WebSettings.LOAD_DEFAULT);
        if (!hasConnection()) {
            settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
        }

        // Open links in default browser and show offline message when needed
        mWebView.setWebViewClient(new WebViewClient() {

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                mHasFailed = false;
                //noinspection PointlessBooleanExpression
                if (OPEN_LINKS_IN_BROWSER && !isSameDomain(mWebDomain, url)) {
                    Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                    startActivity(browserIntent);
                    return true;
                }
                return false;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                if (!mHasFailed) {
                    // Hide offline message (if any)
                    mWebView.setVisibility(View.VISIBLE);
                    mRootView.findViewById(R.id.vg_offline).setVisibility(View.GONE);
                }
            }

            @Override
            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
                // Check if we can reach the settings:
                final Intent intent = new Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS);
                List<ResolveInfo> list = getActivity().getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
                if (list.size() == 0) {
                    // No? Hide the settings button and divider
                    mRootView.findViewById(R.id.divider).setVisibility(View.GONE);
                    mRootView.findViewById(R.id.bt_settings).setVisibility(View.GONE);
                }
                // Show message:
                mRootView.findViewById(R.id.vg_offline).setVisibility(View.VISIBLE);
                mWebView.setVisibility(View.GONE);
                mHasFailed = true;
            }
        });

        // Configure progress bar
        mWebView.setWebChromeClient(
                new WebChromeClient() {
                    public void onProgressChanged(WebView view, int progress) {
                        if (progress < 100) {
                            mProgress.setVisibility(View.VISIBLE);
                            mProgress.setProgress(progress);
                        } else {
                            mProgress.setVisibility(View.GONE);
                        }
                    }
                });

        // Go!
        mWebView.loadUrl(mWebUrl);

        return mRootView;
    }

    /**
     * Checks if the given url is in the same domain as the given domain
     *
     * @param domain The domain
     * @param url    The url
     * @return
     */
    private boolean isSameDomain(String domain, String url) {
        return domain != null && domain.equals(getDomain(url));
    }

    /**
     * Returns the domain part of a url
     *
     * @param url
     * @return
     */
    private String getDomain(String url) {
        try {
            return new URL(url).getHost();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Creates a custom user-agent string in the shape of {@link #USER_AGENT}.
     *
     * @return A string, suitable to be used as the user-agent in the webview
     */
    protected String getUserAgent() {
        int versionCode = 0;
        String versionName = "";
        try {
            versionCode = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0).versionCode;
            versionName = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0).versionName;
        } catch (NameNotFoundException e) {
            Log.e(TAG, Log.getStackTraceString(e));
        }
        String appName = getString(R.string.app_name);
        return String.format(USER_AGENT,
                appName,
                versionName,
                versionCode,
                Build.VERSION.RELEASE,
                Build.VERSION.SDK_INT,
                Build.MANUFACTURER,
                Build.MODEL);
    }

    /**
     * Checks if there's an active data connection. Does not bother verifying the connection, or to
     * differentiate between WiFi or metered connection.
     *
     * @return {@code true} if the page can probably be loaded.
     */
    public boolean hasConnection() {
        ConnectivityManager man = (ConnectivityManager)
                getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
        return man.getActiveNetworkInfo() != null;
    }

    @Override
    public void onPause() {
        super.onPause();
        mWebView.onPause();
    }

    @Override
    public void onResume() {
        if (mIsWebViewAvailable) {
            mWebView.onResume();
        }
        super.onResume();
    }

    @Override
    public void onDestroy() {
        mIsWebViewAvailable = false;
        if (mWebView != null) {
            mWebView.destroy();
            mWebView = null;
        }
        super.onDestroy();
    }
}

fragment_web.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">

    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:id="@+id/vg_offline"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_marginLeft="@dimen/activity_horizontal_margin"
        android:layout_marginRight="@dimen/activity_horizontal_margin"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:orientation="vertical"
        android:visibility="gone"
        tools:visibility="visible">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/activity_horizontal_margin"
            android:gravity="center"
            android:text="@string/tv_no_connection" />

        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <Button
                android:id="@+id/bt_refresh"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:drawableLeft="@drawable/ic_refresh"
                android:drawableStart="@drawable/ic_refresh"
                android:padding="@dimen/activity_horizontal_margin"
                android:text="@string/bt_refresh" />

            <View
                android:id="@+id/divider"
                android:layout_width="2dp"
                android:layout_height="match_parent"
                android:layout_margin="@dimen/activity_horizontal_margin"
                android:background="@color/dark_gray" />

            <ImageButton
                android:id="@+id/bt_settings"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:contentDescription="@string/bt_settings"
                android:padding="@dimen/activity_horizontal_margin"
                android:src="@drawable/ic_settings" />
        </LinearLayout>
    </LinearLayout>

    <ProgressBar
        android:id="@+id/pb_web"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="-6dp"
        android:visibility="gone"
        tools:visibility="visible" />
</FrameLayout>