Last active
August 17, 2018 15:40
-
-
Save mlagerberg/b3d19685c8c021fa5ee6 to your computer and use it in GitHub Desktop.
[WebFragment] Fragment with WebView, busy indicator, custom user-agent, and correct fallback when offline. SDK 9 and up #android
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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" | |
android:paddingTop="?attr/actionBarSize"> | |
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.github.gist.monkeyinmysoup; | |
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.github.gist.monkeyinmysoup.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(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment