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 }