Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / remoting / android / java / src / org / chromium / chromoting / DesktopView.java
blob5151559b0adcf5ef83509fd5827c58db46bf272f
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;
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.
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) {
70 return mRunning;
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.
79 public void startAnimation() {
80 synchronized (mLock) {
81 mRunning = true;
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.
88 float progress;
89 synchronized (mLock) {
90 progress = (SystemClock.uptimeMillis() - mStartTime) / TOTAL_DURATION_MS;
91 if (progress >= 1) {
92 mRunning = false;
93 return;
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) {
137 mDesktop = desktop;
140 /** Request repainting of the desktop view. */
141 void requestRepaint() {
142 synchronized (mRenderData) {
143 if (mRepaintPending) {
144 return;
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();
164 if (image == null) {
165 // This can happen if the client is connected, but a complete video frame has not yet
166 // been decoded.
167 return;
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
177 // changes.
178 mRenderData.imageWidth = width;
179 mRenderData.imageHeight = height;
180 sizeChanged = true;
183 if (sizeChanged) {
184 mInputHandler.onHostSizeChanged(width, height);
187 Canvas canvas;
188 int x, y;
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) {
196 return;
198 canvas = getHolder().lockCanvas();
199 if (canvas == null) {
200 return;
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) {
212 float scaleFactor;
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() {
230 @Override
231 public void run() {
232 processAnimation();
234 }, startTimeMs + 30);
239 private void processAnimation() {
240 boolean running;
241 synchronized (mAnimationLock) {
242 running = mInputAnimationRunning;
244 if (running) {
245 mInputHandler.processAnimation();
247 running |= mFeedbackAnimator.isAnimationRunning();
248 if (running) {
249 requestRepaint();
254 * Called after the canvas is initially created, then after every subsequent resize, as when
255 * the display is rotated.
257 @Override
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);
266 requestRepaint();
269 public void attachRedrawCallback() {
270 JniInterface.provideRedrawCallback(new Runnable() {
271 @Override
272 public void run() {
273 paint();
278 /** Called when the canvas is first created. */
279 @Override
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.
290 @Override
291 public void surfaceDestroyed(SurfaceHolder holder) {
292 synchronized (mRenderData) {
293 mSurfaceCreated = false;
297 /** Called when a software keyboard is requested, and specifies its options. */
298 @Override
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;
312 return null;
315 /** Called whenever the user attempts to touch the canvas. */
316 @Override
317 public boolean onTouchEvent(MotionEvent event) {
318 return mInputHandler.onTouchEvent(event);
321 @Override
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|
327 // directly.
328 if (x != mRenderData.cursorPosition.x) {
329 mRenderData.cursorPosition.x = x;
330 cursorMoved = true;
332 if (y != mRenderData.cursorPosition.y) {
333 mRenderData.cursorPosition.y = y;
334 cursorMoved = true;
338 if (button == TouchInputHandler.BUTTON_UNDEFINED && !cursorMoved) {
339 // No need to inject anything or repaint.
340 return;
343 JniInterface.sendMouseEvent(x, y, button, pressed);
344 if (cursorMoved) {
345 // TODO(lambroslambrou): Optimize this by only repainting the affected areas.
346 requestRepaint();
350 @Override
351 public void injectMouseWheelDeltaEvent(int deltaX, int deltaY) {
352 JniInterface.sendMouseWheelEvent(deltaX, deltaY);
355 @Override
356 public void showLongPressFeedback() {
357 mFeedbackAnimator.startAnimation();
358 requestRepaint();
361 @Override
362 public void showActionBar() {
363 mDesktop.showActionBar();
366 @Override
367 public void showKeyboard() {
368 InputMethodManager inputManager =
369 (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
370 inputManager.showSoftInput(this, 0);
373 @Override
374 public void transformationChanged() {
375 requestRepaint();
378 @Override
379 public void setAnimationEnabled(boolean enabled) {
380 synchronized (mAnimationLock) {
381 if (enabled && !mInputAnimationRunning) {
382 requestRepaint();
384 mInputAnimationRunning = enabled;