1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 package versioned.host.exp.exponent.modules.api;
4 
5 import android.os.AsyncTask;
6 import android.os.Bundle;
7 import androidx.annotation.NonNull;
8 import androidx.fragment.app.FragmentActivity;
9 
10 import com.facebook.infer.annotation.Assertions;
11 import com.facebook.react.bridge.Arguments;
12 import com.facebook.react.bridge.LifecycleEventListener;
13 import com.facebook.react.bridge.Promise;
14 import com.facebook.react.bridge.ReactApplicationContext;
15 import com.facebook.react.bridge.ReactContextBaseJavaModule;
16 import com.facebook.react.bridge.ReactMethod;
17 import com.facebook.react.bridge.WritableMap;
18 import com.facebook.react.modules.core.DeviceEventManagerModule;
19 import com.google.android.gms.common.ConnectionResult;
20 import com.google.android.gms.common.Scopes;
21 import com.google.android.gms.common.api.GoogleApiClient;
22 import com.google.android.gms.common.api.ResultCallback;
23 import com.google.android.gms.common.api.Scope;
24 import com.google.android.gms.common.api.Status;
25 import com.google.android.gms.fitness.Fitness;
26 import com.google.android.gms.fitness.data.Bucket;
27 import com.google.android.gms.fitness.data.DataPoint;
28 import com.google.android.gms.fitness.data.DataSet;
29 import com.google.android.gms.fitness.data.DataSource;
30 import com.google.android.gms.fitness.data.DataType;
31 import com.google.android.gms.fitness.data.Field;
32 import com.google.android.gms.fitness.data.Value;
33 import com.google.android.gms.fitness.request.DataReadRequest;
34 import com.google.android.gms.fitness.request.DataSourcesRequest;
35 import com.google.android.gms.fitness.request.OnDataPointListener;
36 import com.google.android.gms.fitness.request.SensorRequest;
37 import com.google.android.gms.fitness.result.DataReadResult;
38 import com.google.android.gms.fitness.result.DataSourcesResult;
39 
40 import java.util.HashMap;
41 import java.util.Map;
42 import java.util.concurrent.TimeUnit;
43 
44 import javax.annotation.Nullable;
45 
46 import host.exp.exponent.experience.ExperienceActivity;
47 import host.exp.exponent.kernel.ExperienceKey;
48 
49 public class PedometerModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
50   private static String TAG = "PedometerModule";
51   private @Nullable GoogleApiClient mClient;
52   private @Nullable OnDataPointListener mListener;
53   private int mWatchTotalSteps = 0;
54   private static Map<String, GoogleApiClient> sScopeKeyInstanceMap = new HashMap<>();
55 
PedometerModule(ReactApplicationContext context)56   public PedometerModule(ReactApplicationContext context) {
57     super(context);
58     context.addLifecycleEventListener(this);
59   }
60 
61   @Override
getName()62   public String getName() {
63     return "ExponentPedometer";
64   }
65 
assertApiClient()66   public void assertApiClient() {
67     if (mClient == null) {
68       if (sScopeKeyInstanceMap.get(getExperienceScopeKey()) != null) {
69         mClient = sScopeKeyInstanceMap.get(getExperienceScopeKey());
70         return;
71       }
72       final FragmentActivity activity = (FragmentActivity) getCurrentActivity();
73       mClient = new GoogleApiClient.Builder(getReactApplicationContext())
74           .addApi(Fitness.HISTORY_API)
75           .addApi(Fitness.SENSORS_API)
76           .addApi(Fitness.RECORDING_API)
77           .addScope(Fitness.SCOPE_ACTIVITY_READ)
78           .addConnectionCallbacks(
79               new GoogleApiClient.ConnectionCallbacks() {
80                 @Override
81                 public void onConnected(Bundle bundle) {
82                 }
83 
84                 @Override
85                 public void onConnectionSuspended(int i) {
86                 }
87               }
88           )
89           .enableAutoManage(Assertions.assertNotNull(activity), 0, new GoogleApiClient.OnConnectionFailedListener() {
90             @Override
91             public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
92               // TODO: Figure out how to handle these errors.
93             }
94           })
95           .build();
96 
97       sScopeKeyInstanceMap.put(getExperienceScopeKey(), mClient);
98 
99       Fitness.RecordingApi.subscribe(mClient, DataType.TYPE_STEP_COUNT_DELTA)
100           .setResultCallback(new ResultCallback<Status>() {
101             @Override
102             public void onResult(@NonNull Status status) {
103               // TODO: Figure out how to handle these errors.
104             }
105           });
106     }
107   }
108 
109   @ReactMethod
getStepCountAsync(final double startTime, final double endTime, final Promise promise)110   public void getStepCountAsync(final double startTime, final double endTime, final Promise promise) {
111     assertApiClient();
112 
113     AsyncTask.execute(new Runnable() {
114       @Override
115       public void run() {
116         final DataReadRequest req = new DataReadRequest.Builder()
117             .aggregate(DataType.AGGREGATE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA)
118             .bucketByTime(1, TimeUnit.DAYS)
119             .setTimeRange((long) startTime, (long) endTime, TimeUnit.MILLISECONDS)
120             .build();
121 
122         DataReadResult dataReadResult =
123             Fitness.HistoryApi.readData(mClient, req).await(1, TimeUnit.MINUTES);
124 
125         int steps = 0;
126         for (Bucket bucket : dataReadResult.getBuckets()) {
127           DataSet ds = bucket.getDataSet(DataType.TYPE_STEP_COUNT_DELTA);
128           for (DataPoint dp : ds.getDataPoints()) {
129             Value value = dp.getValue(Field.FIELD_STEPS);
130             steps += value.asInt();
131           }
132         }
133 
134         WritableMap result = Arguments.createMap();
135         result.putInt("steps", steps);
136         promise.resolve(result);
137       }
138     });
139   }
140 
141   @ReactMethod
watchStepCount()142   public void watchStepCount() {
143     assertApiClient();
144     stopWatchingStepCount();
145 
146     mWatchTotalSteps = 0;
147 
148     mListener = new OnDataPointListener() {
149       @Override
150       public void onDataPoint(DataPoint dataPoint) {
151         Value value = dataPoint.getValue(Field.FIELD_STEPS);
152         WritableMap response = Arguments.createMap();
153         mWatchTotalSteps += value.asInt();
154         response.putInt("steps", mWatchTotalSteps);
155         getReactApplicationContext()
156             .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
157             .emit("Exponent.pedometerUpdate", response);
158       }
159     };
160 
161     Fitness.SensorsApi.add(
162         mClient,
163         new SensorRequest.Builder()
164             .setDataType(DataType.TYPE_STEP_COUNT_DELTA)
165             .setSamplingRate(5, TimeUnit.SECONDS)
166             .build(),
167         mListener);
168   }
169 
170   @ReactMethod
stopWatchingStepCount()171   public void stopWatchingStepCount() {
172     assertApiClient();
173 
174     if (mListener != null) {
175       Fitness.SensorsApi.remove(mClient, mListener);
176     }
177   }
178 
179   @ReactMethod
isAvailableAsync(final Promise promise)180   public void isAvailableAsync(final Promise promise) {
181     assertApiClient();
182 
183     Fitness.SensorsApi.findDataSources(mClient, new DataSourcesRequest.Builder()
184         .setDataTypes(DataType.TYPE_STEP_COUNT_DELTA)
185         .build())
186         .setResultCallback(new ResultCallback<DataSourcesResult>() {
187           @Override
188           public void onResult(@NonNull DataSourcesResult dataSourcesResult) {
189             if (!dataSourcesResult.getStatus().isSuccess()) {
190               promise.reject("E_PEDOMETER", "Failed to determine if the pedometer is available.");
191               return;
192             }
193             for (DataSource ds : dataSourcesResult.getDataSources()) {
194               if (ds.getDataType().equals(DataType.TYPE_STEP_COUNT_DELTA)) {
195                 promise.resolve(true);
196                 return;
197               }
198             }
199             promise.resolve(false);
200           }
201         });
202   }
203 
204   @Override
onHostResume()205   public void onHostResume() {
206 
207   }
208 
209   @Override
onHostPause()210   public void onHostPause() {
211 
212   }
213 
214   @Override
onHostDestroy()215   public void onHostDestroy() {
216     sScopeKeyInstanceMap.remove(getExperienceScopeKey());
217   }
218 
getExperienceScopeKey()219   private String getExperienceScopeKey() {
220     try {
221       ExperienceActivity activity = (ExperienceActivity) getCurrentActivity();
222       return activity != null ? activity.getExperienceKey().getScopeKey() : null;
223     } catch (ClassCastException e) {
224       return null;
225     }
226   }
227 }