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.
5 package org
.chromium
.chromoting
;
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
;
27 import org
.chromium
.chromoting
.jni
.JniInterface
;
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.
34 /** GUI element that holds the drawing canvas. */
35 public class DesktopView
extends SurfaceView
implements DesktopViewInterface
,
36 SurfaceHolder
.Callback
{
37 private final RenderData mRenderData
;
38 private TouchInputHandler mInputHandler
;
40 /** The parent Desktop activity. */
41 private Desktop mDesktop
;
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
;
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;
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;
58 /** Start time of the animation, from {@link SystemClock#uptimeMillis()}. */
59 private long mStartTime
= 0;
61 private boolean mRunning
= false;
63 /** Lock to allow multithreaded access to {@link #mStartTime} and {@link #mRunning}. */
64 private final Object mLock
= new Object();
66 private Paint mPaint
= new Paint();
68 public boolean isAnimationRunning() {
69 synchronized (mLock
) {
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.
79 public void startAnimation() {
80 synchronized (mLock
) {
82 mStartTime
= SystemClock
.uptimeMillis();
86 public void render(Canvas canvas
, float x
, float y
, float size
) {
87 // |progress| is 0 at the beginning, 1 at the end.
89 synchronized (mLock
) {
90 progress
= (SystemClock
.uptimeMillis() - mStartTime
) / TOTAL_DURATION_MS
;
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);
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
);
113 private FeedbackAnimator mFeedbackAnimator
= new FeedbackAnimator();
115 // Variables to control animation by the TouchInputHandler.
117 /** Protects mInputAnimationRunning. */
118 private final Object mAnimationLock
= new Object();
120 /** Whether the TouchInputHandler has requested animation to be performed. */
121 private boolean mInputAnimationRunning
= false;
123 public DesktopView(Context context
, AttributeSet attributes
) {
124 super(context
, attributes
);
126 // Give this view keyboard focus, allowing us to customize the soft keyboard's settings.
127 setFocusableInTouchMode(true);
129 mRenderData
= new RenderData();
130 mInputHandler
= new TrackingInputHandler(this, context
, mRenderData
);
131 mRepaintPending
= false;
133 getHolder().addCallback(this);
136 public void setDesktop(Desktop desktop
) {
140 /** Request repainting of the desktop view. */
141 void requestRepaint() {
142 synchronized (mRenderData
) {
143 if (mRepaintPending
) {
146 mRepaintPending
= true;
148 JniInterface
.redrawGraphics();
152 * Redraws the canvas. This should be done on a non-UI thread or it could
153 * cause the UI to lag. Specifically, it is currently invoked on the native
154 * graphics thread using a JNI.
156 public void paint() {
157 long startTimeMs
= SystemClock
.uptimeMillis();
159 if (Looper
.myLooper() == Looper
.getMainLooper()) {
160 Log
.w("deskview", "Canvas being redrawn on UI thread");
163 Bitmap image
= JniInterface
.getVideoFrame();
165 // This can happen if the client is connected, but a complete video frame has not yet
170 int width
= image
.getWidth();
171 int height
= image
.getHeight();
172 boolean sizeChanged
= false;
173 synchronized (mRenderData
) {
174 if (mRenderData
.imageWidth
!= width
|| mRenderData
.imageHeight
!= height
) {
175 // TODO(lambroslambrou): Move this code into a sizeChanged() callback, to be
176 // triggered from JniInterface (on the display thread) when the remote screen size
178 mRenderData
.imageWidth
= width
;
179 mRenderData
.imageHeight
= height
;
184 mInputHandler
.onHostSizeChanged(width
, height
);
189 synchronized (mRenderData
) {
190 mRepaintPending
= false;
191 // Don't try to lock the canvas before it is ready, as the implementation of
192 // lockCanvas() may throttle these calls to a slow rate in order to avoid consuming CPU.
193 // Note that a successful call to lockCanvas() will prevent the framework from
194 // destroying the Surface until it is unlocked.
195 if (!mSurfaceCreated
) {
198 canvas
= getHolder().lockCanvas();
199 if (canvas
== null) {
202 canvas
.setMatrix(mRenderData
.transform
);
203 x
= mRenderData
.cursorPosition
.x
;
204 y
= mRenderData
.cursorPosition
.y
;
207 canvas
.drawColor(Color
.BLACK
);
208 canvas
.drawBitmap(image
, 0, 0, new Paint());
210 boolean feedbackAnimationRunning
= mFeedbackAnimator
.isAnimationRunning();
211 if (feedbackAnimationRunning
) {
213 synchronized (mRenderData
) {
214 scaleFactor
= mRenderData
.transform
.mapRadius(1);
216 mFeedbackAnimator
.render(canvas
, x
, y
, 40 / scaleFactor
);
219 Bitmap cursorBitmap
= JniInterface
.getCursorBitmap();
220 if (cursorBitmap
!= null) {
221 Point hotspot
= JniInterface
.getCursorHotspot();
222 canvas
.drawBitmap(cursorBitmap
, x
- hotspot
.x
, y
- hotspot
.y
, new Paint());
225 getHolder().unlockCanvasAndPost(canvas
);
227 synchronized (mAnimationLock
) {
228 if (mInputAnimationRunning
|| feedbackAnimationRunning
) {
229 getHandler().postAtTime(new Runnable() {
234 }, startTimeMs
+ 30);
239 private void processAnimation() {
241 synchronized (mAnimationLock
) {
242 running
= mInputAnimationRunning
;
245 mInputHandler
.processAnimation();
247 running
|= mFeedbackAnimator
.isAnimationRunning();
254 * Called after the canvas is initially created, then after every subsequent resize, as when
255 * the display is rotated.
258 public void surfaceChanged(SurfaceHolder holder
, int format
, int width
, int height
) {
259 synchronized (mRenderData
) {
260 mRenderData
.screenWidth
= width
;
261 mRenderData
.screenHeight
= height
;
264 attachRedrawCallback();
265 mInputHandler
.onClientSizeChanged(width
, height
);
269 public void attachRedrawCallback() {
270 JniInterface
.provideRedrawCallback(new Runnable() {
278 /** Called when the canvas is first created. */
280 public void surfaceCreated(SurfaceHolder holder
) {
281 synchronized (mRenderData
) {
282 mSurfaceCreated
= true;
287 * Called when the canvas is finally destroyed. Marks the canvas as needing a redraw so that it
288 * will not be blank if the user later switches back to our window.
291 public void surfaceDestroyed(SurfaceHolder holder
) {
292 synchronized (mRenderData
) {
293 mSurfaceCreated
= false;
297 /** Called when a software keyboard is requested, and specifies its options. */
299 public InputConnection
onCreateInputConnection(EditorInfo outAttrs
) {
300 // Disables rich input support and instead requests simple key events.
301 outAttrs
.inputType
= InputType
.TYPE_NULL
;
303 // Prevents most third-party IMEs from ignoring our Activity's adjustResize preference.
304 outAttrs
.imeOptions
|= EditorInfo
.IME_FLAG_NO_FULLSCREEN
;
306 // Ensures that keyboards will not decide to hide the remote desktop on small displays.
307 outAttrs
.imeOptions
|= EditorInfo
.IME_FLAG_NO_EXTRACT_UI
;
309 // Stops software keyboards from closing as soon as the enter key is pressed.
310 outAttrs
.imeOptions
|= EditorInfo
.IME_MASK_ACTION
| EditorInfo
.IME_FLAG_NO_ENTER_ACTION
;
315 /** Called whenever the user attempts to touch the canvas. */
317 public boolean onTouchEvent(MotionEvent event
) {
318 return mInputHandler
.onTouchEvent(event
);
322 public void injectMouseEvent(int x
, int y
, int button
, boolean pressed
) {
323 boolean cursorMoved
= false;
324 synchronized (mRenderData
) {
325 // Test if the cursor actually moved, which requires repainting the cursor. This
326 // requires that the TouchInputHandler doesn't mutate |mRenderData.cursorPosition|
328 if (x
!= mRenderData
.cursorPosition
.x
) {
329 mRenderData
.cursorPosition
.x
= x
;
332 if (y
!= mRenderData
.cursorPosition
.y
) {
333 mRenderData
.cursorPosition
.y
= y
;
338 if (button
== TouchInputHandler
.BUTTON_UNDEFINED
&& !cursorMoved
) {
339 // No need to inject anything or repaint.
343 JniInterface
.sendMouseEvent(x
, y
, button
, pressed
);
345 // TODO(lambroslambrou): Optimize this by only repainting the affected areas.
351 public void injectMouseWheelDeltaEvent(int deltaX
, int deltaY
) {
352 JniInterface
.sendMouseWheelEvent(deltaX
, deltaY
);
356 public void showLongPressFeedback() {
357 mFeedbackAnimator
.startAnimation();
362 public void showActionBar() {
363 mDesktop
.showActionBar();
367 public void showKeyboard() {
368 InputMethodManager inputManager
=
369 (InputMethodManager
) getContext().getSystemService(Context
.INPUT_METHOD_SERVICE
);
370 inputManager
.showSoftInput(this, 0);
374 public void transformationChanged() {
379 public void setAnimationEnabled(boolean enabled
) {
380 synchronized (mAnimationLock
) {
381 if (enabled
&& !mInputAnimationRunning
) {
384 mInputAnimationRunning
= enabled
;