io.cloudmatch.demo.pinchanddrag.PADActivity.java Source code

Java tutorial

Introduction

Here is the source code for io.cloudmatch.demo.pinchanddrag.PADActivity.java

Source

/*
 * Copyright 2014 CloudMatch.io
 *
 * 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
 *
 * 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 io.cloudmatch.demo.pinchanddrag;

import android.app.Dialog;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.FragmentActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.DragEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.DragShadowBuilder;
import android.view.View.OnDragListener;
import android.view.View.OnTouchListener;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.RelativeLayout.LayoutParams;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.LocationServices;

import java.util.Random;

import butterknife.Bind;
import butterknife.ButterKnife;
import io.cloudmatch.demo.R;
import io.ticofab.cm_android_sdk.library.consts.MovementType;
import io.ticofab.cm_android_sdk.library.consts.Movements;
import io.ticofab.cm_android_sdk.library.interfaces.CloudMatchViewInterface;
import io.ticofab.cm_android_sdk.library.interfaces.LocationProvider;
import io.ticofab.cm_android_sdk.library.views.CloudMatchPinchViewHorizontal;

/*
 * This is the most complex demo. There are two phases: the first phase is when two devices are matched, by pinching
 * across their screens on the long side. Once a match has been established, one of the two devices involved displays two shapes.
 * At that point, either shape can be dragged across on to the other device. This effect is achieved using this mechanism:
 *
 *   1. The device where a shape starts to be dragged sends a message to the other one. A shape being dropped on the side
 *      of the first device will temporarily disappear.
 *
 *   2. Upon receiving such message, and for the duration of an interval, the other device "awaits" the shape:
 *      it means that it will show it if a drag action starts on one of the two sides.
 *
 *   3. If the shape is then dropped in the center area of the second device, it will "acquire it" and send an ACK message
 *      to the first device, which won't make the shape appear again.
 */
