summaryrefslogtreecommitdiffstats
path: root/src/com/android/nfc/SendUi.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/nfc/SendUi.java')
-rw-r--r--src/com/android/nfc/SendUi.java878
1 files changed, 878 insertions, 0 deletions
diff --git a/src/com/android/nfc/SendUi.java b/src/com/android/nfc/SendUi.java
new file mode 100644
index 00000000..dec529c4
--- /dev/null
+++ b/src/com/android/nfc/SendUi.java
@@ -0,0 +1,878 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.nfc;
+
+import com.android.internal.policy.PolicyManager;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeAnimator;
+import android.app.ActivityManager;
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.graphics.SurfaceTexture;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.Display;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * This class is responsible for handling the UI animation
+ * around Android Beam. The animation consists of the following
+ * animators:
+ *
+ * mPreAnimator: scales the screenshot down to INTERMEDIATE_SCALE
+ * mSlowSendAnimator: scales the screenshot down to 0.2f (used as a "send in progress" animation)
+ * mFastSendAnimator: quickly scales the screenshot down to 0.0f (used for send success)
+ * mFadeInAnimator: fades the current activity back in (used after mFastSendAnimator completes)
+ * mScaleUpAnimator: scales the screenshot back up to full screen (used for failure or receiving)
+ * mHintAnimator: Slowly turns up the alpha of the "Touch to Beam" hint
+ *
+ * Possible sequences are:
+ *
+ * mPreAnimator => mSlowSendAnimator => mFastSendAnimator => mFadeInAnimator (send success)
+ * mPreAnimator => mSlowSendAnimator => mScaleUpAnimator (send failure)
+ * mPreAnimator => mScaleUpAnimator (p2p link broken, or data received)
+ *
+ * Note that mFastSendAnimator and mFadeInAnimator are combined in a set, as they
+ * are an atomic animation that cannot be interrupted.
+ *
+ * All methods of this class must be called on the UI thread
+ */
+public class SendUi implements Animator.AnimatorListener, View.OnTouchListener,
+ TimeAnimator.TimeListener, TextureView.SurfaceTextureListener, android.view.Window.Callback {
+ static final String TAG = "SendUi";
+
+ static final float INTERMEDIATE_SCALE = 0.6f;
+
+ static final float[] PRE_SCREENSHOT_SCALE = {1.0f, INTERMEDIATE_SCALE};
+ static final int PRE_DURATION_MS = 350;
+
+ static final float[] SEND_SCREENSHOT_SCALE = {INTERMEDIATE_SCALE, 0.2f};
+ static final int SLOW_SEND_DURATION_MS = 8000; // Stretch out sending over 8s
+ static final int FAST_SEND_DURATION_MS = 350;
+
+ static final float[] SCALE_UP_SCREENSHOT_SCALE = {INTERMEDIATE_SCALE, 1.0f};
+ static final int SCALE_UP_DURATION_MS = 300;
+
+ static final int FADE_IN_DURATION_MS = 250;
+ static final int FADE_IN_START_DELAY_MS = 350;
+
+ static final int SLIDE_OUT_DURATION_MS = 300;
+
+ static final float[] BLACK_LAYER_ALPHA_DOWN_RANGE = {0.9f, 0.0f};
+ static final float[] BLACK_LAYER_ALPHA_UP_RANGE = {0.0f, 0.9f};
+
+ static final float[] TEXT_HINT_ALPHA_RANGE = {0.0f, 1.0f};
+ static final int TEXT_HINT_ALPHA_DURATION_MS = 500;
+ static final int TEXT_HINT_ALPHA_START_DELAY_MS = 300;
+
+ static final int FINISH_SCALE_UP = 0;
+ static final int FINISH_SEND_SUCCESS = 1;
+
+ static final int STATE_IDLE = 0;
+ static final int STATE_W4_SCREENSHOT = 1;
+ static final int STATE_W4_SCREENSHOT_PRESEND_REQUESTED = 2;
+ static final int STATE_W4_SCREENSHOT_PRESEND_NFC_TAP_REQUESTED = 3;
+ static final int STATE_W4_SCREENSHOT_THEN_STOP = 4;
+ static final int STATE_W4_PRESEND = 5;
+ static final int STATE_W4_TOUCH = 6;
+ static final int STATE_W4_NFC_TAP = 7;
+ static final int STATE_SENDING = 8;
+ static final int STATE_COMPLETE = 9;
+
+ // all members are only used on UI thread
+ final WindowManager mWindowManager;
+ final Context mContext;
+ final Display mDisplay;
+ final DisplayMetrics mDisplayMetrics;
+ final Matrix mDisplayMatrix;
+ final WindowManager.LayoutParams mWindowLayoutParams;
+ final LayoutInflater mLayoutInflater;
+ final StatusBarManager mStatusBarManager;
+ final View mScreenshotLayout;
+ final ImageView mScreenshotView;
+ final ImageView mBlackLayer;
+ final TextureView mTextureView;
+ final TextView mTextHint;
+ final TextView mTextRetry;
+ final Callback mCallback;
+
+ // The mFrameCounter animation is purely used to count down a certain
+ // number of (vsync'd) frames. This is needed because the first 3
+ // times the animation internally calls eglSwapBuffers(), large buffers
+ // are allocated by the graphics drivers. This causes the animation
+ // to look janky. So on platforms where we can use hardware acceleration,
+ // the animation order is:
+ // Wait for hw surface => start frame counter => start pre-animation after 3 frames
+ // For platforms where no hw acceleration can be used, the pre-animation
+ // is started immediately.
+ final TimeAnimator mFrameCounterAnimator;
+
+ final ObjectAnimator mPreAnimator;
+ final ObjectAnimator mSlowSendAnimator;
+ final ObjectAnimator mFastSendAnimator;
+ final ObjectAnimator mFadeInAnimator;
+ final ObjectAnimator mHintAnimator;
+ final ObjectAnimator mScaleUpAnimator;
+ final ObjectAnimator mAlphaDownAnimator;
+ final ObjectAnimator mAlphaUpAnimator;
+ final AnimatorSet mSuccessAnimatorSet;
+
+ // Besides animating the screenshot, the Beam UI also renders
+ // fireflies on platforms where we can do hardware-acceleration.
+ // Firefly rendering is only started once the initial
+ // "pre-animation" has scaled down the screenshot, to avoid
+ // that animation becoming janky. Likewise, the fireflies are
+ // stopped in their tracks as soon as we finish the animation,
+ // to make the finishing animation smooth.
+ final boolean mHardwareAccelerated;
+ final FireflyRenderer mFireflyRenderer;
+
+ String mToastString;
+ Bitmap mScreenshotBitmap;
+
+ int mState;
+ int mRenderedFrames;
+
+ View mDecor;
+
+ // Used for holding the surface
+ SurfaceTexture mSurface;
+ int mSurfaceWidth;
+ int mSurfaceHeight;
+
+ interface Callback {
+ public void onSendConfirmed();
+ public void onCanceled();
+ }
+
+ public SendUi(Context context, Callback callback) {
+ mContext = context;
+ mCallback = callback;
+
+ mDisplayMetrics = new DisplayMetrics();
+ mDisplayMatrix = new Matrix();
+ mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ mStatusBarManager = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
+
+ mDisplay = mWindowManager.getDefaultDisplay();
+
+ mLayoutInflater = (LayoutInflater)
+ context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mScreenshotLayout = mLayoutInflater.inflate(R.layout.screenshot, null);
+
+ mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.screenshot);
+ mScreenshotLayout.setFocusable(true);
+
+ mTextHint = (TextView) mScreenshotLayout.findViewById(R.id.calltoaction);
+ mTextRetry = (TextView) mScreenshotLayout.findViewById(R.id.retrytext);
+ mBlackLayer = (ImageView) mScreenshotLayout.findViewById(R.id.blacklayer);
+ mTextureView = (TextureView) mScreenshotLayout.findViewById(R.id.fireflies);
+ mTextureView.setSurfaceTextureListener(this);
+
+ // We're only allowed to use hardware acceleration if
+ // isHighEndGfx() returns true - otherwise, we're too limited
+ // on resources to do it.
+ mHardwareAccelerated = ActivityManager.isHighEndGfx();
+ int hwAccelerationFlags = mHardwareAccelerated ?
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED : 0;
+
+ mWindowLayoutParams = new WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
+ WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
+ | hwAccelerationFlags
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
+ PixelFormat.OPAQUE);
+ mWindowLayoutParams.privateFlags |=
+ WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+ mWindowLayoutParams.token = new Binder();
+
+ mFrameCounterAnimator = new TimeAnimator();
+ mFrameCounterAnimator.setTimeListener(this);
+
+ PropertyValuesHolder preX = PropertyValuesHolder.ofFloat("scaleX", PRE_SCREENSHOT_SCALE);
+ PropertyValuesHolder preY = PropertyValuesHolder.ofFloat("scaleY", PRE_SCREENSHOT_SCALE);
+ mPreAnimator = ObjectAnimator.ofPropertyValuesHolder(mScreenshotView, preX, preY);
+ mPreAnimator.setInterpolator(new DecelerateInterpolator());
+ mPreAnimator.setDuration(PRE_DURATION_MS);
+ mPreAnimator.addListener(this);
+
+ PropertyValuesHolder postX = PropertyValuesHolder.ofFloat("scaleX", SEND_SCREENSHOT_SCALE);
+ PropertyValuesHolder postY = PropertyValuesHolder.ofFloat("scaleY", SEND_SCREENSHOT_SCALE);
+ PropertyValuesHolder alphaDown = PropertyValuesHolder.ofFloat("alpha",
+ new float[]{1.0f, 0.0f});
+
+ mSlowSendAnimator = ObjectAnimator.ofPropertyValuesHolder(mScreenshotView, postX, postY);
+ mSlowSendAnimator.setInterpolator(new DecelerateInterpolator());
+ mSlowSendAnimator.setDuration(SLOW_SEND_DURATION_MS);
+
+ mFastSendAnimator = ObjectAnimator.ofPropertyValuesHolder(mScreenshotView, postX,
+ postY, alphaDown);
+ mFastSendAnimator.setInterpolator(new DecelerateInterpolator());
+ mFastSendAnimator.setDuration(FAST_SEND_DURATION_MS);
+ mFastSendAnimator.addListener(this);
+
+ PropertyValuesHolder scaleUpX = PropertyValuesHolder.ofFloat("scaleX", SCALE_UP_SCREENSHOT_SCALE);
+ PropertyValuesHolder scaleUpY = PropertyValuesHolder.ofFloat("scaleY", SCALE_UP_SCREENSHOT_SCALE);
+
+ mScaleUpAnimator = ObjectAnimator.ofPropertyValuesHolder(mScreenshotView, scaleUpX, scaleUpY);
+ mScaleUpAnimator.setInterpolator(new DecelerateInterpolator());
+ mScaleUpAnimator.setDuration(SCALE_UP_DURATION_MS);
+ mScaleUpAnimator.addListener(this);
+
+ PropertyValuesHolder fadeIn = PropertyValuesHolder.ofFloat("alpha", 1.0f);
+ mFadeInAnimator = ObjectAnimator.ofPropertyValuesHolder(mScreenshotView, fadeIn);
+ mFadeInAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
+ mFadeInAnimator.setDuration(FADE_IN_DURATION_MS);
+ mFadeInAnimator.setStartDelay(FADE_IN_START_DELAY_MS);
+ mFadeInAnimator.addListener(this);
+
+ PropertyValuesHolder alphaUp = PropertyValuesHolder.ofFloat("alpha", TEXT_HINT_ALPHA_RANGE);
+ mHintAnimator = ObjectAnimator.ofPropertyValuesHolder(mTextHint, alphaUp);
+ mHintAnimator.setInterpolator(null);
+ mHintAnimator.setDuration(TEXT_HINT_ALPHA_DURATION_MS);
+ mHintAnimator.setStartDelay(TEXT_HINT_ALPHA_START_DELAY_MS);
+
+ alphaDown = PropertyValuesHolder.ofFloat("alpha", BLACK_LAYER_ALPHA_DOWN_RANGE);
+ mAlphaDownAnimator = ObjectAnimator.ofPropertyValuesHolder(mBlackLayer, alphaDown);
+ mAlphaDownAnimator.setInterpolator(new DecelerateInterpolator());
+ mAlphaDownAnimator.setDuration(400);
+
+ alphaUp = PropertyValuesHolder.ofFloat("alpha", BLACK_LAYER_ALPHA_UP_RANGE);
+ mAlphaUpAnimator = ObjectAnimator.ofPropertyValuesHolder(mBlackLayer, alphaUp);
+ mAlphaUpAnimator.setInterpolator(new DecelerateInterpolator());
+ mAlphaUpAnimator.setDuration(200);
+
+ mSuccessAnimatorSet = new AnimatorSet();
+ mSuccessAnimatorSet.playSequentially(mFastSendAnimator, mFadeInAnimator);
+
+ // Create a Window with a Decor view; creating a window allows us to get callbacks
+ // on key events (which require a decor view to be dispatched).
+ mContext.setTheme(android.R.style.Theme_Black_NoTitleBar_Fullscreen);
+ Window window = PolicyManager.makeNewWindow(mContext);
+ window.setCallback(this);
+ window.requestFeature(Window.FEATURE_NO_TITLE);
+ mDecor = window.getDecorView();
+ window.setContentView(mScreenshotLayout, mWindowLayoutParams);
+
+ if (mHardwareAccelerated) {
+ mFireflyRenderer = new FireflyRenderer(context);
+ } else {
+ mFireflyRenderer = null;
+ }
+ mState = STATE_IDLE;
+ }
+
+ public void takeScreenshot() {
+ // There's no point in taking the screenshot if
+ // we're still finishing the previous animation.
+ if (mState >= STATE_W4_TOUCH) {
+ return;
+ }
+ mState = STATE_W4_SCREENSHOT;
+ new ScreenshotTask().execute();
+ }
+
+ /** Show pre-send animation */
+ public void showPreSend(boolean promptToNfcTap) {
+ switch (mState) {
+ case STATE_IDLE:
+ Log.e(TAG, "Unexpected showPreSend() in STATE_IDLE");
+ return;
+ case STATE_W4_SCREENSHOT:
+ // Still waiting for screenshot, store request in state
+ // and wait for screenshot completion.
+ if (promptToNfcTap) {
+ mState = STATE_W4_SCREENSHOT_PRESEND_NFC_TAP_REQUESTED;
+ } else {
+ mState = STATE_W4_SCREENSHOT_PRESEND_REQUESTED;
+ }
+ return;
+ case STATE_W4_SCREENSHOT_PRESEND_REQUESTED:
+ case STATE_W4_SCREENSHOT_PRESEND_NFC_TAP_REQUESTED:
+ Log.e(TAG, "Unexpected showPreSend() in STATE_W4_SCREENSHOT_PRESEND_REQUESTED");
+ return;
+ case STATE_W4_PRESEND:
+ // Expected path, continue below
+ break;
+ default:
+ Log.e(TAG, "Unexpected showPreSend() in state " + Integer.toString(mState));
+ return;
+ }
+ // Update display metrics
+ mDisplay.getRealMetrics(mDisplayMetrics);
+
+ final int statusBarHeight = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_height);
+
+ mBlackLayer.setVisibility(View.GONE);
+ mBlackLayer.setAlpha(0f);
+ mScreenshotLayout.setOnTouchListener(this);
+ mScreenshotView.setImageBitmap(mScreenshotBitmap);
+ mScreenshotView.setTranslationX(0f);
+ mScreenshotView.setAlpha(1.0f);
+ mScreenshotView.setPadding(0, statusBarHeight, 0, 0);
+
+ mScreenshotLayout.requestFocus();
+
+ if (promptToNfcTap) {
+ mTextHint.setText(mContext.getResources().getString(R.string.ask_nfc_tap));
+ } else {
+ mTextHint.setText(mContext.getResources().getString(R.string.touch));
+ }
+ mTextHint.setAlpha(0.0f);
+ mTextHint.setVisibility(View.VISIBLE);
+ mHintAnimator.start();
+
+ // Lock the orientation.
+ // The orientation from the configuration does not specify whether
+ // the orientation is reverse or not (ie landscape or reverse landscape).
+ // So we have to use SENSOR_LANDSCAPE or SENSOR_PORTRAIT to make sure
+ // we lock in portrait / landscape and have the sensor determine
+ // which way is up.
+ int orientation = mContext.getResources().getConfiguration().orientation;
+
+ switch (orientation) {
+ case Configuration.ORIENTATION_LANDSCAPE:
+ mWindowLayoutParams.screenOrientation =
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
+ break;
+ case Configuration.ORIENTATION_PORTRAIT:
+ mWindowLayoutParams.screenOrientation =
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
+ break;
+ default:
+ mWindowLayoutParams.screenOrientation =
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
+ break;
+ }
+
+ mWindowManager.addView(mDecor, mWindowLayoutParams);
+ // Disable statusbar pull-down
+ mStatusBarManager.disable(StatusBarManager.DISABLE_EXPAND);
+
+ mToastString = null;
+
+ if (!mHardwareAccelerated) {
+ mPreAnimator.start();
+ } // else, we will start the animation once we get the hardware surface
+ mState = promptToNfcTap ? STATE_W4_NFC_TAP : STATE_W4_TOUCH;
+ }
+
+ /** Show starting send animation */
+ public void showStartSend() {
+ if (mState < STATE_SENDING) return;
+
+ mTextRetry.setVisibility(View.GONE);
+ // Update the starting scale - touchscreen-mashers may trigger
+ // this before the pre-animation completes.
+ float currentScale = mScreenshotView.getScaleX();
+ PropertyValuesHolder postX = PropertyValuesHolder.ofFloat("scaleX",
+ new float[] {currentScale, 0.0f});
+ PropertyValuesHolder postY = PropertyValuesHolder.ofFloat("scaleY",
+ new float[] {currentScale, 0.0f});
+
+ mSlowSendAnimator.setValues(postX, postY);
+
+ float currentAlpha = mBlackLayer.getAlpha();
+ if (mBlackLayer.isShown() && currentAlpha > 0.0f) {
+ PropertyValuesHolder alphaDown = PropertyValuesHolder.ofFloat("alpha",
+ new float[] {currentAlpha, 0.0f});
+ mAlphaDownAnimator.setValues(alphaDown);
+ mAlphaDownAnimator.start();
+ }
+ mSlowSendAnimator.start();
+ }
+
+ public void finishAndToast(int finishMode, String toast) {
+ mToastString = toast;
+
+ finish(finishMode);
+ }
+
+ /** Return to initial state */
+ public void finish(int finishMode) {
+ switch (mState) {
+ case STATE_IDLE:
+ return;
+ case STATE_W4_SCREENSHOT:
+ case STATE_W4_SCREENSHOT_PRESEND_REQUESTED:
+ case STATE_W4_SCREENSHOT_PRESEND_NFC_TAP_REQUESTED:
+ // Screenshot is still being captured on a separate thread.
+ // Update state, and stop everything when the capture is done.
+ mState = STATE_W4_SCREENSHOT_THEN_STOP;
+ return;
+ case STATE_W4_SCREENSHOT_THEN_STOP:
+ Log.e(TAG, "Unexpected call to finish() in STATE_W4_SCREENSHOT_THEN_STOP");
+ return;
+ case STATE_W4_PRESEND:
+ // We didn't build up any animation state yet, but
+ // did store the bitmap. Clear out the bitmap, reset
+ // state and bail.
+ mScreenshotBitmap = null;
+ mState = STATE_IDLE;
+ return;
+ default:
+ // We've started animations and attached a view; tear stuff down below.
+ break;
+ }
+
+ // Stop rendering the fireflies
+ if (mFireflyRenderer != null) {
+ mFireflyRenderer.stop();
+ }
+
+ mTextHint.setVisibility(View.GONE);
+ mTextRetry.setVisibility(View.GONE);
+
+ float currentScale = mScreenshotView.getScaleX();
+ float currentAlpha = mScreenshotView.getAlpha();
+
+ if (finishMode == FINISH_SCALE_UP) {
+ mBlackLayer.setVisibility(View.GONE);
+ PropertyValuesHolder scaleUpX = PropertyValuesHolder.ofFloat("scaleX",
+ new float[] {currentScale, 1.0f});
+ PropertyValuesHolder scaleUpY = PropertyValuesHolder.ofFloat("scaleY",
+ new float[] {currentScale, 1.0f});
+ PropertyValuesHolder scaleUpAlpha = PropertyValuesHolder.ofFloat("alpha",
+ new float[] {currentAlpha, 1.0f});
+ mScaleUpAnimator.setValues(scaleUpX, scaleUpY, scaleUpAlpha);
+
+ mScaleUpAnimator.start();
+ } else if (finishMode == FINISH_SEND_SUCCESS){
+ // Modify the fast send parameters to match the current scale
+ PropertyValuesHolder postX = PropertyValuesHolder.ofFloat("scaleX",
+ new float[] {currentScale, 0.0f});
+ PropertyValuesHolder postY = PropertyValuesHolder.ofFloat("scaleY",
+ new float[] {currentScale, 0.0f});
+ PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha",
+ new float[] {currentAlpha, 0.0f});
+ mFastSendAnimator.setValues(postX, postY, alpha);
+
+ // Reset the fadeIn parameters to start from alpha 1
+ PropertyValuesHolder fadeIn = PropertyValuesHolder.ofFloat("alpha",
+ new float[] {0.0f, 1.0f});
+ mFadeInAnimator.setValues(fadeIn);
+
+ mSlowSendAnimator.cancel();
+ mSuccessAnimatorSet.start();
+ }
+ mState = STATE_COMPLETE;
+ }
+
+ void dismiss() {
+ if (mState < STATE_W4_TOUCH) return;
+ // Immediately set to IDLE, to prevent .cancel() calls
+ // below from immediately calling into dismiss() again.
+ // (They can do so on the same thread).
+ mState = STATE_IDLE;
+ mSurface = null;
+ mFrameCounterAnimator.cancel();
+ mPreAnimator.cancel();
+ mSlowSendAnimator.cancel();
+ mFastSendAnimator.cancel();
+ mSuccessAnimatorSet.cancel();
+ mScaleUpAnimator.cancel();
+ mAlphaUpAnimator.cancel();
+ mAlphaDownAnimator.cancel();
+ mWindowManager.removeView(mDecor);
+ mStatusBarManager.disable(StatusBarManager.DISABLE_NONE);
+ mScreenshotBitmap = null;
+ if (mToastString != null) {
+ Toast.makeText(mContext, mToastString, Toast.LENGTH_LONG).show();
+ }
+ mToastString = null;
+ }
+
+ /**
+ * @return the current display rotation in degrees
+ */
+ static float getDegreesForRotation(int value) {
+ switch (value) {
+ case Surface.ROTATION_90:
+ return 90f;
+ case Surface.ROTATION_180:
+ return 180f;
+ case Surface.ROTATION_270:
+ return 270f;
+ }
+ return 0f;
+ }
+
+ final class ScreenshotTask extends AsyncTask<Void, Void, Bitmap> {
+ @Override
+ protected Bitmap doInBackground(Void... params) {
+ return createScreenshot();
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ if (mState == STATE_W4_SCREENSHOT) {
+ // Screenshot done, wait for request to start preSend anim
+ mState = STATE_W4_PRESEND;
+ } else if (mState == STATE_W4_SCREENSHOT_THEN_STOP) {
+ // We were asked to finish, move to idle state and exit
+ mState = STATE_IDLE;
+ } else if (mState == STATE_W4_SCREENSHOT_PRESEND_REQUESTED ||
+ mState == STATE_W4_SCREENSHOT_PRESEND_NFC_TAP_REQUESTED) {
+ if (result != null) {
+ mScreenshotBitmap = result;
+ boolean requestTap = (mState == STATE_W4_SCREENSHOT_PRESEND_NFC_TAP_REQUESTED);
+ mState = STATE_W4_PRESEND;
+ showPreSend(requestTap);
+ } else {
+ // Failed to take screenshot; reset state to idle
+ // and don't do anything
+ Log.e(TAG, "Failed to create screenshot");
+ mState = STATE_IDLE;
+ }
+ } else {
+ Log.e(TAG, "Invalid state on screenshot completion: " + Integer.toString(mState));
+ }
+ }
+ };
+
+ /**
+ * Returns a screenshot of the current display contents.
+ */
+ Bitmap createScreenshot() {
+ // We need to orient the screenshot correctly (and the Surface api seems to
+ // take screenshots only in the natural orientation of the device :!)
+
+ mDisplay.getRealMetrics(mDisplayMetrics);
+ boolean hasNavBar = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_showNavigationBar);
+
+ float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
+ float degrees = getDegreesForRotation(mDisplay.getRotation());
+ final int statusBarHeight = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.status_bar_height);
+
+ // Navbar has different sizes, depending on orientation
+ final int navBarHeight = hasNavBar ? mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.navigation_bar_height) : 0;
+ final int navBarHeightLandscape = hasNavBar ? mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.navigation_bar_height_landscape) : 0;
+
+ final int navBarWidth = hasNavBar ? mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.navigation_bar_width) : 0;
+
+ boolean requiresRotation = (degrees > 0);
+ if (requiresRotation) {
+ // Get the dimensions of the device in its native orientation
+ mDisplayMatrix.reset();
+ mDisplayMatrix.preRotate(-degrees);
+ mDisplayMatrix.mapPoints(dims);
+ dims[0] = Math.abs(dims[0]);
+ dims[1] = Math.abs(dims[1]);
+ }
+
+ Bitmap bitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
+ // Bail if we couldn't take the screenshot
+ if (bitmap == null) {
+ return null;
+ }
+
+ if (requiresRotation) {
+ // Rotate the screenshot to the current orientation
+ Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
+ mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(ss);
+ c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
+ c.rotate(360f - degrees);
+ c.translate(-dims[0] / 2, -dims[1] / 2);
+ c.drawBitmap(bitmap, 0, 0, null);
+
+ bitmap = ss;
+ }
+
+ // TODO this is somewhat device-specific; need generic solution.
+ // Crop off the status bar and the nav bar
+ // Portrait: 0, statusBarHeight, width, height - status - nav
+ // Landscape: 0, statusBarHeight, width - navBar, height - status
+ int newLeft = 0;
+ int newTop = statusBarHeight;
+ int newWidth = bitmap.getWidth();
+ int newHeight = bitmap.getHeight();
+ float smallestWidth = (float)Math.min(newWidth, newHeight);
+ float smallestWidthDp = smallestWidth / (mDisplayMetrics.densityDpi / 160f);
+ if (bitmap.getWidth() < bitmap.getHeight()) {
+ // Portrait mode: status bar is at the top, navbar bottom, width unchanged
+ newHeight = bitmap.getHeight() - statusBarHeight - navBarHeight;
+ } else {
+ // Landscape mode: status bar is at the top
+ // Navbar: bottom on >599dp width devices, otherwise to the side
+ if (smallestWidthDp > 599) {
+ newHeight = bitmap.getHeight() - statusBarHeight - navBarHeightLandscape;
+ } else {
+ newHeight = bitmap.getHeight() - statusBarHeight;
+ newWidth = bitmap.getWidth() - navBarWidth;
+ }
+ }
+ bitmap = Bitmap.createBitmap(bitmap, newLeft, newTop, newWidth, newHeight);
+
+ return bitmap;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) { }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (animation == mScaleUpAnimator || animation == mSuccessAnimatorSet ||
+ animation == mFadeInAnimator) {
+ // These all indicate the end of the animation
+ dismiss();
+ } else if (animation == mFastSendAnimator) {
+ // After sending is done and we've faded out, reset the scale to 1
+ // so we can fade it back in.
+ mScreenshotView.setScaleX(1.0f);
+ mScreenshotView.setScaleY(1.0f);
+ } else if (animation == mPreAnimator) {
+ if (mHardwareAccelerated && (mState == STATE_W4_TOUCH || mState == STATE_W4_NFC_TAP)) {
+ mFireflyRenderer.start(mSurface, mSurfaceWidth, mSurfaceHeight);
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) { }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) { }
+
+ @Override
+ public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
+ // This gets called on animation vsync
+ if (++mRenderedFrames < 4) {
+ // For the first 3 frames, call invalidate(); this calls eglSwapBuffers
+ // on the surface, which will allocate large buffers the first three calls
+ // as Android uses triple buffering.
+ mScreenshotLayout.invalidate();
+ } else {
+ // Buffers should be allocated, start the real animation
+ mFrameCounterAnimator.cancel();
+ mPreAnimator.start();
+ }
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mState != STATE_W4_TOUCH) {
+ return false;
+ }
+ mState = STATE_SENDING;
+ // Ignore future touches
+ mScreenshotView.setOnTouchListener(null);
+
+ // Cancel any ongoing animations
+ mFrameCounterAnimator.cancel();
+ mPreAnimator.cancel();
+
+ mCallback.onSendConfirmed();
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ if (mHardwareAccelerated && mState < STATE_COMPLETE) {
+ mRenderedFrames = 0;
+
+ mFrameCounterAnimator.start();
+ mSurface = surface;
+ mSurfaceWidth = width;
+ mSurfaceHeight = height;
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ // Since we've disabled orientation changes, we can safely ignore this
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ mSurface = null;
+
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) { }
+
+ public void showSendHint() {
+ if (mAlphaDownAnimator.isRunning()) {
+ mAlphaDownAnimator.cancel();
+ }
+ if (mSlowSendAnimator.isRunning()) {
+ mSlowSendAnimator.cancel();
+ }
+ mBlackLayer.setScaleX(mScreenshotView.getScaleX());
+ mBlackLayer.setScaleY(mScreenshotView.getScaleY());
+ mBlackLayer.setVisibility(View.VISIBLE);
+ mTextHint.setVisibility(View.GONE);
+
+ mTextRetry.setText(mContext.getResources().getString(R.string.beam_try_again));
+ mTextRetry.setVisibility(View.VISIBLE);
+
+ PropertyValuesHolder alphaUp = PropertyValuesHolder.ofFloat("alpha",
+ new float[] {mBlackLayer.getAlpha(), 0.9f});
+ mAlphaUpAnimator.setValues(alphaUp);
+ mAlphaUpAnimator.start();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ mCallback.onCanceled();
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
+ keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ // Treat as if it's a touch event
+ return onTouch(mScreenshotView, null);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ return mScreenshotLayout.dispatchTouchEvent(event);
+ }
+
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ return false;
+ }
+
+ @Override
+ public View onCreatePanelView(int featureId) {
+ return null;
+ }
+
+ @Override
+ public boolean onCreatePanelMenu(int featureId, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onPreparePanel(int featureId, View view, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public void onWindowAttributesChanged(LayoutParams attrs) {
+ }
+
+ @Override
+ public void onContentChanged() {
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ }
+
+ @Override
+ public void onPanelClosed(int featureId, Menu menu) {
+
+ }
+
+ @Override
+ public boolean onSearchRequested() {
+ return false;
+ }
+
+ @Override
+ public ActionMode onWindowStartingActionMode(
+ android.view.ActionMode.Callback callback) {
+ return null;
+ }
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ }
+
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+ }
+}