org.runnerup.tracker.component.TrackerWear.java Source code

Java tutorial

Introduction

Here is the source code for org.runnerup.tracker.component.TrackerWear.java

Source

/*
 * Copyright (C) 2014 weides@gmail.com
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.runnerup.tracker.component;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.util.Pair;

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.common.api.ResultCallback;
import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.DataItem;
import com.google.android.gms.wearable.DataItemBuffer;
import com.google.android.gms.wearable.DataMap;
import com.google.android.gms.wearable.MessageApi;
import com.google.android.gms.wearable.MessageEvent;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.PutDataRequest;
import com.google.android.gms.wearable.Wearable;

import org.runnerup.common.tracker.TrackerState;
import org.runnerup.common.util.Constants;
import org.runnerup.common.util.ValueModel;
import org.runnerup.tracker.Tracker;
import org.runnerup.tracker.WorkoutObserver;
import org.runnerup.util.Formatter;
import org.runnerup.workout.Dimension;
import org.runnerup.workout.Scope;
import org.runnerup.workout.Step;
import org.runnerup.workout.Workout;
import org.runnerup.workout.WorkoutInfo;
import org.runnerup.workout.WorkoutStepListener;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

import static com.google.android.gms.wearable.PutDataRequest.WEAR_URI_SCHEME;

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class TrackerWear extends DefaultTrackerComponent
        implements Constants, TrackerComponent, WorkoutObserver, NodeApi.NodeListener, MessageApi.MessageListener,
        DataApi.DataListener, WorkoutStepListener, ValueModel.ChangeListener<TrackerState> {

    public static final String NAME = "WEAR";
    private Tracker tracker;
    private Context context;
    private GoogleApiClient mGoogleApiClient;
    private Formatter formatter;
    @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
    private HashSet<Node> connectedNodes = new HashSet<Node>();
    private String wearNode;

    private final Handler handler = new Handler();
    private Bundle lastCreatedWorkoutEvent;
    private Bundle lastSentWorkoutEvent;
    private long tickFrequency = 1000;
    private long tickFrequencyPause = 500; // so that seconds does show "slowly"
    private boolean mWorkoutSenderRunning = false;

    private List<Pair<Scope, Dimension>> items = new ArrayList<Pair<Scope, Dimension>>(3);
    private Step currentStep;
    private boolean pauseStep;

    public TrackerWear(Tracker tracker) {
        this.tracker = tracker;
        items.add(new Pair<Scope, Dimension>(Scope.ACTIVITY, Dimension.TIME));
        items.add(new Pair<Scope, Dimension>(Scope.ACTIVITY, Dimension.DISTANCE));
        items.add(new Pair<Scope, Dimension>(Scope.LAP, Dimension.PACE));
    }

    @Override
    public String getName() {
        return NAME;
    }

    private boolean hasPlay() {
        int result;
        try {
            result = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
        } catch (Exception e) {
            return false;
        }
        return (result == ConnectionResult.SUCCESS);
    }

    @Override
    public TrackerComponent.ResultCode onInit(final Callback callback, Context context) {
        this.context = context;
        if (!hasPlay()) {
            return ResultCode.RESULT_NOT_SUPPORTED;
        }

        try {
            context.getPackageManager().getPackageInfo("com.google.android.wearable.app",
                    PackageManager.GET_META_DATA);
        } catch (PackageManager.NameNotFoundException e) {
            // android wear app is not installed => can't be paired
            return ResultCode.RESULT_NOT_SUPPORTED;
        }

        tracker.registerTrackerStateListener(this);
        mGoogleApiClient = new GoogleApiClient.Builder(context)
                .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
                    @Override
                    public void onConnected(Bundle connectionHint) {
                        callback.run(TrackerWear.this, ResultCode.RESULT_OK);

                        /* set "our" data */
                        setData();

                        Wearable.MessageApi.addListener(mGoogleApiClient, TrackerWear.this);
                        Wearable.NodeApi.addListener(mGoogleApiClient, TrackerWear.this);
                        Wearable.DataApi.addListener(mGoogleApiClient, TrackerWear.this);

                        /* read already existing data */
                        readData();

                        /** get info about connected nodes in background */
                        Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).

                                setResultCallback(new ResultCallback<NodeApi.GetConnectedNodesResult>() {

                                    @Override
                                    public void onResult(NodeApi.GetConnectedNodesResult nodes) {
                                        for (Node node : nodes.getNodes()) {
                                            onPeerConnected(node);
                                        }
                                    }
                                }

                );
                    }

                    @Override
                    public void onConnectionSuspended(int cause) {
                    }
                }).addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
                    @Override
                    public void onConnectionFailed(ConnectionResult result) {
                        callback.run(TrackerWear.this, ResultCode.RESULT_ERROR);
                    }
                }).addApi(Wearable.API).build();
        mGoogleApiClient.connect();
        return ResultCode.RESULT_PENDING;
    }

    private void readData() {
        Wearable.DataApi
                .getDataItems(mGoogleApiClient,
                        new Uri.Builder().scheme(WEAR_URI_SCHEME).path(Wear.Path.WEAR_NODE_ID).build())
                .setResultCallback(new ResultCallback<DataItemBuffer>() {
                    @Override
                    public void onResult(DataItemBuffer dataItems) {
                        for (DataItem dataItem : dataItems) {
                            wearNode = dataItem.getUri().getHost();
                            Log.e(getName(), "getDataItem => wearNode:" + wearNode);
                        }
                        dataItems.release();
                    }
                });
    }

    private void setData() {
        Wearable.DataApi.putDataItem(mGoogleApiClient, PutDataRequest.create(Constants.Wear.Path.PHONE_NODE_ID));
    }

    @Override
    public void onBind(HashMap<String, Object> bindValues) {
        formatter = (Formatter) bindValues.get(Workout.KEY_FORMATTER);
    }

    @Override
    public void onStart() {
        updateHeaders();
        setTrackerState(tracker.getState());
        tracker.getWorkout().registerWorkoutStepListener(this);
    }

    private void setTrackerState(TrackerState val) {
        Log.e(getName(), "setTrackerState(" + val + ")");
        Bundle b = new Bundle();
        b.putInt(Wear.TrackerState.STATE, val.getValue());
        setData(Wear.Path.TRACKER_STATE, b);
    }

    private void setData(String path, Bundle b) {
        Wearable.DataApi
                .putDataItem(mGoogleApiClient,
                        PutDataRequest.create(path).setData(DataMap.fromBundle(b).toByteArray()))
                .setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
                    @Override
                    public void onResult(DataApi.DataItemResult dataItemResult) {
                        if (!dataItemResult.getStatus().isSuccess()) {
                            Log.e(getName(), "TrackerWear: ERROR: failed to putDataItem, " + "status code: "
                                    + dataItemResult.getStatus().getStatusCode());
                        }
                    }
                });
    }

    @Override
    public void workoutEvent(WorkoutInfo workoutInfo, int type) {
        switch (type) {
        case DB.LOCATION.TYPE_START:
        case DB.LOCATION.TYPE_RESUME:
            setTrackerState(TrackerState.STARTED);
            break;
        case DB.LOCATION.TYPE_PAUSE:
            setTrackerState(TrackerState.PAUSED);
        case DB.LOCATION.TYPE_END:
            break;
        }

        Bundle b = new Bundle();
        {
            int i = 0;
            for (Pair<Scope, Dimension> item : items) {
                b.putString(Wear.RunInfo.DATA + i, formatter.format(Formatter.TXT_SHORT, item.second,
                        workoutInfo.get(item.first, item.second)));
                i++;
            }
        }

        lastCreatedWorkoutEvent = b;
    }

    private void sendWorkoutEvent() {
        if (!isConnected())
            return;

        /* special handling of pauseStep */
        if (pauseStep) {
            if (lastCreatedWorkoutEvent == null)
                lastCreatedWorkoutEvent = lastSentWorkoutEvent;

            if (lastCreatedWorkoutEvent == null) {
                lastCreatedWorkoutEvent = new Bundle();
            }

            Dimension dim = currentStep.getDurationType();
            if (dim != null) {
                double remaining = tracker.getWorkout().getRemaining(Scope.STEP, dim);
                if (remaining < 0) {
                    remaining = 0;
                }
                lastCreatedWorkoutEvent.putString(Wear.RunInfo.COUNTDOWN,
                        formatter.formatRemaining(Formatter.TXT_SHORT, dim, remaining));
            }
        }

        if (lastCreatedWorkoutEvent != null) {
            Wearable.MessageApi.sendMessage(mGoogleApiClient, wearNode, Wear.Path.MSG_WORKOUT_EVENT,
                    DataMap.fromBundle(lastCreatedWorkoutEvent).toByteArray());
            lastSentWorkoutEvent = lastCreatedWorkoutEvent;
            lastCreatedWorkoutEvent = null;
        }
    }

    private Runnable workoutEventSender = new Runnable() {
        @Override
        public void run() {
            sendWorkoutEvent();
            mWorkoutSenderRunning = false;

            if (!isConnected())
                return;

            if (currentStep == null)
                return;

            mWorkoutSenderRunning = true;
            handler.postDelayed(workoutEventSender, pauseStep ? tickFrequencyPause : tickFrequency);
        }
    };

    @Override
    public void onStepChanged(Step oldStep, Step newStep) {
        currentStep = newStep;

        if (!mWorkoutSenderRunning) {
            // this starts workout sender
            workoutEventSender.run();
        }

        if (currentStep == null) {
            return; // this is end
        }

        updateHeaders();
    }

    private void updateHeaders() {
        Bundle b = new Bundle();
        int i = 0;
        for (Pair<Scope, Dimension> item : items) {
            b.putString(Wear.RunInfo.HEADER + i, context.getString(item.second.getTextId()));
            i++;
        }

        pauseStep = false;
        if (currentStep != null && currentStep.isPauseStep()) {
            pauseStep = true;
            b.putBoolean(Wear.RunInfo.PAUSE_STEP, true);
        }

        setData(Wear.Path.HEADERS, b);
    }

    @Override
    public void onComplete(boolean discarded) {
        tracker.getWorkout().unregisterWorkoutStepListener(this);
        currentStep = null;

        clearData(/* don't clear own node id */ false);
    }

    @Override
    public boolean isConnected() {
        if (mGoogleApiClient == null)
            return false;

        if (!mGoogleApiClient.isConnected())
            return false;

        return wearNode != null;
    }

    @Override
    public void onPeerConnected(Node node) {
        connectedNodes.add(node);
    }

    @Override
    public void onPeerDisconnected(Node node) {
        connectedNodes.remove(node);
        if (wearNode != null && node.getId().contentEquals(wearNode))
            wearNode = null;
    }

    @Override
    public void onMessageReceived(final MessageEvent messageEvent) {
        Log.e(getName(), "onMessageReceived: " + messageEvent);
        //note: skip state checking, do that in receiver instead
        if (Wear.Path.MSG_CMD_WORKOUT_PAUSE.contentEquals(messageEvent.getPath())) {
            sendLocalBroadcast(Intents.PAUSE_WORKOUT);
            return;
        } else if (Wear.Path.MSG_CMD_WORKOUT_RESUME.contentEquals(messageEvent.getPath())) {
            sendLocalBroadcast(Intents.RESUME_WORKOUT);
            return;
        } else if (Wear.Path.MSG_CMD_WORKOUT_NEW_LAP.contentEquals(messageEvent.getPath())) {
            sendLocalBroadcast(Intents.NEW_LAP);
            return;
        } else if (Wear.Path.MSG_CMD_WORKOUT_START.contentEquals(messageEvent.getPath())) {
            /* send broadcast to StartActivity */
            Intent startBroadcastIntent = new Intent();
            startBroadcastIntent.setAction(Intents.START_WORKOUT);
            context.sendBroadcast(startBroadcastIntent);
            return;
        }
    }

    private void sendLocalBroadcast(String action) {
        Intent intent = new Intent();
        intent.setAction(action);
        LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
    }

    @Override
    public ResultCode onEnd(Callback callback, Context context) {
        if (mGoogleApiClient != null) {
            if (mGoogleApiClient.isConnected()) {
                clearData(true);
                wearNode = null;

                Wearable.MessageApi.removeListener(mGoogleApiClient, this);
                Wearable.NodeApi.removeListener(mGoogleApiClient, this);
                Wearable.DataApi.removeListener(mGoogleApiClient, this);
                connectedNodes.clear();
            }
            mGoogleApiClient.disconnect();
            mGoogleApiClient = null;
        }
        tracker.unregisterTrackerStateListener(this);
        return ResultCode.RESULT_OK;
    }

    private void clearData(boolean self) {
        if (self) {
            /* clear our node id */
            Wearable.DataApi.deleteDataItems(mGoogleApiClient,
                    new Uri.Builder().scheme(WEAR_URI_SCHEME).path(Wear.Path.PHONE_NODE_ID).build());
        }

        /* clear HEADERS */
        Wearable.DataApi.deleteDataItems(mGoogleApiClient,
                new Uri.Builder().scheme(WEAR_URI_SCHEME).path(Wear.Path.HEADERS).build());

        /* clear WORKOUT PLAN */
        Wearable.DataApi.deleteDataItems(mGoogleApiClient,
                new Uri.Builder().scheme(WEAR_URI_SCHEME).path(Wear.Path.WORKOUT_PLAN).build());

        /* clear TRACKER_STATE */
        Wearable.DataApi.deleteDataItems(mGoogleApiClient,
                new Uri.Builder().scheme(WEAR_URI_SCHEME).path(Wear.Path.TRACKER_STATE).build());
    }

    @Override
    public void onDataChanged(final DataEventBuffer dataEvents) {
        for (DataEvent ev : dataEvents) {
            Log.e(getName(), "onDataChanged: " + ev.getDataItem().getUri());
            String path = ev.getDataItem().getUri().getPath();
            if (Constants.Wear.Path.WEAR_NODE_ID.contentEquals(path)) {
                setWearNode(ev);
            }
        }
    }

    private void setWearNode(DataEvent ev) {
        if (ev.getType() == DataEvent.TYPE_CHANGED) {
            wearNode = ev.getDataItem().getUri().getHost();
            if (lastCreatedWorkoutEvent == null) {
                lastCreatedWorkoutEvent = lastSentWorkoutEvent;
            }
            if (!mWorkoutSenderRunning)
                workoutEventSender.run();
            else
                sendWorkoutEvent();
        } else if (ev.getType() == DataEvent.TYPE_DELETED) {
            wearNode = null;
        }
    }

    @Override
    public void onValueChanged(ValueModel<TrackerState> instance, TrackerState oldValue, TrackerState newValue) {
        setTrackerState(newValue);
    }
}