public class PADActivity extends FragmentActivity
        implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
    static final String TAG = PADActivity.class.getSimpleName();
    static final String DRAG_LABEL = "shape";
    static final String CIRCLE_STRING = "circle";
    static final String RECT_STRING = "rect";

    static final int SHAPE_VISIBILITY_RESET_INTERVAL = 3000;
    static final int DRAGGING_CANCEL_INTERVAL = 3000; // milliseconds

    // UI stuff
    @Bind(R.id.rect_shape)
    ImageView mRectIV;
    @Bind(R.id.left_view)
    RelativeLayout mLeftRL;
    @Bind(R.id.circle_shape)
    ImageView mCircleIV;
    @Bind(R.id.right_view)
    RelativeLayout mRightRL;
    @Bind(R.id.container_view)
    RelativeLayout mContainerRL;
    @Bind(R.id.pinch_view)
    CloudMatchPinchViewHorizontal mPinchView;
    @Bind(R.id.pinchanddrag_pinch_instruction_iv)
    ImageView mPinchInstructionsIcon;

    String mGroupId;
    boolean mIHaveCircle;
    boolean mIHaveSquare;
    Double mCoinTossMyValue;
    MyCircleView mMyCircleView;
    PADDeliveryHelper mPNDDeliveryHelper;
    String mShapeBeingDraggedOnOtherSide = "";
    final Handler mWaitingForDragHandler = new Handler();
    final Handler mShapeVisibilityHandler = new Handler();

    // location stuff
    Location mLastLocation;
    GoogleApiClient mGoogleApiClient;

    private final Handler mHandler = new Handler();
    private final Runnable mRemoveViewRunnable = new Runnable() {

        @Override
        public void run() {
            mContainerRL.removeView(mMyCircleView);
        }
    };

    // implementation of the PinchAndDragMatchedInterface
    private final PADMatchedInterface mMatchedInterface = new PADMatchedInterface() {

        @Override
        public void onMatched(final String groupId) {
            mGroupId = groupId;

            final String txt = "Matched in group " + groupId;
            Toast.makeText(PADActivity.this, txt, Toast.LENGTH_LONG).show();

            // do the "coin toss" to decide who gets the shapes first
            mCoinTossMyValue = new Random().nextDouble();
            mPNDDeliveryHelper.sendCointoss(groupId, mCoinTossMyValue);

            mPinchInstructionsIcon.setVisibility(View.GONE);
        }

        @Override
        public void onMatcheeLeft() {
            mGroupId = null;
            final String txt = "Everybody left";
            Toast.makeText(PADActivity.this, txt, Toast.LENGTH_LONG).show();

            mIHaveCircle = false;
            mIHaveSquare = false;
            setShapesVisibility();
            mPinchInstructionsIcon.setVisibility(View.VISIBLE);
        }
    };

    private void setShapesVisibility() {
        mCircleIV.setVisibility(mIHaveCircle ? View.VISIBLE : View.INVISIBLE);
        mRectIV.setVisibility(mIHaveSquare ? View.VISIBLE : View.INVISIBLE);
    }

    /*
     * Implementation of PinchAndDragDeliveryInterface, triggered in response to deliveries from other devices in
     * the same group.
     */
    private final PADDeliveryInterface mDeliveryInterface = new PADDeliveryInterface() {

        // upon receiving a coin toss, check who has the biggest value
        @Override
        public void onCoinToss(final Double value) {
            final boolean haveStuff = mCoinTossMyValue > value;
            mIHaveCircle = haveStuff;
            mIHaveSquare = haveStuff;
            setShapesVisibility();
        }

        // the other side started to drag a shape over.
        @Override
        public void onShapeDragInitiatedOnOtherSide(final String shape) {
            mShapeBeingDraggedOnOtherSide = shape;

            // if I don't receive the shape within some interval, stop waiting for it.
            mWaitingForDragHandler.postDelayed(new Runnable() {

                @Override
                public void run() {
                    mShapeBeingDraggedOnOtherSide = "";
                }
            }, DRAGGING_CANCEL_INTERVAL);
        }

        // the other side stopped dragging a shape.
        @Override
        public void onShapeDragStoppedOnOtherSide() {
            // the other side stopped dragging a shape.
            mShapeBeingDraggedOnOtherSide = "";
        }

        // the other side has received a shape, so he has it now.
        @Override
        public void onShapeReceivedOnOtherSide(final String shape) {
            // the other side got it. Don't make it back to visibility.
            final boolean haveLostCircle = shape.equals(CIRCLE_STRING);
            if (haveLostCircle) {
                mIHaveCircle = false;
            } else {
                mIHaveSquare = false;
            }
            setShapesVisibility();
            mShapeVisibilityHandler.removeCallbacksAndMessages(null);
        }
    };

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pinch_and_drag_demo);
        ButterKnife.bind(this);

        // initialize here or otherwise 'this' & the view will be null
        mMyCircleView = new MyCircleView(this);
        mPNDDeliveryHelper = new PADDeliveryHelper(mPinchView);

        // init google api client
        mGoogleApiClient = new GoogleApiClient.Builder(this).addConnectionCallbacks(this)
                .enableAutoManage(this, this).addApi(LocationServices.API).build();
    }

    public void initCloudMatch() {
        // initializes the CloudMatch. In this case we also immediately connect,
        // but it could be done also at a different stage.
        try {
            mPinchView.initCloudMatch(this, new PADServerEventListener(this, mMatchedInterface, mDeliveryInterface),
                    new LocationProvider() {
                        @Override
                        public Location getLocation() {
                            if (mGoogleApiClient.isConnected()) {
                                mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
                            }
                            return mLastLocation;
                        }
                    }, mPinchDemoSMVI);
            mPinchView.connect();
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        // get the CloudMatchPinchViewHorizontal object from the xml layout and sets the interface on it.
        mRectIV.setOnTouchListener(new ShapeOnTouchListener());
        mCircleIV.setOnTouchListener(new ShapeOnTouchListener());

        // setup gesture detector and areas
        mRightRL.setOnDragListener(new SideAreaDragListener(SideAreas.Right));
        mRightRL.setOnTouchListener(new SideAreaOnTouchListener(SideAreas.Right));

        mLeftRL.setOnDragListener(new SideAreaDragListener(SideAreas.Left));
        mLeftRL.setOnTouchListener(new SideAreaOnTouchListener(SideAreas.Left));

        final RelativeLayout centerRL = (RelativeLayout) findViewById(R.id.center_view);
        centerRL.setOnDragListener(new CenterAreaDragListener());
    }

    /*
     * Closing the connection in the onDestroy() method.
     */
    @Override
    public void onDestroy() {
        mPinchView.closeConnection();
        super.onDestroy();
    }

    /*
     * Pass touches to the CloudMatchView object.
     */
    @Override
    public boolean onTouchEvent(final MotionEvent event) {
        mPinchView.onTouchEvent(event);
        return true;
    }

    /*
     * Implementation of CloudMatchViewInterface. When a matching movement is detected, we'll show a little red
     * circle at the side of the screen where the gesture ended, and remove it after a little time.
     */
    private final CloudMatchViewInterface mPinchDemoSMVI = new CloudMatchViewInterface() {

        @Override
        public void onMovementDetection(final Movements movement, final MovementType movementType,
                final PointF pointStart, final PointF pointEnd) {
            Log.d(TAG, "onMovementDetection. Movement: " + movement + ", swipeType: " + movementType
                    + ", point start: " + pointStart + ", point end: " + pointEnd);

            // remove existing stuff if there
            mContainerRL.removeView(mMyCircleView);
            mWaitingForDragHandler.removeCallbacksAndMessages(null);

            // only display circle if we're not already connected
            if (TextUtils.isEmpty(mGroupId)) {
                // position the little rect view at the appropriate coordinates
                final int side = MyCircleView.RADIUS * 2;
                final LayoutParams params = new RelativeLayout.LayoutParams(side, side);

                int rule = RelativeLayout.ALIGN_PARENT_RIGHT;
                switch (movement) {
                case innerleft:
                case topleft:
                case bottomleft:
                case rightleft:
                    rule = RelativeLayout.ALIGN_PARENT_LEFT;
                    break;
                default:
                    break;
                }

                params.addRule(rule, R.id.container_view);
                params.setMargins(10, (int) pointEnd.y, 10, 0);
                mContainerRL.addView(mMyCircleView, params);

                mHandler.postDelayed(mRemoveViewRunnable, 2000);
            }
        }

        @Override
        public void onError(final RuntimeException e) {
            Log.d(TAG, "onError: " + e);
        }

        @Override
        public boolean isGestureValid() {
            // only attempt to connect if we're not already connected
            return TextUtils.isEmpty(mGroupId);
        }
    };

    // a little custom view to show where a matching movement ended.
    private class MyCircleView extends View {
        public static final int RADIUS = 15;

        public MyCircleView(final Context context) {
            super(context);
        }

        @Override
        protected void onDraw(final Canvas canvas) {
            final Paint myPaint = new Paint();
            myPaint.setColor(Color.RED);
            myPaint.setStyle(Paint.Style.FILL);
            canvas.drawCircle(RADIUS, RADIUS, RADIUS, myPaint);
        }
    }

    // enum to facilitate dragging logic
    private enum SideAreas {
        Left, Right
    }

    // invoked when a shape has been dropped in this device from another device.
    // It will be "acquired" and a confirmation will be sent to the other one.
    private void shapeHasBeenDropped() {
        if (!TextUtils.isEmpty(mShapeBeingDraggedOnOtherSide)) {
            final boolean isDraggingCircle = mShapeBeingDraggedOnOtherSide.equals(CIRCLE_STRING);

            if (isDraggingCircle && !mIHaveCircle) {
                // a circle has been dragged from the other side
                mIHaveCircle = true;
            } else if (!mIHaveSquare) {
                // a square has been dragged from the other side
                mIHaveSquare = true;
            }

            setShapesVisibility();

            mPNDDeliveryHelper.sendShapeReceivedAck(mGroupId, isDraggingCircle ? CIRCLE_STRING : RECT_STRING);
        }
    }

    // listener that detects if a drag has ended. Used on the central area.
    private class CenterAreaDragListener implements OnDragListener {

        @Override
        public boolean onDrag(final View v, final DragEvent event) {
            if (event.getAction() == DragEvent.ACTION_DROP) {
                if (TextUtils.isEmpty(mShapeBeingDraggedOnOtherSide)) {
                    // we were dragging our own shape and dropped in in the center
                    setShapesVisibility();

                    mPNDDeliveryHelper.sendShapeDragStopped(mGroupId);
                } else {
                    // a shape has been dragged to us
                    shapeHasBeenDropped();
                }
            }

            return true;
        }

    }

    // listener that detects a dragging action on a side that will be specified in the constructor.
    private class SideAreaDragListener implements OnDragListener {

        private final SideAreas mMyself;

        public SideAreaDragListener(final SideAreas area) {
            mMyself = area;
        }

        @Override
        public boolean onDrag(final View v, final DragEvent event) {
            if (event.getAction() == DragEvent.ACTION_DROP) {
                final String tag = event.getClipData().getItemAt(0).getText().toString();
                Log.d(TAG, "shape-to-side drop, tag: " + tag + ", end area: " + mMyself.name());

                // I assume that if we get here we are connected
                if (TextUtils.isEmpty(mShapeBeingDraggedOnOtherSide)) {
                    // we dropped a shape on the side to transfer it to the other side

                    final View viewToRestore = tag.equals(CIRCLE_STRING) ? mCircleIV : mRectIV;
                    mShapeVisibilityHandler.postDelayed(new Runnable() {

                        @Override
                        public void run() {
                            Log.d(TAG, "mShapeVisibilityHandler, didn't receive any ack.");
                            viewToRestore.setVisibility(View.VISIBLE);
                            mPNDDeliveryHelper.sendShapeDragStopped(mGroupId);
                        }
                    }, SHAPE_VISIBILITY_RESET_INTERVAL);

                } else {
                    // a shape has been dragged to us
                    shapeHasBeenDropped();
                }
            }
            return true;
        }
    }

    // Upon touching a shape, we start to drag it.
    private class ShapeOnTouchListener implements OnTouchListener {

        @Override
        public boolean onTouch(final View v, final MotionEvent event) {
            // we should get here only if the shape is visible
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                final DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(v);
                final String shapeTag = (String) v.getTag();
                final ClipData tag = ClipData.newPlainText(DRAG_LABEL, shapeTag);
                v.startDrag(tag, shadowBuilder, v, 0);
                v.setVisibility(View.INVISIBLE);

                mPNDDeliveryHelper.sendShapeDragStart(mGroupId, shapeTag);

                return true;
            }

            return false;
        }
    }

    // if a movement is detected on the side area, and we know that a shape is being dragged from the other side,
    // let's "drag the shape in", simulating the passage between the two screens.
    private class SideAreaOnTouchListener implements OnTouchListener {

        private final SideAreas mArea;

        public SideAreaOnTouchListener(final SideAreas area) {
            mArea = area;
        }

        @Override
        public boolean onTouch(final View v, final MotionEvent event) {
            // move grey ball to where the touch started
            if (event.getAction() == MotionEvent.ACTION_DOWN && !TextUtils.isEmpty(mShapeBeingDraggedOnOtherSide)) {
                final boolean isDraggingCircle = mShapeBeingDraggedOnOtherSide.equals(CIRCLE_STRING);
                final View draggedView = isDraggingCircle ? mCircleIV : mRectIV;
                final DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(draggedView);
                final ClipData tag = ClipData.newPlainText(DRAG_LABEL, mArea.name());
                draggedView.startDrag(tag, shadowBuilder, draggedView, 0);
                return true;
            }

            return false;
        }
    }

    // *******************************************************************
    // The following code comes from Google's guide to check for
    // the presence of GooglePlayServices.
    // https://developers.google.com/android/guides/api-client
    // *******************************************************************
    // Request code to use when launching the resolution activity
    private static final int REQUEST_RESOLVE_ERROR = 1001;
    // Unique tag for the error dialog fragment
    private static final String DIALOG_ERROR = "dialog_error";
    // Bool to track whether the app is already resolving an error
    private boolean mResolvingError = false;

    @Override
    public void onConnected(Bundle bundle) {
        mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
        initCloudMatch();
    }

    @Override
    public void onConnectionSuspended(int i) {

    }

    @Override
    public void onConnectionFailed(ConnectionResult result) {
        if (mResolvingError) {
            // Already attempting to resolve an error.
            return;
        } else if (result.hasResolution()) {
            try {
                mResolvingError = true;
                result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
            } catch (IntentSender.SendIntentException e) {
                // There was an error with the resolution intent. Try again.
                mGoogleApiClient.connect();
            }
        } else {
            // Show dialog using GoogleApiAvailability.getErrorDialog()
            showErrorDialog(result.getErrorCode());
            mResolvingError = true;
        }
    }

    // The rest of this code is all about building the error dialog

    /* Creates a dialog for an error message */
    private void showErrorDialog(int errorCode) {
        // Create a fragment for the error dialog
        ErrorDialogFragment dialogFragment = new ErrorDialogFragment();
        // Pass the error that should be displayed
        Bundle args = new Bundle();
        args.putInt(DIALOG_ERROR, errorCode);
        dialogFragment.setArguments(args);
        dialogFragment.show(getSupportFragmentManager(), "errordialog");

    }

    /* Called from ErrorDialogFragment when the dialog is dismissed. */
    public void onDialogDismissed() {
        mResolvingError = false;
    }

    /* A fragment to display an error dialog */
    public static class ErrorDialogFragment extends android.support.v4.app.DialogFragment {
        public ErrorDialogFragment() {
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            // Get the error code and retrieve the appropriate dialog
            int errorCode = this.getArguments().getInt(DIALOG_ERROR);
            return GoogleApiAvailability.getInstance().getErrorDialog(this.getActivity(), errorCode,
                    REQUEST_RESOLVE_ERROR);
        }

        @Override
        public void onDismiss(DialogInterface dialog) {
            ((PADActivity) getActivity()).onDialogDismissed();
        }
    }
}