blob: 49f893c20c48f4837479b8238ad790d96d74319d [file] [log] [blame]
/*
* 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.beam;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import com.android.nfc.R;
import com.android.nfc.beam.FireflyRenderer;
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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
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.Rect;
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.KeyboardShortcutGroup;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import com.android.internal.policy.PhoneWindow;
import android.view.SearchEvent;
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;
import java.util.List;
/**
* 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;
public static final int FINISH_SCALE_UP = 0;
public 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;
public 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.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
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 = new PhoneWindow(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();
final IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
mContext.registerReceiver(mReceiver, filter);
}
/** 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.tap_to_beam));
}
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;
mContext.unregisterReceiver(mReceiver);
if (mToastString != null) {
Toast toast = Toast.makeText(mContext, mToastString, Toast.LENGTH_LONG);
toast.getWindowParams().privateFlags |= LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
toast.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
mScreenshotBitmap = result;
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) {
mScreenshotBitmap = result;
boolean requestTap = (mState == STATE_W4_SCREENSHOT_PRESEND_NFC_TAP_REQUESTED);
mState = STATE_W4_PRESEND;
showPreSend(requestTap);
} else {
Log.e(TAG, "Invalid state on screenshot completion: " + Integer.toString(mState));
}
}
};
/**
* Returns a screenshot of the current display contents.
*/
Bitmap createScreenshot() {
boolean hasNavBar = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_showNavigationBar);
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;
mDisplay.getRealMetrics(mDisplayMetrics);
float smallestWidth = (float)Math.min(mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels);
float smallestWidthDp = smallestWidth / (mDisplayMetrics.densityDpi / 160f);
int rot = mDisplay.getRotation();
// TODO this is somewhat device-specific; need generic solution.
// The starting crop for the screenshot is the fullscreen without the status bar, which
// is always on top. The conditional check will determine how to crop the navbar,
// depending on orienation and screen size.
Rect crop = new Rect(0, statusBarHeight, mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels);
if (mDisplayMetrics.widthPixels < mDisplayMetrics.heightPixels) {
// Portrait mode: crop the navbar out from the bottom, width unchanged
crop.bottom -= navBarHeight;
} else {
// Landscape mode:
if (smallestWidthDp > 599) {
// Navbar on bottom on >599dp width devices, so crop navbar out from the bottom.
crop.bottom -= navBarHeightLandscape;
} else {
// Navbar on right, so crop navbar out from right of screen.
crop.right -= navBarWidth;
}
}
int width = crop.width();
int height = crop.height();
// Take the screenshot. SurfaceControl will generate a hardware bitmap in the correct
// orientation and size.
Bitmap bitmap = SurfaceControl.screenshot(crop, width, height, rot);
// Bail if we couldn't take the screenshot
if (bitmap == null) {
return null;
}
// Convert to a software bitmap so it can be set in an ImageView.
Bitmap swBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
return swBitmap;
}
@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(SearchEvent searchEvent) {
return onSearchRequested();
}
@Override
public boolean onSearchRequested() {
return false;
}
@Override
public ActionMode onWindowStartingActionMode(
android.view.ActionMode.Callback callback) {
return null;
}
public ActionMode onWindowStartingActionMode(
android.view.ActionMode.Callback callback, int type) {
return null;
}
@Override
public void onActionModeStarted(ActionMode mode) {
}
@Override
public void onActionModeFinished(ActionMode mode) {
}
public boolean isSendUiInIdleState() {
return mState == STATE_IDLE;
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
mCallback.onCanceled();
}
}
};
}