Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / remoting / android / java / src / org / chromium / chromoting / DesktopView.java
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.chromoting;
6
7 import android.content.Context;
8 import android.graphics.Bitmap;
9 import android.graphics.Canvas;
10 import android.graphics.Color;
11 import android.graphics.Paint;
12 import android.graphics.Point;
13 import android.graphics.RadialGradient;
14 import android.graphics.Shader;
15 import android.os.Looper;
16 import android.os.SystemClock;
17 import android.text.InputType;
18 import android.util.AttributeSet;
19 import android.util.Log;
20 import android.view.MotionEvent;
21 import android.view.SurfaceHolder;
22 import android.view.SurfaceView;
23 import android.view.inputmethod.EditorInfo;
24 import android.view.inputmethod.InputConnection;
25 import android.view.inputmethod.InputMethodManager;
26
27 import org.chromium.chromoting.jni.JniInterface;
28
29 /**
30  * The user interface for viewing and interacting with a specific remote host.
31  * It provides a canvas onto which the video feed is rendered, handles
32  * multitouch pan and zoom gestures, and collects and forwards input events.
33  */
34 /** GUI element that holds the drawing canvas. */
35 public class DesktopView extends SurfaceView implements DesktopViewInterface,
36         SurfaceHolder.Callback {
37     private RenderData mRenderData;
38     private TouchInputHandler mInputHandler;
39
40     /** The parent Desktop activity. */
41     private Desktop mDesktop;
42
43     // Flag to prevent multiple repaint requests from being backed up. Requests for repainting will
44     // be dropped if this is already set to true. This is used by the main thread and the painting
45     // thread, so the access should be synchronized on |mRenderData|.
46     private boolean mRepaintPending;
47
48     // Flag used to ensure that the SurfaceView is only painted between calls to surfaceCreated()
49     // and surfaceDestroyed(). Accessed on main thread and display thread, so this should be
50     // synchronized on |mRenderData|.
51     private boolean mSurfaceCreated = false;
52
53     /** Helper class for displaying the long-press feedback animation. This class is thread-safe. */
54     private static class FeedbackAnimator {
55         /** Total duration of the animation, in milliseconds. */
56         private static final float TOTAL_DURATION_MS = 220;
57
58         /** Start time of the animation, from {@link SystemClock#uptimeMillis()}. */
59         private long mStartTime = 0;
60
61         private boolean mRunning = false;
62
63         /** Lock to allow multithreaded access to {@link #mStartTime} and {@link #mRunning}. */
64         private Object mLock = new Object();
65
66         private Paint mPaint = new Paint();
67
68         public boolean isAnimationRunning() {
69             synchronized (mLock) {
70                 return mRunning;
71             }
72         }
73
74         /**
75          * Begins a new animation sequence. After calling this method, the caller should
76          * call {@link #render(Canvas, float, float, float)} periodically whilst
77          * {@link #isAnimationRunning()} returns true.
78          */
79         public void startAnimation() {
80             synchronized (mLock) {
81                 mRunning = true;
82                 mStartTime = SystemClock.uptimeMillis();
83             }
84         }
85
86         public void render(Canvas canvas, float x, float y, float size) {
87             // |progress| is 0 at the beginning, 1 at the end.
88             float progress;
89             synchronized (mLock) {
90                 progress = (SystemClock.uptimeMillis() - mStartTime) / TOTAL_DURATION_MS;
91                 if (progress >= 1) {
92                     mRunning = false;
93                     return;
94                 }
95             }
96
97             // Animation grows from 0 to |size|, and goes from fully opaque to transparent for a
98             // seamless fading-out effect. The animation needs to have more than one color so it's
99             // visible over any background color.
100             float radius = size * progress;
101             int alpha = (int) ((1 - progress) * 0xff);
102
103             int transparentBlack = Color.argb(0, 0, 0, 0);
104             int white = Color.argb(alpha, 0xff, 0xff, 0xff);
105             int black = Color.argb(alpha, 0, 0, 0);
106             mPaint.setShader(new RadialGradient(x, y, radius,
107                     new int[] {transparentBlack, white, black, transparentBlack},
108                     new float[] {0.0f, 0.8f, 0.9f, 1.0f}, Shader.TileMode.CLAMP));
109             canvas.drawCircle(x, y, radius, mPaint);
110         }
111     }
112
113     private FeedbackAnimator mFeedbackAnimator = new FeedbackAnimator();
114
115     // Variables to control animation by the TouchInputHandler.
116
117     /** Protects mInputAnimationRunning. */
118     private Object mAnimationLock = new Object();
119
120     /** Whether the TouchInputHandler has requested animation to be performed. */
121     private boolean mInputAnimationRunning = false;
122
123     public DesktopView(Context context, AttributeSet attributes) {
124         super(context, attributes);
125
126         // Give this view keyboard focus, allowing us to customize the soft keyboard's settings.
127         setFocusableInTouchMode(true);
128
129         mRenderData = new RenderData();
130         mInputHandler = new TrackingInputHandler(this, context, mRenderData);
131         mRepaintPending = false;
132
133         getHolder().addCallback(this);
134     }
135
136     public void setDesktop(Desktop desktop) {
137         mDesktop = desktop;
138     }
139
140     /** Request repainting of the desktop view. */
141     void requestRepaint() {
142         synchronized (mRenderData) {
143             if (mRepaintPending) {
144                 return;
145             }
146             mRepaintPending = true;
147         }
148         JniInterface.redrawGraphics();
149     }
150
151     /** Called whenever the screen configuration is changed. */
152     public void onScreenConfigurationChanged() {
153         mInputHandler.onScreenConfigurationChanged();
154     }
155
156     /**
157      * Redraws the canvas. This should be done on a non-UI thread or it could
158      * cause the UI to lag. Specifically, it is currently invoked on the native
159      * graphics thread using a JNI.
160      */
161     public void paint() {
162         long startTimeMs = SystemClock.uptimeMillis();
163
164         if (Looper.myLooper() == Looper.getMainLooper()) {
165             Log.w("deskview", "Canvas being redrawn on UI thread");
166         }
167
168         Bitmap image = JniInterface.getVideoFrame();
169         if (image == null) {
170             // This can happen if the client is connected, but a complete video frame has not yet
171             // been decoded.
172             return;
173         }
174
175         int width = image.getWidth();
176         int height = image.getHeight();
177         boolean sizeChanged = false;
178         synchronized (mRenderData) {
179             if (mRenderData.imageWidth != width || mRenderData.imageHeight != height) {
180                 // TODO(lambroslambrou): Move this code into a sizeChanged() callback, to be
181                 // triggered from JniInterface (on the display thread) when the remote screen size
182                 // changes.
183                 mRenderData.imageWidth = width;
184                 mRenderData.imageHeight = height;
185                 sizeChanged = true;
186             }
187         }
188         if (sizeChanged) {
189             mInputHandler.onHostSizeChanged(width, height);
190         }
191
192         Canvas canvas;
193         int x, y;
194         synchronized (mRenderData) {
195             mRepaintPending = false;
196             // Don't try to lock the canvas before it is ready, as the implementation of
197             // lockCanvas() may throttle these calls to a slow rate in order to avoid consuming CPU.
198             // Note that a successful call to lockCanvas() will prevent the framework from
199             // destroying the Surface until it is unlocked.
200             if (!mSurfaceCreated) {
201                 return;
202             }
203             canvas = getHolder().lockCanvas();
204             if (canvas == null) {
205                 return;
206             }
207             canvas.setMatrix(mRenderData.transform);
208             x = mRenderData.cursorPosition.x;
209             y = mRenderData.cursorPosition.y;
210         }
211
212         canvas.drawColor(Color.BLACK);
213         canvas.drawBitmap(image, 0, 0, new Paint());
214
215         boolean feedbackAnimationRunning = mFeedbackAnimator.isAnimationRunning();
216         if (feedbackAnimationRunning) {
217             float scaleFactor;
218             synchronized (mRenderData) {
219                 scaleFactor = mRenderData.transform.mapRadius(1);
220             }
221             mFeedbackAnimator.render(canvas, x, y, 40 / scaleFactor);
222         }
223
224         Bitmap cursorBitmap = JniInterface.getCursorBitmap();
225         if (cursorBitmap != null) {
226             Point hotspot = JniInterface.getCursorHotspot();
227             canvas.drawBitmap(cursorBitmap, x - hotspot.x, y - hotspot.y, new Paint());
228         }
229
230         getHolder().unlockCanvasAndPost(canvas);
231
232         synchronized (mAnimationLock) {
233             if (mInputAnimationRunning || feedbackAnimationRunning) {
234                 getHandler().postAtTime(new Runnable() {
235                     @Override
236                     public void run() {
237                         processAnimation();
238                     }
239                 }, startTimeMs + 30);
240             }
241         };
242     }
243
244     private void processAnimation() {
245         boolean running;
246         synchronized (mAnimationLock) {
247             running = mInputAnimationRunning;
248         }
249         if (running) {
250             mInputHandler.processAnimation();
251         }
252         running |= mFeedbackAnimator.isAnimationRunning();
253         if (running) {
254             requestRepaint();
255         }
256     }
257
258     /**
259      * Called after the canvas is initially created, then after every subsequent resize, as when
260      * the display is rotated.
261      */
262     @Override
263     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
264         synchronized (mRenderData) {
265             mRenderData.screenWidth = width;
266             mRenderData.screenHeight = height;
267         }
268
269         JniInterface.provideRedrawCallback(new Runnable() {
270             @Override
271             public void run() {
272                 paint();
273             }
274         });
275         mInputHandler.onClientSizeChanged(width, height);
276         requestRepaint();
277     }
278
279     /** Called when the canvas is first created. */
280     @Override
281     public void surfaceCreated(SurfaceHolder holder) {
282         synchronized (mRenderData) {
283             mSurfaceCreated = true;
284         }
285     }
286
287     /**
288      * Called when the canvas is finally destroyed. Marks the canvas as needing a redraw so that it
289      * will not be blank if the user later switches back to our window.
290      */
291     @Override
292     public void surfaceDestroyed(SurfaceHolder holder) {
293         // Stop this canvas from being redrawn.
294         JniInterface.provideRedrawCallback(null);
295
296         synchronized (mRenderData) {
297             mSurfaceCreated = false;
298         }
299     }
300
301     /** Called when a software keyboard is requested, and specifies its options. */
302     @Override
303     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
304         // Disables rich input support and instead requests simple key events.
305         outAttrs.inputType = InputType.TYPE_NULL;
306
307         // Prevents most third-party IMEs from ignoring our Activity's adjustResize preference.
308         outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
309
310         // Ensures that keyboards will not decide to hide the remote desktop on small displays.
311         outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI;
312
313         // Stops software keyboards from closing as soon as the enter key is pressed.
314         outAttrs.imeOptions |= EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION;
315
316         return null;
317     }
318
319     /** Called whenever the user attempts to touch the canvas. */
320     @Override
321     public boolean onTouchEvent(MotionEvent event) {
322         return mInputHandler.onTouchEvent(event);
323     }
324
325     @Override
326     public void injectMouseEvent(int x, int y, int button, boolean pressed) {
327         boolean cursorMoved = false;
328         synchronized (mRenderData) {
329             // Test if the cursor actually moved, which requires repainting the cursor. This
330             // requires that the TouchInputHandler doesn't mutate |mRenderData.cursorPosition|
331             // directly.
332             if (x != mRenderData.cursorPosition.x) {
333                 mRenderData.cursorPosition.x = x;
334                 cursorMoved = true;
335             }
336             if (y != mRenderData.cursorPosition.y) {
337                 mRenderData.cursorPosition.y = y;
338                 cursorMoved = true;
339             }
340         }
341
342         if (button == TouchInputHandler.BUTTON_UNDEFINED && !cursorMoved) {
343             // No need to inject anything or repaint.
344             return;
345         }
346
347         JniInterface.sendMouseEvent(x, y, button, pressed);
348         if (cursorMoved) {
349             // TODO(lambroslambrou): Optimize this by only repainting the affected areas.
350             requestRepaint();
351         }
352     }
353
354     @Override
355     public void injectMouseWheelDeltaEvent(int deltaX, int deltaY) {
356         JniInterface.sendMouseWheelEvent(deltaX, deltaY);
357     }
358
359     @Override
360     public void showLongPressFeedback() {
361         mFeedbackAnimator.startAnimation();
362         requestRepaint();
363     }
364
365     @Override
366     public void showActionBar() {
367         mDesktop.showActionBar();
368     }
369
370     @Override
371     public void showKeyboard() {
372         InputMethodManager inputManager =
373                 (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
374         inputManager.showSoftInput(this, 0);
375     }
376
377     @Override
378     public void transformationChanged() {
379         requestRepaint();
380     }
381
382     @Override
383     public void setAnimationEnabled(boolean enabled) {
384         synchronized (mAnimationLock) {
385             if (enabled && !mInputAnimationRunning) {
386                 requestRepaint();
387             }
388             mInputAnimationRunning = enabled;
389         }
390     }
391 }