Merge "Make Beam more forgiving." into jb-mr2-dev
diff --git a/res/layout/screenshot.xml b/res/layout/screenshot.xml
index 24a3a71..2523e50 100644
--- a/res/layout/screenshot.xml
+++ b/res/layout/screenshot.xml
@@ -49,5 +49,36 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:gravity="center"
-    />
+        />
+    <ImageView android:id="@+id/blacklayer"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="#ff000000"
+        android:visibility="gone"
+        />
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+    >
+        <View
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_weight="0.25"
+            />
+        <TextView android:id="@+id/retrytext"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0.5"
+            android:gravity="center"
+            android:textAppearance="?android:attr/textAppearanceLarge"
+            android:background="@null"
+            android:textColor="@android:color/holo_blue_light"
+        />
+        <View
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_weight="0.25"
+            />
+    </LinearLayout>
 </FrameLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a4bda3b..b944937 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -27,6 +27,7 @@
     <string name="cancel">Cancel</string>
     <string name="beam_touch_to_view">Touch to view</string>
     <string name="beam_handover_not_supported">The receiver\'s device doesn\'t support large file transfer via beam.</string>
+    <string name="beam_try_again">Bring devices together again</string>
 
     <string name="connecting_headset">Connecting</string>
     <string name="connected_headset">Connected</string>
diff --git a/src/com/android/nfc/P2pEventManager.java b/src/com/android/nfc/P2pEventManager.java
index 68f479b..65e428e 100644
--- a/src/com/android/nfc/P2pEventManager.java
+++ b/src/com/android/nfc/P2pEventManager.java
@@ -144,4 +144,17 @@
         mSending = true;
 
     }
+
+    @Override
+    public void onP2pSendDebounce() {
+        mNfcService.playSound(NfcService.SOUND_ERROR);
+        mSendUi.showSendHint();
+    }
+
+    @Override
+    public void onP2pResumeSend() {
+        mVibrator.vibrate(VIBRATION_PATTERN, -1);
+        mNfcService.playSound(NfcService.SOUND_START);
+        mSendUi.showStartSend();
+    }
 }
diff --git a/src/com/android/nfc/P2pLinkManager.java b/src/com/android/nfc/P2pLinkManager.java
index 0b3fc73..b25b41d 100755
--- a/src/com/android/nfc/P2pLinkManager.java
+++ b/src/com/android/nfc/P2pLinkManager.java
@@ -76,6 +76,20 @@
     public void onP2pSendComplete();
 
     /**
+     *
+     * Called to indicate the link has broken while we were trying to send
+     * a message. We'll start a debounce timer for the user to get the devices
+     * back together. UI may show a hint to achieve that
+     */
+    public void onP2pSendDebounce();
+
+    /**
+     * Called to indicate a link has come back up after being temporarily
+     * broken, and sending is resuming
+     */
+    public void onP2pResumeSend();
+
+    /**
      * Called to indicate the remote device does not support connection handover
      */
     public void onP2pHandoverNotSupported();
@@ -124,8 +138,10 @@
     static final int NDEFPUSH_SAP = 0x10;
     static final int HANDOVER_SAP = 0x14;
 
-    static final int LINK_DEBOUNCE_MS = 750;
-
+    static final int LINK_NOTHING_TO_SEND_DEBOUNCE_MS = 750;
+    static final int LINK_SEND_PENDING_DEBOUNCE_MS = 3000;
+    static final int LINK_SEND_CONFIRMED_DEBOUNCE_MS = 5000;
+    static final int LINK_SEND_COMPLETE_DEBOUNCE_MS = 250;
     static final int MSG_DEBOUNCE_TIMEOUT = 1;
     static final int MSG_RECEIVE_COMPLETE = 2;
     static final int MSG_RECEIVE_HANDOVER = 3;
@@ -137,12 +153,13 @@
     // values for mLinkState
     static final int LINK_STATE_DOWN = 1;
     static final int LINK_STATE_UP = 2;
-    static final int LINK_STATE_DEBOUNCE =3;
+    static final int LINK_STATE_DEBOUNCE = 3;
 
     // values for mSendState
     static final int SEND_STATE_NOTHING_TO_SEND = 1;
     static final int SEND_STATE_NEED_CONFIRMATION = 2;
     static final int SEND_STATE_SENDING = 3;
+    static final int SEND_STATE_SEND_COMPLETE = 4;
 
     // return values for doSnepProtocol
     static final int SNEP_SUCCESS = 0;
@@ -263,7 +280,6 @@
             if (mEchoServer != null) {
                 mEchoServer.onLlcpActivated();
             }
