| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.chromoting; |
| |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Point; |
| import android.graphics.RadialGradient; |
| import android.graphics.Shader; |
| import android.os.Looper; |
| import android.os.SystemClock; |
| import android.text.InputType; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.SurfaceHolder; |
| import android.view.SurfaceView; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputConnection; |
| import android.view.inputmethod.InputMethodManager; |
| |
| import org.chromium.chromoting.jni.JniInterface; |
| |
| /** |
| * The user interface for viewing and interacting with a specific remote host. |
| * It provides a canvas onto which the video feed is rendered, handles |
| * multitouch pan and zoom gestures, and collects and forwards input events. |
| */ |
| /** GUI element that holds the drawing canvas. */ |
| public class DesktopView extends SurfaceView implements DesktopViewInterface, |
| SurfaceHolder.Callback { |
| private RenderData mRenderData; |
| private TouchInputHandler mInputHandler; |
| |
| /** The parent Desktop activity. */ |
| private Desktop mDesktop; |
| |
| // Flag to prevent multiple repaint requests from being backed up. Requests for repainting will |
| // be dropped if this is already set to true. This is used by the main thread and the painting |
| // thread, so the access should be synchronized on |mRenderData|. |
| private boolean mRepaintPending; |
| |
| // Flag used to ensure that the SurfaceView is only painted between calls to surfaceCreated() |
| // and surfaceDestroyed(). Accessed on main thread and display thread, so this should be |
| // synchronized on |mRenderData|. |
| private boolean mSurfaceCreated = false; |
| |
| /** Helper class for displaying the long-press feedback animation. This class is thread-safe. */ |
| private static class FeedbackAnimator { |
| /** Total duration of the animation, in milliseconds. */ |
| private static final float TOTAL_DURATION_MS = 220; |
| |
| /** Start time of the animation, from {@link SystemClock#uptimeMillis()}. */ |
| private long mStartTime = 0; |
| |
| private boolean mRunning = false; |
| |
| /** Lock to allow multithreaded access to {@link #mStartTime} and {@link #mRunning}. */ |
| private Object mLock = new Object(); |
| |
| private Paint mPaint = new Paint(); |
| |
| public boolean isAnimationRunning() { |
| synchronized (mLock) { |
| return mRunning; |
| } |
| } |
| |
| /** |
| * Begins a new animation sequence. After calling this method, the caller should |
| * call {@link #render(Canvas, float, float, float)} periodically whilst |
| * {@link #isAnimationRunning()} returns true. |
| */ |
| public void startAnimation() { |
| synchronized (mLock) { |
| mRunning = true; |
| mStartTime = SystemClock.uptimeMillis(); |
| } |
| } |
| |
| public void render(Canvas canvas, float x, float y, float size) { |
| // |progress| is 0 at the beginning, 1 at the end. |
| float progress; |
| synchronized (mLock) { |
| progress = (SystemClock.uptimeMillis() - mStartTime) / TOTAL_DURATION_MS; |
| if (progress >= 1) { |
| mRunning = false; |
| return; |
| } |
| } |
| |
| // Animation grows from 0 to |size|, and goes from fully opaque to transparent for a |
| // seamless fading-out effect. The animation needs to have more than one color so it's |
| // visible over any background color. |
| float radius = size * progress; |
| int alpha = (int)((1 - progress) * 0xff); |
| |
| int transparentBlack = Color.argb(0, 0, 0, 0); |
| int white = Color.argb(alpha, 0xff, 0xff, 0xff); |
| int black = Color.argb(alpha, 0, 0, 0); |
| mPaint.setShader(new RadialGradient(x, y, radius, |
| new int[] {transparentBlack, white, black, transparentBlack}, |
| new float[] {0.0f, 0.8f, 0.9f, 1.0f}, Shader.TileMode.CLAMP)); |
| canvas.drawCircle(x, y, radius, mPaint); |
| } |
| } |
| |
| private FeedbackAnimator mFeedbackAnimator = new FeedbackAnimator(); |
| |
| // Variables to control animation by the TouchInputHandler. |
| |
| /** Protects mInputAnimationRunning. */ |
| private Object mAnimationLock = new Object(); |
| |
| /** Whether the TouchInputHandler has requested animation to be performed. */ |
| private boolean mInputAnimationRunning = false; |
| |
| public DesktopView(Context context, AttributeSet attributes) { |
| super(context, attributes); |
| |
| // Give this view keyboard focus, allowing us to customize the soft keyboard's settings. |
| setFocusableInTouchMode(true); |
| |
| mRenderData = new RenderData(); |
| mInputHandler = new TrackingInputHandler(this, context, mRenderData); |
| mRepaintPending = false; |
| |
| getHolder().addCallback(this); |
| } |
| |
| public void setDesktop(Desktop desktop) { |
| mDesktop = desktop; |
| } |
| |
| /** Request repainting of the desktop view. */ |
| void requestRepaint() { |
| synchronized (mRenderData) { |
| if (mRepaintPending) { |
| return; |
| } |
| mRepaintPending = true; |
| } |
| JniInterface.redrawGraphics(); |
| } |
| |
| /** Called whenever the screen configuration is changed. */ |
| public void onScreenConfigurationChanged() { |
| mInputHandler.onScreenConfigurationChanged(); |
| } |
| |
| /** |
| * Redraws the canvas. This should be done on a non-UI thread or it could |
| * cause the UI to lag. Specifically, it is currently invoked on the native |
| * graphics thread using a JNI. |
| */ |
| public void paint() { |
| long startTimeMs = SystemClock.uptimeMillis(); |
| |
| if (Looper.myLooper() == Looper.getMainLooper()) { |
| Log.w("deskview", "Canvas being redrawn on UI thread"); |
| } |
| |
| Bitmap image = JniInterface.getVideoFrame(); |
| if (image == null) { |
| // This can happen if the client is connected, but a complete video frame has not yet |
| // been decoded. |
| return; |
| } |
| |
| int width = image.getWidth(); |
| int height = image.getHeight(); |
| boolean sizeChanged = false; |
| synchronized (mRenderData) { |
| if (mRenderData.imageWidth != width || mRenderData.imageHeight != height) { |
| // TODO(lambroslambrou): Move this code into a sizeChanged() callback, to be |
| // triggered from JniInterface (on the display thread) when the remote screen size |
| // changes. |
| mRenderData.imageWidth = width; |
| mRenderData.imageHeight = height; |
| sizeChanged = true; |
| } |
| } |
| if (sizeChanged) { |
| mInputHandler.onHostSizeChanged(width, height); |
| } |
| |
| Canvas canvas; |
| int x, y; |
| synchronized (mRenderData) { |
| mRepaintPending = false; |
| // Don't try to lock the canvas before it is ready, as the implementation of |
| // lockCanvas() may throttle these calls to a slow rate in order to avoid consuming CPU. |
| // Note that a successful call to lockCanvas() will prevent the framework from |
| // destroying the Surface until it is unlocked. |
| if (!mSurfaceCreated) { |
| return; |
| } |
| canvas = getHolder().lockCanvas(); |
| if (canvas == null) { |
| return; |
| } |
| canvas.setMatrix(mRenderData.transform); |
| x = mRenderData.cursorPosition.x; |
| y = mRenderData.cursorPosition.y; |
| } |
| |
| canvas.drawColor(Color.BLACK); |
| canvas.drawBitmap(image, 0, 0, new Paint()); |
| |
| boolean feedbackAnimationRunning = mFeedbackAnimator.isAnimationRunning(); |
| if (feedbackAnimationRunning) { |
| float scaleFactor; |
| synchronized (mRenderData) { |
| scaleFactor = mRenderData.transform.mapRadius(1); |
| } |
| mFeedbackAnimator.render(canvas, x, y, 40 / scaleFactor); |
| } |
| |
| Bitmap cursorBitmap = JniInterface.getCursorBitmap(); |
| if (cursorBitmap != null) { |
| Point hotspot = JniInterface.getCursorHotspot(); |
| canvas.drawBitmap(cursorBitmap, x - hotspot.x, y - hotspot.y, new Paint()); |
| } |
| |
| getHolder().unlockCanvasAndPost(canvas); |
| |
| synchronized (mAnimationLock) { |
| if (mInputAnimationRunning || feedbackAnimationRunning) { |
| getHandler().postAtTime(new Runnable() { |
| @Override |
| public void run() { |
| processAnimation(); |
| } |
| }, startTimeMs + 30); |
| } |
| }; |
| } |
| |
| private void processAnimation() { |
| boolean running; |
| synchronized (mAnimationLock) { |
| running = mInputAnimationRunning; |
| } |
| if (running) { |
| mInputHandler.processAnimation(); |
| } |
| running |= mFeedbackAnimator.isAnimationRunning(); |
| if (running) { |
| requestRepaint(); |
| } |
| } |
| |
| /** |
| * Called after the canvas is initially created, then after every subsequent resize, as when |
| * the display is rotated. |
| */ |
| @Override |
| public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { |
| synchronized (mRenderData) { |
| mRenderData.screenWidth = width; |
| mRenderData.screenHeight = height; |
| } |
| |
| JniInterface.provideRedrawCallback(new Runnable() { |
| @Override |
| public void run() { |
| paint(); |
| } |
| }); |
| mInputHandler.onClientSizeChanged(width, height); |
| requestRepaint(); |
| } |
| |
| /** Called when the canvas is first created. */ |
| @Override |
| public void surfaceCreated(SurfaceHolder holder) { |
| synchronized (mRenderData) { |
| mSurfaceCreated = true; |
| } |
| } |
| |
| /** |
| * Called when the canvas is finally destroyed. Marks the canvas as needing a redraw so that it |
| * will not be blank if the user later switches back to our window. |
| */ |
| @Override |
| public void surfaceDestroyed(SurfaceHolder holder) { |
| // Stop this canvas from being redrawn. |
| JniInterface.provideRedrawCallback(null); |
| |
| synchronized (mRenderData) { |
| mSurfaceCreated = false; |
| } |
| } |
| |
| /** Called when a software keyboard is requested, and specifies its options. */ |
| @Override |
| public InputConnection onCreateInputConnection(EditorInfo outAttrs) { |
| // Disables rich input support and instead requests simple key events. |
| outAttrs.inputType = InputType.TYPE_NULL; |
| |
| // Prevents most third-party IMEs from ignoring our Activity's adjustResize preference. |
| outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN; |
| |
| // Ensures that keyboards will not decide to hide the remote desktop on small displays. |
| outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI; |
| |
| // Stops software keyboards from closing as soon as the enter key is pressed. |
| outAttrs.imeOptions |= EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION; |
| |
| return null; |
| } |
| |
| /** Called whenever the user attempts to touch the canvas. */ |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| return mInputHandler.onTouchEvent(event); |
| } |
| |
| @Override |
| public void injectMouseEvent(int x, int y, int button, boolean pressed) { |
| boolean cursorMoved = false; |
| synchronized (mRenderData) { |
| // Test if the cursor actually moved, which requires repainting the cursor. This |
| // requires that the TouchInputHandler doesn't mutate |mRenderData.cursorPosition| |
| // directly. |
| if (x != mRenderData.cursorPosition.x) { |
| mRenderData.cursorPosition.x = x; |
| cursorMoved = true; |
| } |
| if (y != mRenderData.cursorPosition.y) { |
| mRenderData.cursorPosition.y = y; |
| cursorMoved = true; |
| } |
| } |
| |
| if (button == TouchInputHandler.BUTTON_UNDEFINED && !cursorMoved) { |
| // No need to inject anything or repaint. |
| return; |
| } |
| |
| JniInterface.sendMouseEvent(x, y, button, pressed); |
| if (cursorMoved) { |
| // TODO(lambroslambrou): Optimize this by only repainting the affected areas. |
| requestRepaint(); |
| } |
| } |
| |
| @Override |
| public void injectMouseWheelDeltaEvent(int deltaX, int deltaY) { |
| JniInterface.sendMouseWheelEvent(deltaX, deltaY); |
| } |
| |
| @Override |
| public void showLongPressFeedback() { |
| mFeedbackAnimator.startAnimation(); |
| requestRepaint(); |
| } |
| |
| @Override |
| public void showActionBar() { |
| mDesktop.showActionBar(); |
| } |
| |
| @Override |
| public void showKeyboard() { |
| InputMethodManager inputManager = |
| (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); |
| inputManager.showSoftInput(this, 0); |
| } |
| |
| @Override |
| public void transformationChanged() { |
| requestRepaint(); |
| } |
| |
| @Override |
| public void setAnimationEnabled(boolean enabled) { |
| synchronized (mAnimationLock) { |
| if (enabled && !mInputAnimationRunning) { |
| requestRepaint(); |
| } |
| mInputAnimationRunning = enabled; |
| } |
| } |
| } |