Java tutorial
/* * Copyright (C) 2013 Daniel Velazco * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.danvelazco.fbwrapper.activity; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.support.annotation.NonNull; import android.support.v4.widget.SwipeRefreshLayout; import android.text.TextUtils; import android.view.ContextMenu; import android.view.KeyEvent; import android.view.MenuItem; import android.view.View; import android.webkit.CookieSyncManager; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.Toast; import com.danvelazco.fbwrapper.R; import com.danvelazco.fbwrapper.util.Logger; import com.danvelazco.fbwrapper.util.OrbotHelper; import com.danvelazco.fbwrapper.util.WebViewProxyUtil; import com.danvelazco.fbwrapper.webview.FacebookWebChromeClient; import com.danvelazco.fbwrapper.webview.FacebookWebView; import com.danvelazco.fbwrapper.webview.FacebookWebViewClient; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import java.io.File; import java.io.FileOutputStream; /** * Base activity that uses a {@link FacebookWebView} to load the Facebook * site in different formats. Here we can implement all the boilerplate code * that has to do with loading the activity as well as lifecycle events. * <p/> * See {@link #onActivityCreated()} * See {@link #onWebViewInit(android.os.Bundle)} * See {@link #onResumeActivity()} */ public abstract class BaseFacebookWebViewActivity extends Activity implements FacebookWebViewClient.WebViewClientListener, FacebookWebChromeClient.WebChromeClientListener, SwipeRefreshLayout.OnRefreshListener { // Constants private final static String LOG_TAG = "BaseFacebookWebViewActivity"; protected final static int RESULT_CODE_FILE_UPLOAD = 1001; protected final static int RESULT_CODE_FILE_UPLOAD_LOLLIPOP = 2001; protected static final String KEY_SAVE_STATE_TIME = "_instance_save_state_time"; private static final int ID_CONTEXT_MENU_SAVE_IMAGE = 2981279; protected final static String INIT_URL_MOBILE = "https://m.facebook.com"; protected final static String INIT_URL_DESKTOP = "https://www.facebook.com"; protected final static String INIT_URL_FACEBOOK_ZERO = "https://0.facebook.com"; protected final static String INIT_URL_FACEBOOK_ONION = "https://facebookcorewwwi.onion"; protected final static String URL_PAGE_NOTIFICATIONS = "/notifications.php"; protected final static String URL_PAGE_MESSAGES = "/messages"; // URL for Sharing Links // u = url & t = title protected final static String URL_PAGE_SHARE_LINKS = "/sharer.php?u=%s&t=%s"; // Desktop user agent (Google Chrome's user agent from a MacBook running 10.9.1 protected static final String USER_AGENT_DESKTOP = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36"; // Mobile user agent (Mobile user agent from a Google Nexus S running Android 2.3.3 protected static final String USER_AGENT_MOBILE_OLD = "Mozilla/5.0 (Linux; U; Android 2.3.3; en-gb; " + "Nexus S Build/GRI20) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"; // Mobile user agent (Mobile user agent from a Google Nexus 5 running Android 4.4.2 protected static final String USER_AGENT_MOBILE = "Mozilla/5.0 (Mobile; rv:68.0) Gecko/68.0 Firefox/68.0"; // Firefox for Android user agent, it brings up a basic version of the site. Halfway between touch site and zero site. protected static final String USER_AGENT_BASIC = "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0"; // Members protected ConnectivityManager mConnectivityManager = null; protected CookieSyncManager mCookieSyncManager = null; protected FacebookWebView mWebView = null; protected ProgressBar mProgressBar = null; protected SwipeRefreshLayout mSwipeRefreshLayout = null; protected WebSettings mWebSettings = null; protected ValueCallback<Uri> mUploadMessage = null; protected ValueCallback<Uri[]> mUploadMessageLollipop = null; private boolean mCreatingActivity = true; private String mPendingImageUrlToSave = null; /** * BroadcastReceiver to handle ConnectivityManager.CONNECTIVITY_ACTION intent action. */ private BroadcastReceiver mConnectivityReceiver = new BroadcastReceiver() { /** * {@inheritDoc} */ @Override public void onReceive(Context context, Intent intent) { // Set the cache mode depending on connection type and availability updateCacheMode(); } }; /** * Called when the Activity is created. Make sure the content view * is set here. */ protected abstract void onActivityCreated(); /** * Called when we are ready to start restoring or loading * data in the {@link FacebookWebView} * * @param savedInstanceState {@link Bundle} */ protected abstract void onWebViewInit(Bundle savedInstanceState); /** * Called anything the activity is resumed. Could be used to * reload any type of preference. */ protected abstract void onResumeActivity(); /** * {@inheritDoc} */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Create the activity and set the layout onActivityCreated(); mConnectivityManager = (ConnectivityManager) getSystemService(Activity.CONNECTIVITY_SERVICE); mWebView = (FacebookWebView) findViewById(R.id.webview); mWebView.setCustomContentView((FrameLayout) findViewById(R.id.fullscreen_custom_content)); mWebView.setWebChromeClientListener(this); mWebView.setWebViewClientListener(this); mWebSettings = mWebView.getWebSettings(); mProgressBar = (ProgressBar) findViewById(R.id.progress_bar); mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout); mSwipeRefreshLayout.setOnRefreshListener(this); // Set the database path for this WebView so that // HTML5 Storage API works properly mWebSettings.setAppCacheEnabled(true); mWebSettings.setDatabaseEnabled(true); // Create a CookieSyncManager instance and keep a reference of it mCookieSyncManager = CookieSyncManager.createInstance(this); registerForContextMenu(mWebView); // Have the activity open the proper URL onWebViewInit(savedInstanceState); } /** * {@inheritDoc} */ @Override public void onResume() { super.onResume(); // Pass lifecycle events to the WebView mWebView.onResume(); // Start synchronizing the CookieSyncManager mCookieSyncManager.startSync(); // Set the cache mode depending on connection type and availability updateCacheMode(); // Register a Connectivity action receiver so that we can update the cache mode accordingly registerReceiver(mConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); // Horrible lifecycle hack if (mCreatingActivity) { mCreatingActivity = false; return; } // Resume this activity properly, reload preferences, etc. onResumeActivity(); } /** * {@inheritDoc} */ @Override public void onPause() { // Un-register the connectivity changed receiver unregisterReceiver(mConnectivityReceiver); if (mWebView != null) { // Pass lifecycle events to the WebView mWebView.onPause(); } if (mCookieSyncManager != null) { // Stop synchronizing the CookieSyncManager mCookieSyncManager.stopSync(); } super.onPause(); } /** * {@inheritDoc} */ @Override protected void onSaveInstanceState(@NonNull Bundle outState) { // Save the current time to the state bundle outState.putLong(KEY_SAVE_STATE_TIME, System.currentTimeMillis()); // Save the state of the WebView as a Bundle to the Instance State mWebView.saveState(outState); super.onSaveInstanceState(outState); } /** * {@inheritDoc} */ @Override public void onConfigurationChanged(Configuration newConfig) { // Handle orientation configuration changes super.onConfigurationChanged(newConfig); } /** * {@inheritDoc} */ @Override public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { WebView.HitTestResult result = mWebView.getHitTestResult(); switch (result.getType()) { case WebView.HitTestResult.IMAGE_TYPE: showLongPressedImageMenu(menu, result.getExtra()); break; case WebView.HitTestResult.SRC_ANCHOR_TYPE: showLongPressedLinkMenu(menu, result.getExtra()); break; } } /** * {@inheritDoc} */ @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case ID_CONTEXT_MENU_SAVE_IMAGE: saveImageToDisk(mPendingImageUrlToSave); break; } return super.onContextItemSelected(item); } /** * Set a proxy for the {@link com.danvelazco.fbwrapper.webview.FacebookWebView} * * @param host {@link String} * @param port {@link int} */ protected final void setProxy(String host, int port) { if (mWebView != null && !TextUtils.isEmpty(host) && port > 0) { WebViewProxyUtil.setProxy(getApplicationContext(), mWebView, host, port); } } /** * Restore the state of the {@link FacebookWebView} * * @param inState {@link Bundle} */ protected void restoreWebView(Bundle inState) { if (mWebView != null) { mWebView.restoreState(inState); } } /** * Set the browser user agent to be used. If the user agent should be forced, * make sure the 'force' param is set to true, otherwise the devices' default * user agent will be used. * * @param force {@link boolean} * true if we should force a custom user agent, false if not. * Note, if this flag is false the default user agent will be * used while disregarding the mobile {@link boolean} parameter * @param mobile {@link boolean} * true if we should use a custom user agent for mobile devices, * false if not. */ protected void setUserAgent(boolean force, boolean mobile, boolean facebookBasic) { if (force && mobile && !facebookBasic) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { mWebSettings.setUserAgentString(USER_AGENT_MOBILE_OLD); } else { mWebSettings.setUserAgentString(USER_AGENT_MOBILE); } } else if (force && !mobile && !facebookBasic) { mWebSettings.setUserAgentString(USER_AGENT_DESKTOP); } else if (force && mobile && facebookBasic) { mWebSettings.setUserAgentString(USER_AGENT_BASIC); } else { mWebSettings.setUserAgentString(null); } } /** * Used to load a new URL in the {@link FacebookWebView} * * @param url {@link String} */ protected void loadNewPage(String url) { if (mWebView != null) { mWebView.loadUrl(url); } } /** * Method used to allow the user to refresh the current page */ protected void refreshCurrentPage() { if (mWebView != null) { mWebView.reload(); } } /** * Method used to allow the user to jump to the top of the webview */ protected void jumpToTop() { loadNewPage("javascript:window.scrollTo(0,0);"); } /** * Used to change the geolocation flag. * * @param allow {@link boolean} true if the use of * geolocation is allowed, false if not */ protected void setAllowCheckins(boolean allow) { if (mWebView != null) { mWebView.setAllowGeolocation(allow); } } /** * Used to change to change the behaviour of the {@link FacebookWebView}<br/> * By default, this {@link FacebookWebView} will only open URLs in which the * host is facebook.com, any other links should be sent to the default browser.<br/> * However, if the user wants to open the link inside this same webview, he could, * so in that case, make sure this flag is set to true. * * @param allow {@link boolean} true if any domain could be opened * on this webview, false if only facebook domains * are allowed. */ protected void setAllowAnyDomain(boolean allow) { if (mWebView != null) { mWebView.setAllowAnyDomain(allow); } } /** * Used to block network requests of images in the {@link WebView}. * <p/> * See {@link WebSettings#setBlockNetworkImage(boolean)} * * @param blockImages {@link boolean} */ protected void setBlockImages(boolean blockImages) { if (mWebSettings != null) { mWebSettings.setBlockNetworkImage(blockImages); } } /** * Allows us to share the page that's currently opened * using the ACTION_SEND share intent. */ protected void shareCurrentPage() { Intent i = new Intent(Intent.ACTION_SEND); i.setType("text/plain"); i.putExtra(Intent.EXTRA_SUBJECT, R.string.share_action_subject); i.putExtra(Intent.EXTRA_TEXT, mWebView.getUrl()); startActivity(Intent.createChooser(i, getString(R.string.share_action))); } /** * Show a context menu to allow the user to perform actions specifically related to the link they just long pressed * on. * * @param menu * {@link ContextMenu} * @param url * {@link String} */ private void showLongPressedLinkMenu(ContextMenu menu, String url) { // TODO: needs to be implemented, add ability to open site with external browser } /** * Show a context menu to allow the user to perform actions specifically related to the image they just long pressed * on. * * @param menu * {@link ContextMenu} * @param imageUrl * {@link String} */ private void showLongPressedImageMenu(ContextMenu menu, String imageUrl) { mPendingImageUrlToSave = imageUrl; menu.add(0, ID_CONTEXT_MENU_SAVE_IMAGE, 0, getString(R.string.lbl_save_image)); } /** * This is to be used in case we want to force kill the activity. * Might not be necessary, but it's here in case we'd like to use it. */ protected void destroyWebView() { if (mWebView != null) { mWebView.removeAllViews(); /** Free memory and destroy WebView */ mWebView.freeMemory(); mWebView.destroy(); mWebView = null; } } /** * Check whether this device has internet connection or not. * * @return {@link boolean} */ private boolean checkNetworkConnection() { try { NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo(); if (networkInfo != null) { return networkInfo.isConnected(); } return false; } catch (SecurityException e) { // Catch the Security Exception in case the user revokes the ACCESS_NETWORK_STATE permission e.printStackTrace(); // Let's assume the device has internet access return true; } } /** * Update the cache mode depending on the network connection state of the device. */ private void updateCacheMode() { if (checkNetworkConnection()) { Logger.d(LOG_TAG, "Setting cache mode to: LOAD_DEFAULT"); mWebSettings.setCacheMode(WebSettings.LOAD_DEFAULT); } else { Logger.d(LOG_TAG, "Setting cache mode to: LOAD_CACHE_ONLY"); mWebSettings.setCacheMode(WebSettings.LOAD_CACHE_ONLY); } } /** * {@inheritDoc} */ @SuppressLint("NewApi") @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { switch (requestCode) { case OrbotHelper.REQUEST_CODE_START_ORBOT: mWebView.reload(); break; case RESULT_CODE_FILE_UPLOAD: if (null == mUploadMessage) { return; } Uri result = intent == null || resultCode != RESULT_OK ? null : intent.getData(); mUploadMessage.onReceiveValue(result); mUploadMessage = null; break; case RESULT_CODE_FILE_UPLOAD_LOLLIPOP: if (null == mUploadMessageLollipop) { return; } mUploadMessageLollipop .onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, intent)); mUploadMessageLollipop = null; break; } } /** * {@inheritDoc} */ @Override public void onProgressChanged(WebView view, int progress) { Logger.d(LOG_TAG, "onProgressChanged(), progress: " + progress); // Posts current progress to the ProgressBar mProgressBar.setProgress(progress); // Hide the progress bar as soon as it goes over 85% if (progress >= 85) { mProgressBar.setVisibility(View.GONE); mSwipeRefreshLayout.setRefreshing(false); } } private AlertDialog mLocationAlertDialog = null; /** * {@inheritDoc} */ @Override public void showGeolocationDisabledAlert() { mLocationAlertDialog = new AlertDialog.Builder(this).create(); mLocationAlertDialog.setTitle(getString(R.string.lbl_dialog_alert)); mLocationAlertDialog.setMessage(getString(R.string.txt_checkins_disables)); mLocationAlertDialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.lbl_dialog_ok), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // Don't do anything here, simply close the dialog } }); mLocationAlertDialog.show(); } /** * {@inheritDoc} */ @Override public void hideGeolocationAlert() { if ((mLocationAlertDialog != null) && mLocationAlertDialog.isShowing()) { mLocationAlertDialog.dismiss(); mLocationAlertDialog = null; } } /** * {@inheritDoc} */ @Override public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { Logger.d(LOG_TAG, "openFileChooser()"); mUploadMessage = uploadMsg; Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType("image/*"); startActivityForResult(Intent.createChooser(i, getString(R.string.upload_file_choose)), RESULT_CODE_FILE_UPLOAD); } /** * {@inheritDoc} */ @SuppressLint("NewApi") @Override public boolean openFileChooser(ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) { try { Logger.d(LOG_TAG, "openFileChooser()"); mUploadMessageLollipop = filePathCallback; startActivityForResult( Intent.createChooser(fileChooserParams.createIntent(), getString(R.string.upload_file_choose)), RESULT_CODE_FILE_UPLOAD_LOLLIPOP); return true; } catch (ActivityNotFoundException e) { mUploadMessageLollipop = null; return false; } } /** * {@inheritDoc} */ @Override public void onPageLoadStarted(String url) { Logger.d(LOG_TAG, "onPageLoadStarted() -- url: " + url); mProgressBar.setVisibility(View.VISIBLE); } /** * {@inheritDoc} */ @Override public void onPageLoadFinished(String url) { Logger.d(LOG_TAG, "onPageLoadFinished() -- url: " + url); mProgressBar.setVisibility(View.GONE); mSwipeRefreshLayout.setRefreshing(false); } /** * {@inheritDoc} */ @Override public void openExternalSite(String url) { Logger.d(LOG_TAG, "openExternalSite() -- url: " + url); // This link is not for a page on my site, launch another Activity // that handles this URL Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); } } /** * {@inheritDoc} */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // Override the back button if (keyCode == KeyEvent.KEYCODE_BACK) { if (mWebView.canGoBack()) { // Check to see if there's history to go back to mWebView.goBack(); return true; } } return super.onKeyDown(keyCode, event); } /** * Save the image on the specified URL to disk * * @param imageUrl * {@link String} */ private void saveImageToDisk(String imageUrl) { if (imageUrl != null) { Picasso.with(this).load(imageUrl).into(saveImageTarget); } } private Target saveImageTarget = new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) { (new SaveImageTask()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, bitmap); } @Override public void onBitmapFailed(Drawable drawable) { Toast.makeText(BaseFacebookWebViewActivity.this, getString(R.string.txt_save_image_failed), Toast.LENGTH_LONG).show(); } @Override public void onPrepareLoad(Drawable drawable) { // Not implemented } }; public void onRefresh() { refreshCurrentPage(); } private class SaveImageTask extends AsyncTask<Bitmap, Void, Boolean> { @Override protected Boolean doInBackground(Bitmap... images) { Bitmap bitmap = images[0]; File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); File imageFile = new File(directory, System.currentTimeMillis() + ".jpg"); try { if (imageFile.createNewFile()) { FileOutputStream ostream = new FileOutputStream(imageFile); bitmap.compress(Bitmap.CompressFormat.JPEG, 75, ostream); ostream.close(); // Ping the media scanner sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(imageFile))); return true; } } catch (Exception e) { e.printStackTrace(); } return false; } @Override protected void onPostExecute(Boolean result) { if (result) { Toast.makeText(BaseFacebookWebViewActivity.this, getString(R.string.txt_save_image_success), Toast.LENGTH_LONG).show(); } else { Toast.makeText(BaseFacebookWebViewActivity.this, getString(R.string.txt_save_image_failed), Toast.LENGTH_LONG).show(); } } } }