-
             switch (mLinkState) {
                 case LINK_STATE_DOWN:
                     mLinkState = LINK_STATE_UP;
@@ -442,7 +458,26 @@
                 case LINK_STATE_UP:
                     // Debounce
                     mLinkState = LINK_STATE_DEBOUNCE;
-                    mHandler.sendEmptyMessageDelayed(MSG_DEBOUNCE_TIMEOUT, LINK_DEBOUNCE_MS);
+                    int debounceTimeout = 0;
+                    switch (mSendState) {
+                        case SEND_STATE_NOTHING_TO_SEND:
+                            debounceTimeout = 0;
+                            break;
+                        case SEND_STATE_NEED_CONFIRMATION:
+                            debounceTimeout = LINK_SEND_PENDING_DEBOUNCE_MS;
+                            break;
+                        case SEND_STATE_SENDING:
+                            debounceTimeout = LINK_SEND_CONFIRMED_DEBOUNCE_MS;
+                            break;
+                        case SEND_STATE_SEND_COMPLETE:
+                            debounceTimeout = LINK_SEND_COMPLETE_DEBOUNCE_MS;
+                            break;
+                    }
+                    mHandler.sendEmptyMessageDelayed(MSG_DEBOUNCE_TIMEOUT, debounceTimeout);
+                    if (mSendState == SEND_STATE_SENDING) {
+                        Log.e(TAG, "onP2pSendDebounce()");
+                        mEventListener.onP2pSendDebounce();
+                    }
                     cancelSendNdefMessage();
                     disconnectLlcpServices();
                     break;
@@ -502,6 +537,7 @@
             mLlcpServicesConnected = true;
             if (mSendState == SEND_STATE_SENDING) {
                 // Send was previously confirmed, we probably cycled through a debounce
+                mEventListener.onP2pResumeSend();
                 sendNdefMessage();
             } else {
                 // User still needs to confirm, or we may have received something already.
@@ -840,7 +876,8 @@
                     if (mLinkState == LINK_STATE_DOWN || mSendState != SEND_STATE_SENDING) {
                         break;
                     }
-                    mSendState = SEND_STATE_NOTHING_TO_SEND;
+                    mSendState = SEND_STATE_SEND_COMPLETE;
+                    mHandler.removeMessages(MSG_DEBOUNCE_TIMEOUT);
                     if (DBG) Log.d(TAG, "onP2pSendComplete()");
                     mEventListener.onP2pSendComplete();
                     if (mCallbackNdef != null) {
@@ -924,6 +961,13 @@
             mSendState = SEND_STATE_SENDING;
             if (mLinkState == LINK_STATE_UP && mLlcpServicesConnected) {
                 sendNdefMessage();
+            } else if (mLinkState == LINK_STATE_DEBOUNCE) {
+                // Restart debounce timeout
+                mHandler.removeMessages(MSG_DEBOUNCE_TIMEOUT);
+                mHandler.sendEmptyMessageDelayed(MSG_DEBOUNCE_TIMEOUT,
+                        LINK_SEND_CONFIRMED_DEBOUNCE_MS);
+                // Tell user to tap devices again
+                mEventListener.onP2pSendDebounce();
             }
         }
     }
diff --git a/src/com/android/nfc/SendUi.java b/src/com/android/nfc/SendUi.java
index 7839ed6..d153a62 100644
--- a/src/com/android/nfc/SendUi.java
+++ b/src/com/android/nfc/SendUi.java
@@ -43,6 +43,7 @@
 import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
 import android.view.animation.DecelerateInterpolator;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -90,6 +91,9 @@
 
     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;
@@ -108,8 +112,10 @@
     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
@@ -129,6 +135,8 @@
     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
@@ -177,7 +185,8 @@
         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);
 
@@ -245,6 +254,16 @@
         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);
 
@@ -271,6 +290,8 @@
         if (mScreenshotBitmap == null || mAttached) {
             return;
         }
+        mBlackLayer.setVisibility(View.GONE);
+        mBlackLayer.setAlpha(0f);
         mScreenshotView.setOnTouchListener(this);
         mScreenshotView.setImageBitmap(mScreenshotBitmap);
         mScreenshotView.setTranslationX(0f);
@@ -279,6 +300,7 @@
 
         mScreenshotLayout.requestFocus();
 
+        mTextHint.setText(mContext.getResources().getString(R.string.touch));
         mTextHint.setAlpha(0.0f);
         mTextHint.setVisibility(View.VISIBLE);
         mHintAnimator.start();
@@ -323,6 +345,7 @@
     public void showStartSend() {
         if (!mAttached) return;
 
+        mTextRetry.setVisibility(View.GONE);
         // Update the starting scale - touchscreen-mashers may trigger
         // this before the pre-animation completes.
         float currentScale = mScreenshotView.getScaleX();
@@ -332,6 +355,14 @@
                 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();
     }
 
@@ -352,11 +383,13 @@
         }
 
         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",
@@ -373,7 +406,7 @@
             PropertyValuesHolder postY = PropertyValuesHolder.ofFloat("scaleY",
                     new float[] {currentScale, 0.0f});
             PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha",
-                    new float[] {1.0f, 0.0f});
+                    new float[] {currentAlpha, 0.0f});
             mFastSendAnimator.setValues(postX, postY, alpha);
 
             // Reset the fadeIn parameters to start from alpha 1
@@ -399,6 +432,8 @@
         mFastSendAnimator.cancel();
         mSuccessAnimatorSet.cancel();
         mScaleUpAnimator.cancel();
+        mAlphaUpAnimator.cancel();
+        mAlphaDownAnimator.cancel();
         mWindowManager.removeView(mScreenshotLayout);
         mStatusBarManager.disable(StatusBarManager.DISABLE_NONE);
         releaseScreenshot();
@@ -595,4 +630,24 @@
     @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();
+    }
 }