1 /*
2  * Copyright (C) 2012 Google Inc.
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mail.browse;
19 
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.Canvas;
24 import android.util.AttributeSet;
25 import android.view.MotionEvent;
26 
27 import com.android.mail.R;
28 import com.android.mail.utils.LogTag;
29 import com.android.mail.utils.LogUtils;
30 
31 import java.util.Set;
32 import java.util.concurrent.CopyOnWriteArraySet;
33 
34 public class ConversationWebView extends MailWebView implements ScrollNotifier {
35     /** The initial delay when rendering in hardware layer. */
36     private final int mWebviewInitialDelay;
37 
38     private Bitmap mBitmap;
39     private Canvas mCanvas;
40 
41     private boolean mUseSoftwareLayer;
42     /**
43      * Whether this view is user-visible; we don't bother doing supplemental software drawing
44      * if the view is off-screen.
45      */
46     private boolean mVisible;
47 
48     /** {@link Runnable} to be run when the page is rendered in hardware layer. */
49     private final Runnable mNotifyPageRenderedInHardwareLayer = new Runnable() {
50         @Override
51         public void run() {
52             // Switch to hardware layer.
53             mUseSoftwareLayer = false;
54             destroyBitmap();
55             invalidate();
56         }
57     };
58 
59     @Override
onDraw(Canvas canvas)60     public void onDraw(Canvas canvas) {
61         // Always render in hardware layer to avoid flicker when switch.
62         super.onDraw(canvas);
63 
64         // Render in software layer on top if needed, and we're visible (i.e. it's worthwhile to
65         // do all this)
66         if (mUseSoftwareLayer && mVisible && getWidth() > 0 && getHeight() > 0) {
67             if (mBitmap == null) {
68                 try {
69                     // Create an offscreen bitmap.
70                     mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.RGB_565);
71                     mCanvas = new Canvas(mBitmap);
72                 } catch (OutOfMemoryError e) {
73                     // just give up
74                     mBitmap = null;
75                     mCanvas = null;
76                     mUseSoftwareLayer = false;
77                 }
78             }
79 
80             if (mBitmap != null) {
81                 final int x = getScrollX();
82                 final int y = getScrollY();
83 
84                 mCanvas.save();
85                 mCanvas.translate(-x, -y);
86                 super.onDraw(mCanvas);
87                 mCanvas.restore();
88 
89                 canvas.drawBitmap(mBitmap, x, y, null /* paint */);
90             }
91         }
92     }
93 
94     @Override
destroy()95     public void destroy() {
96         destroyBitmap();
97         removeCallbacks(mNotifyPageRenderedInHardwareLayer);
98 
99         super.destroy();
100     }
101 
102     /**
103      * Destroys the {@link Bitmap} used for software layer.
104      */
destroyBitmap()105     private void destroyBitmap() {
106         if (mBitmap != null) {
107             mBitmap = null;
108             mCanvas = null;
109         }
110     }
111 
112     /**
113      * Enable this WebView to also draw to an internal software canvas until
114      * {@link #onRenderComplete()} is called. The software draw will happen every time
115      * a normal {@link #onDraw(Canvas)} happens, and will overwrite whatever is normally drawn
116      * (i.e. drawn in hardware) with the results of software rendering.
117      * <p>
118      * This is useful when you know that the WebView draws sooner to a software layer than it does
119      * to its normal hardware layer.
120      */
setUseSoftwareLayer(boolean useSoftware)121     public void setUseSoftwareLayer(boolean useSoftware) {
122         mUseSoftwareLayer = useSoftware;
123     }
124 
125     /**
126      * Notifies the {@link ConversationWebView} that it has become visible. It can use this signal
127      * to switch between software and hardware layer.
128      */
onRenderComplete()129     public void onRenderComplete() {
130         if (mUseSoftwareLayer) {
131             // Schedule to switch from software layer to hardware layer in 1s.
132             postDelayed(mNotifyPageRenderedInHardwareLayer, mWebviewInitialDelay);
133         }
134     }
135 
onUserVisibilityChanged(boolean visible)136     public void onUserVisibilityChanged(boolean visible) {
137         mVisible = visible;
138     }
139 
140     private final int mViewportWidth;
141     private final float mDensity;
142 
143     private final Set<ScrollListener> mScrollListeners =
144             new CopyOnWriteArraySet<ScrollListener>();
145 
146     /**
147      * True when WebView is handling a touch-- in between POINTER_DOWN and
148      * POINTER_UP/POINTER_CANCEL.
149      */
150     private boolean mHandlingTouch;
151     private boolean mIgnoringTouch;
152 
153     private static final String LOG_TAG = LogTag.getLogTag();
154 
ConversationWebView(Context c)155     public ConversationWebView(Context c) {
156         this(c, null);
157     }
158 
ConversationWebView(Context c, AttributeSet attrs)159     public ConversationWebView(Context c, AttributeSet attrs) {
160         super(c, attrs);
161 
162         final Resources r = getResources();
163         mViewportWidth = r.getInteger(R.integer.conversation_webview_viewport_px);
164         mWebviewInitialDelay = r.getInteger(R.integer.webview_initial_delay);
165         mDensity = r.getDisplayMetrics().density;
166     }
167 
168     @Override
addScrollListener(ScrollListener l)169     public void addScrollListener(ScrollListener l) {
170         mScrollListeners.add(l);
171     }
172 
173     @Override
removeScrollListener(ScrollListener l)174     public void removeScrollListener(ScrollListener l) {
175         mScrollListeners.remove(l);
176     }
177 
178     @Override
onScrollChanged(int l, int t, int oldl, int oldt)179     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
180         super.onScrollChanged(l, t, oldl, oldt);
181 
182         for (ScrollListener listener : mScrollListeners) {
183             listener.onNotifierScroll(t);
184         }
185     }
186 
187     @Override
onTouchEvent(MotionEvent ev)188     public boolean onTouchEvent(MotionEvent ev) {
189         final int action = ev.getActionMasked();
190 
191         switch (action) {
192             case MotionEvent.ACTION_DOWN:
193                 mHandlingTouch = true;
194                 break;
195             case MotionEvent.ACTION_POINTER_DOWN:
196                 LogUtils.d(LOG_TAG, "WebView disabling intercepts: POINTER_DOWN");
197                 requestDisallowInterceptTouchEvent(true);
198                 break;
199             case MotionEvent.ACTION_CANCEL:
200             case MotionEvent.ACTION_UP:
201                 mHandlingTouch = false;
202                 mIgnoringTouch = false;
203                 break;
204         }
205 
206         final boolean handled = mIgnoringTouch || super.onTouchEvent(ev);
207 
208         return handled;
209     }
210 
isHandlingTouch()211     public boolean isHandlingTouch() {
212         return mHandlingTouch;
213     }
214 
getViewportWidth()215     public int getViewportWidth() {
216         return mViewportWidth;
217     }
218 
219     /**
220      * Returns the effective width available for HTML content in DP units. This width takes into
221      * account the given margin (in screen px) by excluding it. This is not the same as DOM width,
222      * since the document is rendered at CSS px={@link #mViewportWidth}.
223      *
224      * @param sideMarginPx HTML body margin, if any (in screen px)
225      * @return width available for HTML content (in dp)
226      */
getWidthInDp(int sideMarginPx)227     public int getWidthInDp(int sideMarginPx) {
228         return (int) ((getWidth() - sideMarginPx * 2) / mDensity);
229     }
230 
231     /**
232      * Similar to {@link #getScale()}, except that it returns the initially expected scale, as
233      * determined by the ratio of actual screen pixels to logical HTML pixels.
234      * <p>This assumes that we are able to control the logical HTML viewport with a meta-viewport
235      * tag.
236      */
getInitialScale()237     public float getInitialScale() {
238         final float scale;
239         if (getSettings().getLoadWithOverviewMode()) {
240             // in overview mode (aka auto-fit mode), the base ratio is screen px : viewport px
241             scale = (float) getWidth() / getViewportWidth();
242         } else {
243             // in no-zoom mode, the base ratio is just screen px : mdpi css px (i.e. density)
244             scale = mDensity;
245         }
246         return scale;
247     }
248 
screenPxToWebPx(int screenPx)249     public int screenPxToWebPx(int screenPx) {
250         return (int) (screenPx / getInitialScale());
251     }
252 
webPxToScreenPx(int webPx)253     public int webPxToScreenPx(int webPx) {
254         return (int) (webPx * getInitialScale());
255     }
256 
screenPxToWebPxError(int screenPx)257     public float screenPxToWebPxError(int screenPx) {
258         return screenPx / getInitialScale() - screenPxToWebPx(screenPx);
259     }
260 
webPxToScreenPxError(int webPx)261     public float webPxToScreenPxError(int webPx) {
262         return webPx * getInitialScale() - webPxToScreenPx(webPx);
263     }
264 
265 }
266