/** * Taken from http://stackoverflow.com/a/8949378 */ package com.erakk.lnreader.helper; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.webkit.ValueCallback; import android.webkit.WebBackForwardList; import android.webkit.WebChromeClient; import android.webkit.WebHistoryItem; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; import android.widget.ZoomButtonsController; import com.erakk.lnreader.LNReaderApplication; import com.erakk.lnreader.UIHelper; import com.erakk.lnreader.dao.NovelsDao; import com.erakk.lnreader.model.PageModel; import java.io.File; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.Method; /** * see http://stackoverflow.com/questions/3130654/memory-leak-in-webview and * http://code.google.com/p/android/issues/detail?id=9375 * Note that the bug does NOT appear to be fixed in android 2.2 as romain claims * <p/> * Also, you must call {@link #destroy()} from your activity's onDestroy method. */ public class NonLeakingWebView extends WebView { private static final String TAG = NonLeakingWebView.class.toString(); private static Field sConfigCallback; private ZoomButtonsController zoom_control; private static boolean showZoom; static { try { sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback"); sConfigCallback.setAccessible(true); } catch (Exception e) { // ignored } } public NonLeakingWebView(Context context) { super(context); init(context); } public NonLeakingWebView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public NonLeakingWebView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(Context context) { if (!isInEditMode()) { setWebViewClient(new MyWebViewClient((Activity) context)); // Create our ScaleGestureDetector mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); // fake user agent to mobile String userAgent = this.getSettings().getUserAgentString(); if (!userAgent.contains("Mobile")) { if (userAgent.contains("Safari")) { userAgent = userAgent.replace("Safari", "Mobile Safari"); } else { userAgent = userAgent + " Mobile Safari/537.16"; } this.getSettings().setUserAgentString(userAgent); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // allow to open local file even in https mode. this.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); } } } @Override public void destroy() { Log.d(TAG, "Destroying WebView"); setDisplayZoomControl(false); super.destroy(); try { if (sConfigCallback != null) sConfigCallback.set(null, null); } catch (Exception e) { throw new RuntimeException(e); } } /** * Set option to display zoom control * http://stackoverflow.com/a/11901948 * * @param show */ @SuppressLint("NewApi") public void setDisplayZoomControl(boolean show) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { this.getSettings().setDisplayZoomControls(show); } else { // get the control try { Class webview = Class.forName("android.webkit.WebView"); Method method = webview.getMethod("getZoomButtonsController"); zoom_control = (ZoomButtonsController) method.invoke(this); showZoom = show; } catch (Exception e) { Log.e(TAG, "Error when getting zoom control", e); } } } @Override public boolean onTouchEvent(MotionEvent ev) { // TODO: Error when exiting the current activity with zoom control shown. // E/WindowManager(6797): android.view.WindowLeaked: Activity // com.erakk.lnreader.UI.activity.DisplayImageActivity has leaked window // android.widget.ZoomButtonsController$Container{41c709a0 V.E..... ........ 0,0-540,73} that was originally // added here super.onTouchEvent(ev); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB && zoom_control != null) { // Hide the control AFTER they where made visible by the default implementation. zoom_control.setVisible(showZoom); } try { checkZoomEvent(ev); } catch (IllegalArgumentException ex) { Log.e(TAG, "Failed to handle zoom event.", ex); } return true; } public static final String PREFIX_PAGEMODEL = "pageModel:"; @Override public boolean canGoBack() { WebBackForwardList history = this.copyBackForwardList(); WebHistoryItem prevData = history.getItemAtIndex(history.getCurrentIndex() - 1); if (prevData != null) { if (prevData.getUrl().startsWith(PREFIX_PAGEMODEL)) { Log.d(TAG, "Previous data is internal page: " + prevData.getUrl()); return false; } else Log.d(TAG, "Back to: " + prevData.getUrl()); } return super.canGoBack(); } protected static class MyWebViewClient extends WebViewClient { protected WeakReference<Activity> activityRef; public MyWebViewClient(Activity activity) { this.activityRef = new WeakReference<Activity>(activity); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { try { final Activity activity = activityRef.get(); if (activity != null) activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } catch (RuntimeException ignored) { // ignore any url parsing exceptions } return true; } } /** * Enable onScaleChange for pinch zoom * http://android-developers.blogspot.sg/2010/06/making-sense-of-multitouch.html */ private float mPosX; private float mPosY; private float mLastTouchX; private float mLastTouchY; private static final int INVALID_POINTER_ID = -1; // The �active pointer� is the one currently moving our object. private int mActivePointerId = INVALID_POINTER_ID; private ScaleGestureDetector mScaleDetector; private float mScaleFactor = 1.f; private void checkZoomEvent(MotionEvent ev) { // Let the ScaleGestureDetector inspect all events. mScaleFactor = this.getScale(); mScaleDetector.onTouchEvent(ev); final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); mLastTouchX = x; mLastTouchY = y; mActivePointerId = ev.getPointerId(0); break; } case MotionEvent.ACTION_MOVE: { try { final int pointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(pointerIndex); final float y = ev.getY(pointerIndex); // Only move if the ScaleGestureDetector isn't processing a gesture. if (!mScaleDetector.isInProgress()) { final float dx = x - mLastTouchX; final float dy = y - mLastTouchY; mPosX += dx; mPosY += dy; invalidate(); } mLastTouchX = x; mLastTouchY = y; } catch (ArrayIndexOutOfBoundsException ex) { Log.i(TAG, "Failed to get the motion event"); } break; } case MotionEvent.ACTION_UP: { mActivePointerId = INVALID_POINTER_ID; break; } case MotionEvent.ACTION_CANCEL: { mActivePointerId = INVALID_POINTER_ID; break; } case MotionEvent.ACTION_POINTER_UP: { final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastTouchX = ev.getX(newPointerIndex); mLastTouchY = ev.getY(newPointerIndex); mActivePointerId = ev.getPointerId(newPointerIndex); } break; } } } private WebViewClient currentWebClient = null; @Override public void setWebViewClient(WebViewClient client) { super.setWebViewClient(client); this.currentWebClient = client; } public WebViewClient getWebViewClient() { return this.currentWebClient; } private WebChromeClient currentWebChromeClient; @Override public void setWebChromeClient(WebChromeClient client) { super.setWebChromeClient(client); this.currentWebChromeClient = client; } public WebChromeClient getWebChromeClient() { return this.currentWebChromeClient; } private void triggerOnScaleChanged(float oldScale, float newScale) { if (currentWebClient != null) { currentWebClient.onScaleChanged(this, oldScale, newScale); } } /** * Save as webarchive, only works for API 11++ * * @param page */ @SuppressLint("NewApi") public void saveMyWebArchive(String page) { if (page == null) { Log.w(TAG, "Empty page name, trying to resolve from current webView url!"); String url = this.getUrl(); if (Util.isStringNullOrEmpty(url)) { Log.w(TAG, "Empty Url!"); return; } page = url; } if (!page.startsWith("http")) { Log.w(TAG, "Skipping non-http url: " + page); return; } Log.d(TAG, "Trying to save: " + page); try { PageModel pageModel = new PageModel(page); pageModel = NovelsDao.getInstance().getExistingPageModel(pageModel, null); if (pageModel != null && !pageModel.isExternal()) { Log.w(TAG, "PageModel: " + pageModel.getPage() + " is not external!"); return; } } catch (Exception e1) { Log.e(TAG, "Failed to load page model: " + page, e1); } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { String wacName = getWacNameForSaving(Util.SanitizeBaseUrl(page), false); final String p2 = Util.SanitizeBaseUrl(page); this.saveWebArchive(wacName, false, new ValueCallback<String>() { @Override public void onReceiveValue(String value) { Log.i(TAG, "Saving url: " + p2 + " ==> Saved to: " + value); Toast.makeText(LNReaderApplication.getInstance().getApplicationContext(), "Page saved to: " + value, Toast.LENGTH_SHORT).show(); } }); } } catch (Exception e) { Log.e(TAG, "Failed to save external page: " + page, e); } } private String getWacNameForSaving(String url, boolean refresh) { String path = UIHelper.getImageRoot(LNReaderApplication.getInstance()) + "/wac"; File f = new File(path); if (!f.exists()) f.mkdirs(); Log.i(TAG, "WAC dirs: " + path); String filename = path + "/" + Util.getBaseWacName(url); String extension = ".wac"; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { extension = ".mht"; } return filename + extension; } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { float oldScale = mScaleFactor; mScaleFactor *= detector.getScaleFactor(); // Don't let the object get too small or too large. mScaleFactor = Math.max(0.5f, Math.min(mScaleFactor, 5.0f)); triggerOnScaleChanged(oldScale, mScaleFactor); return true; } } }