/* * 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 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.Binder; import android.util.DisplayMetrics; import android.view.Display; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; 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 { 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[] 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; // 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 TextureView mTextureView; final TextView mTextHint; 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 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; boolean mAttached; boolean mSending; int mRenderedFrames; // Used for holding the surface SurfaceTexture mSurface; int mSurfaceWidth; int mSurfaceHeight; interface Callback { public void onSendConfirmed(); } 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); 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); mSuccessAnimatorSet = new AnimatorSet(); mSuccessAnimatorSet.playSequentially(mFastSendAnimator, mFadeInAnimator); if (mHardwareAccelerated) { mFireflyRenderer = new FireflyRenderer(context); } else { mFireflyRenderer = null; } mAttached = false; } public void takeScreenshot() { mScreenshotBitmap = createScreenshot(); } /** Show pre-send animation */ public void showPreSend() { // Update display metrics mDisplay.getRealMetrics(mDisplayMetrics); final int statusBarHeight = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_height); if (mScreenshotBitmap == null || mAttached) { return; } mScreenshotView.setOnTouchListener(this); mScreenshotView.setImageBitmap(mScreenshotBitmap); mScreenshotView.setTranslationX(0f); mScreenshotView.setAlpha(1.0f); mScreenshotView.setPadding(0, statusBarHeight, 0, 0); mScreenshotLayout.requestFocus(); 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(mScreenshotLayout, mWindowLayoutParams); // Disable statusbar pull-down mStatusBarManager.disable(StatusBarManager.DISABLE_EXPAND); mToastString = null; mSending = false; mAttached = true; if (!mHardwareAccelerated) { mPreAnimator.start(); } // else, we will start the animation once we get the hardware surface } /** Show starting send animation */ public void showStartSend() { if (!mAttached) return; // 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); mSlowSendAnimator.start(); } public void finishAndToast(int finishMode, String toast) { if (!mAttached) return; mToastString = toast; finish(finishMode); } /** Return to initial state */ public void finish(int finishMode) { if (!mAttached) return; // Stop rendering the fireflies if (mFireflyRenderer != null) { mFireflyRenderer.stop(); } mTextHint.setVisibility(View.GONE); float currentScale = mScreenshotView.getScaleX(); float currentAlpha = mScreenshotView.getAlpha(); if (finishMode == FINISH_SCALE_UP) { 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[] {1.0f, 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(); } } public void dismiss() { if (!mAttached) return; // Immediately set to false, to prevent .cancel() calls // below from immediately calling into dismiss() again. mAttached = false; mSurface = null; mFrameCounterAnimator.cancel(); mPreAnimator.cancel(); mSlowSendAnimator.cancel(); mFastSendAnimator.cancel(); mSuccessAnimatorSet.cancel(); mScaleUpAnimator.cancel(); mWindowManager.removeView(mScreenshotLayout); mStatusBarManager.disable(StatusBarManager.DISABLE_NONE); releaseScreenshot(); if (mToastString != null) { Toast.makeText(mContext, mToastString, Toast.LENGTH_LONG).show(); } mToastString = null; } public void releaseScreenshot() { mScreenshotBitmap = 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; } /** * 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 = Surface.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 && mAttached && !mSending) { 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 (!mAttached) { return false; } mSending = true; // 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 && !mSending) { 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) { } }