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 } 392