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