> itr = deviceToBeaconMap.entrySet().iterator();
184 |                 boolean findClosest = false;
185 |                 while (itr.hasNext()) {
186 |                     Beacon beacon = itr.next().getValue();
187 |                     if ((time - beacon.lastSeenTimestamp) > ON_LOST_TIMEOUT_MS) {
188 |                         itr.remove();
189 |                     }
190 |                     if (beacon == mClosest) {
191 |                         findClosest = true;
192 |                     }
193 |                 }
194 | 
195 |                 if (findClosest) {
196 |                     findClosest();
197 |                 }
198 | 
199 |                 handler.postDelayed(this, ON_LOST_TIMEOUT_MS);
200 |             }
201 |         };
202 |         handler.postDelayed(removeLostDevices, ON_LOST_TIMEOUT_MS);
203 |     }
204 | 
205 |     private void findClosest() {
206 |         Beacon oldClosest = mClosest;
207 |         mClosest = null;
208 |         for (Beacon other : deviceToBeaconMap.values()) {
209 |             if (other.urlStatus != null) {
210 |                 Uri url = other.urlStatus.getUrl();
211 |                 if (url != null && BEACON_HOST_NAME.equals(url.getHost()) && (mClosest == null || mClosest.rssi < other.rssi)) {
212 |                     mClosest = other;
213 |                 }
214 |             }
215 |         }
216 |         if ((mClosest == null && oldClosest != null) || (mClosest != null && !mClosest.equals(oldClosest))) {
217 |             mOnClosestChangedListener.onClosestChanged(mClosest);
218 |         }
219 |     }
220 | 
221 |     /**
222 |      * Attempts to create the scanner.
223 |      *
224 |      * @param context
225 |      * @return true if successful
226 |      */
227 |     public boolean init(final Activity context) {
228 |         // New Android M+ permission check requirement.
229 |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
230 |             if (context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
231 |                     != PackageManager.PERMISSION_GRANTED) {
232 |                 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
233 |                 builder.setTitle("This app needs coarse location access");
234 |                 builder.setMessage("Please grant coarse location access so this app can scan for beacons");
235 |                 builder.setPositiveButton(android.R.string.ok, null);
236 |                 builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
237 |                     @Override
238 |                     public void onDismiss(DialogInterface dialog) {
239 |                         ActivityCompat.requestPermissions(context, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
240 |                                 PERMISSION_REQUEST_COARSE_LOCATION);
241 |                     }
242 |                 });
243 |                 builder.show();
244 |             }
245 |         }
246 |         BluetoothManager manager = (BluetoothManager) context.getApplicationContext()
247 |                 .getSystemService(Context.BLUETOOTH_SERVICE);
248 |         BluetoothAdapter btAdapter = manager.getAdapter();
249 |         if (btAdapter == null) {
250 |             return false;
251 |         } else if (!btAdapter.isEnabled()) {
252 |             Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
253 |             context.startActivityForResult(enableBtIntent, mRequestEnableBluetooth);
254 |             return false;
255 |         } else {
256 |             scanner = btAdapter.getBluetoothLeScanner();
257 |         }
258 |         return true;
259 |     }
260 | 
261 | 
262 |     // Checks the frame type and hands off the service data to the validation module.
263 |     private void validateServiceData(String deviceAddress, byte[] serviceData) {
264 |         Beacon beacon = deviceToBeaconMap.get(deviceAddress);
265 |         if (serviceData == null) {
266 |             String err = "Null Eddystone service data";
267 |             beacon.frameStatus.nullServiceData = err;
268 |             logDeviceError(deviceAddress, err);
269 |             return;
270 |         }
271 |         switch (serviceData[0]) {
272 |             case Constants.UID_FRAME_TYPE:
273 |                 UidValidator.validate(deviceAddress, serviceData, beacon);
274 |                 break;
275 |             case Constants.TLM_FRAME_TYPE:
276 |                 TlmValidator.validate(deviceAddress, serviceData, beacon);
277 |                 break;
278 |             case Constants.URL_FRAME_TYPE:
279 |                 UrlValidator.validate(deviceAddress, serviceData, beacon);
280 |                 break;
281 |             default:
282 |                 String err = String.format("Invalid frame type byte %02X", serviceData[0]);
283 |                 beacon.frameStatus.invalidFrameType = err;
284 |                 logDeviceError(deviceAddress, err);
285 |                 break;
286 |         }
287 |     }
288 | 
289 |     private void logErrorAndShowToast(Activity activity, String message) {
290 |         Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
291 |         Log.e(TAG, message);
292 |     }
293 | 
294 |     private void logDeviceError(String deviceAddress, String err) {
295 |         Log.e(TAG, deviceAddress + ": " + err);
296 |     }
297 | }
298 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/ble/Beacon.java:
--------------------------------------------------------------------------------
  1 | // Copyright 2015 Google Inc. All rights reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //    http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | 
 15 | package com.divertsy.hid.ble;
 16 | 
 17 | import android.net.Uri;
 18 | import android.support.annotation.Nullable;
 19 | 
 20 | /**
 21 |  *  Divertsy can use Bluetooth beacons for gathering physical location information.
 22 |  *  This is disabled by default in settings. See MainActivity for permission checks.
 23 |  */
 24 | public class Beacon {
 25 | 
 26 |     private static final String BULLET = "● ";
 27 |     final String deviceAddress;
 28 |     int rssi;
 29 |     // TODO: rename to make explicit the validation intent of this timestamp. We use it to
 30 |     // remember a recent frame to make sure that non-monotonic TLM values increase.
 31 |     long timestamp = System.currentTimeMillis();
 32 | 
 33 |     // Used to remove devices from the listview when they haven't been seen in a while.
 34 |     long lastSeenTimestamp = System.currentTimeMillis();
 35 | 
 36 |     byte[] uidServiceData;
 37 |     byte[] tlmServiceData;
 38 |     byte[] urlServiceData;
 39 | 
 40 |     class UidStatus {
 41 |         String uidValue;
 42 |         int txPower;
 43 | 
 44 |         String errTx;
 45 |         String errUid;
 46 |         String errRfu;
 47 | 
 48 |         public String getErrors() {
 49 |             StringBuilder sb = new StringBuilder();
 50 |             if (errTx != null) {
 51 |                 sb.append(BULLET).append(errTx).append("\n");
 52 |             }
 53 |             if (errUid != null) {
 54 |                 sb.append(BULLET).append(errUid).append("\n");
 55 |             }
 56 |             if (errRfu != null) {
 57 |                 sb.append(BULLET).append(errRfu).append("\n");
 58 |             }
 59 |             return sb.toString().trim();
 60 |         }
 61 |     }
 62 | 
 63 |     class TlmStatus {
 64 |         String version;
 65 |         String voltage;
 66 |         String temp;
 67 |         String advCnt;
 68 |         String secCnt;
 69 | 
 70 |         String errIdentialFrame;
 71 |         String errVersion;
 72 |         String errVoltage;
 73 |         String errTemp;
 74 |         String errPduCnt;
 75 |         String errSecCnt;
 76 |         String errRfu;
 77 | 
 78 |         public String getErrors() {
 79 |             StringBuilder sb = new StringBuilder();
 80 |             if (errIdentialFrame != null) {
 81 |                 sb.append(BULLET).append(errIdentialFrame).append("\n");
 82 |             }
 83 |             if (errVersion != null) {
 84 |                 sb.append(BULLET).append(errVersion).append("\n");
 85 |             }
 86 |             if (errVoltage != null) {
 87 |                 sb.append(BULLET).append(errVoltage).append("\n");
 88 |             }
 89 |             if (errTemp != null) {
 90 |                 sb.append(BULLET).append(errTemp).append("\n");
 91 |             }
 92 |             if (errPduCnt != null) {
 93 |                 sb.append(BULLET).append(errPduCnt).append("\n");
 94 |             }
 95 |             if (errSecCnt != null) {
 96 |                 sb.append(BULLET).append(errSecCnt).append("\n");
 97 |             }
 98 |             if (errRfu != null) {
 99 |                 sb.append(BULLET).append(errRfu).append("\n");
100 |             }
101 |             return sb.toString().trim();
102 |         }
103 | 
104 |         @Override
105 |         public String toString() {
106 |             return getErrors();
107 |         }
108 |     }
109 | 
110 |     public class UrlStatus {
111 |         String urlValue;
112 |         String urlNotSet;
113 |         String txPower;
114 | 
115 |         public String getErrors() {
116 |             StringBuilder sb = new StringBuilder();
117 |             if (txPower != null) {
118 |                 sb.append(BULLET).append(txPower).append("\n");
119 |             }
120 |             if (urlNotSet != null) {
121 |                 sb.append(BULLET).append(urlNotSet).append("\n");
122 |             }
123 |             return sb.toString().trim();
124 |         }
125 | 
126 |         @Override
127 |         public String toString() {
128 |             StringBuilder sb = new StringBuilder();
129 |             if (urlValue != null) {
130 |                 sb.append(urlValue).append("\n");
131 |             }
132 |             return sb.append(getErrors()).toString().trim();
133 |         }
134 | 
135 |         @Nullable
136 |         public Uri getUrl() {
137 |             return urlValue != null ? Uri.parse(urlValue) : null;
138 |         }
139 |     }
140 | 
141 |     class FrameStatus {
142 |         String nullServiceData;
143 |         String tooShortServiceData;
144 |         String invalidFrameType;
145 | 
146 |         public String getErrors() {
147 |             StringBuilder sb = new StringBuilder();
148 |             if (nullServiceData != null) {
149 |                 sb.append(BULLET).append(nullServiceData).append("\n");
150 |             }
151 |             if (tooShortServiceData != null) {
152 |                 sb.append(BULLET).append(tooShortServiceData).append("\n");
153 |             }
154 |             if (invalidFrameType != null) {
155 |                 sb.append(BULLET).append(invalidFrameType).append("\n");
156 |             }
157 |             return sb.toString().trim();
158 |         }
159 | 
160 |         @Override
161 |         public String toString() {
162 |             return getErrors();
163 |         }
164 |     }
165 | 
166 |     boolean hasUidFrame;
167 |     UidStatus uidStatus = new UidStatus();
168 | 
169 |     boolean hasTlmFrame;
170 |     TlmStatus tlmStatus = new TlmStatus();
171 | 
172 |     boolean hasUrlFrame;
173 |     public UrlStatus urlStatus = new UrlStatus();
174 | 
175 |     FrameStatus frameStatus = new FrameStatus();
176 | 
177 |     Beacon(String deviceAddress, int rssi) {
178 |         this.deviceAddress = deviceAddress;
179 |         this.rssi = rssi;
180 |     }
181 | 
182 |     /**
183 |      * Performs a case-insensitive contains test of s on the device address (with or without the
184 |      * colon separators) and/or the UID value, and/or the URL value.
185 |      */
186 |     boolean contains(String s) {
187 |         return s == null
188 |                 || s.isEmpty()
189 |                 || deviceAddress.replace(":", "").toLowerCase().contains(s.toLowerCase())
190 |                 || (uidStatus.uidValue != null
191 |                 && uidStatus.uidValue.toLowerCase().contains(s.toLowerCase()))
192 |                 || (urlStatus.urlValue != null
193 |                 && urlStatus.urlValue.toLowerCase().contains(s.toLowerCase()));
194 |     }
195 | }
196 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/ble/Constants.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2015 Google Inc. All rights reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //    http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package com.divertsy.hid.ble;
16 | 
17 | class Constants {
18 | 
19 |     private Constants() {
20 |     }
21 | 
22 |     /**
23 |      * Eddystone-UID frame type value.
24 |      */
25 |     static final byte UID_FRAME_TYPE = 0x00;
26 | 
27 |     /**
28 |      * Eddystone-URL frame type value.
29 |      */
30 |     static final byte URL_FRAME_TYPE = 0x10;
31 | 
32 |     /**
33 |      * Eddystone-TLM frame type value.
34 |      */
35 |     static final byte TLM_FRAME_TYPE = 0x20;
36 | 
37 |     /**
38 |      * Minimum expected Tx power (in dBm) in UID and URL frames.
39 |      */
40 |     static final int MIN_EXPECTED_TX_POWER = -100;
41 | 
42 |     /**
43 |      * Maximum expected Tx power (in dBm) in UID and URL frames.
44 |      */
45 |     static final int MAX_EXPECTED_TX_POWER = 20;
46 | 
47 | }
48 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/ble/TlmValidator.java:
--------------------------------------------------------------------------------
  1 | // Copyright 2015 Google Inc. All rights reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //    http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | 
 15 | package com.divertsy.hid.ble;
 16 | 
 17 | import android.util.Log;
 18 | 
 19 | import com.divertsy.hid.utils.Utils;
 20 | 
 21 | import java.nio.ByteBuffer;
 22 | import java.util.Arrays;
 23 | import java.util.concurrent.TimeUnit;
 24 | 
 25 | 
 26 | /**
 27 |  * Basic validation of an Eddystone-TLM frame. 
 28 |  *
 29 |  * @see TLM frame specification
 30 |  */
 31 | public class TlmValidator {
 32 | 
 33 |     private static final String TAG = TlmValidator.class.getSimpleName();
 34 | 
 35 |     // TODO: tests
 36 |     static final byte MIN_SERVICE_DATA_LEN = 14;
 37 | 
 38 |     // TLM frames only support version 0x00 for now.
 39 |     static final byte EXPECTED_VERSION = 0x00;
 40 | 
 41 |     // Minimum expected voltage value in beacon telemetry in millivolts.
 42 |     static final int MIN_EXPECTED_VOLTAGE = 500;
 43 | 
 44 |     // Maximum expected voltage value in beacon telemetry in millivolts.
 45 |     static final int MAX_EXPECTED_VOLTAGE = 10000;
 46 | 
 47 |     // Value indicating temperature not supported. temp[0] == 0x80, temp[1] == 0x00.
 48 |     static final float TEMPERATURE_NOT_SUPPORTED = -128.0f;
 49 | 
 50 |     // Minimum expected temperature value in beacon telemetry in degrees Celsius.
 51 |     static final float MIN_EXPECTED_TEMP = 0.0f;
 52 | 
 53 |     // Maximum expected temperature value in beacon telemetry in degrees Celsius.
 54 |     static final float MAX_EXPECTED_TEMP = 60.0f;
 55 | 
 56 |     // Maximum expected PDU count in beacon telemetry.
 57 |     // The fastest we'd expect to see a beacon transmitting would be about 10 Hz.
 58 |     // Given that and a lifetime of ~3 years, any value above this is suspicious.
 59 |     static final int MAX_EXPECTED_PDU_COUNT = 10 * 60 * 60 * 24 * 365 * 3;
 60 | 
 61 |     // Maximum expected time since boot in beacon telemetry.
 62 |     // Given that and a lifetime of ~3 years, any value above this is suspicious.
 63 |     static final int MAX_EXPECTED_SEC_COUNT = 10 * 60 * 60 * 24 * 365 * 3;
 64 | 
 65 |     // The service data for a TLM frame should vary with each broadcast, but depending on the
 66 |     // firmware implementation a couple of consecutive TLM frames may be broadcast. Store the
 67 |     // frame only if few seconds have passed since we last saw one.
 68 |     static final int STORE_NEXT_FRAME_DELTA_MS = 3000;
 69 | 
 70 |     private TlmValidator() {
 71 |     }
 72 | 
 73 |     static void validate(String deviceAddress, byte[] serviceData, Beacon beacon) {
 74 |         beacon.hasTlmFrame = true;
 75 | 
 76 |         byte[] previousTlm = null;
 77 |         if (beacon.tlmServiceData == null) {
 78 |             beacon.tlmServiceData = serviceData;
 79 |             beacon.timestamp = System.currentTimeMillis();
 80 |         } else if (System.currentTimeMillis() - beacon.timestamp > STORE_NEXT_FRAME_DELTA_MS) {
 81 |             beacon.timestamp = System.currentTimeMillis();
 82 |             previousTlm = beacon.tlmServiceData.clone();
 83 |             if (Arrays.equals(beacon.tlmServiceData, serviceData)) {
 84 |                 String err =
 85 |                         "TLM service data was identical to recent TLM frame:\n" + Utils
 86 |                                 .toHexString(serviceData);
 87 |                 beacon.tlmStatus.errIdentialFrame = err;
 88 |                 logDeviceError(deviceAddress, err);
 89 |                 beacon.tlmServiceData = serviceData;
 90 |             }
 91 |         }
 92 | 
 93 |         if (serviceData.length < MIN_SERVICE_DATA_LEN) {
 94 |             String err = String.format("TLM frame too short, needs at least %d bytes, got %d",
 95 |                     MIN_SERVICE_DATA_LEN, serviceData.length);
 96 |             beacon.frameStatus.tooShortServiceData = err;
 97 |             logDeviceError(deviceAddress, err);
 98 |             return;
 99 |         }
100 | 
101 |         ByteBuffer buf = ByteBuffer.wrap(serviceData);
102 |         buf.get();  // We already know the frame type byte is 0x20.
103 | 
104 |         // The version should be zero.
105 |         byte version = buf.get();
106 |         beacon.tlmStatus.version = String.format("0x%02X", version);
107 |         if (version != EXPECTED_VERSION) {
108 |             String err = String.format("Bad TLM version, expected 0x%02X, got %02X",
109 |                     EXPECTED_VERSION, version);
110 |             beacon.tlmStatus.errVersion = err;
111 |             logDeviceError(deviceAddress, err);
112 |         }
113 | 
114 |         // Battery voltage should be sane. Zero is fine if the device is externally powered, but
115 |         // it shouldn't be negative or unreasonably high.
116 |         short voltage = buf.getShort();
117 |         beacon.tlmStatus.voltage = String.valueOf(voltage);
118 |         if (voltage != 0 && (voltage < MIN_EXPECTED_VOLTAGE || voltage > MAX_EXPECTED_VOLTAGE)) {
119 |             String err = String.format("Expected TLM voltage to be between %d and %d, got %d",
120 |                     MIN_EXPECTED_VOLTAGE, MAX_EXPECTED_VOLTAGE, voltage);
121 |             beacon.tlmStatus.errVoltage = err;
122 |             logDeviceError(deviceAddress, err);
123 |         }
124 | 
125 |         // Temp varies a lot with the hardware and the margins appear to be very wide. USB beacons
126 |         // in particular can report quite high temps. Let's at least check they're partially sane.
127 |         byte tempIntegral = buf.get();
128 |         int tempFractional = (buf.get() & 0xff);
129 |         float temp = tempIntegral + (tempFractional / 256.0f);
130 |         beacon.tlmStatus.temp = String.valueOf(temp);
131 |         if (temp != TEMPERATURE_NOT_SUPPORTED) {
132 |             if (temp < MIN_EXPECTED_TEMP || temp > MAX_EXPECTED_TEMP) {
133 |                 String err = String.format("Expected TLM temperature to be between %.2f and %.2f, got %.2f",
134 |                         MIN_EXPECTED_TEMP, MAX_EXPECTED_TEMP, temp);
135 |                 beacon.tlmStatus.errTemp = err;
136 |                 logDeviceError(deviceAddress, err);
137 |             }
138 |         }
139 | 
140 |         // Check the PDU count is increasing from frame to frame and is neither too low or too high.
141 |         int advCnt = buf.getInt();
142 |         beacon.tlmStatus.advCnt = String.valueOf(advCnt);
143 |         if (advCnt <= 0) {
144 |             String err = "Expected TLM ADV count to be positive, got " + advCnt;
145 |             beacon.tlmStatus.errPduCnt = err;
146 |             logDeviceError(deviceAddress, err);
147 |         }
148 |         if (advCnt > MAX_EXPECTED_PDU_COUNT) {
149 |             String err = String.format("TLM ADV count %d is higher than expected max of %d",
150 |                     advCnt, MAX_EXPECTED_PDU_COUNT);
151 |             beacon.tlmStatus.errPduCnt = err;
152 |             logDeviceError(deviceAddress, err);
153 |         }
154 |         if (previousTlm != null) {
155 |             int previousAdvCnt = ByteBuffer.wrap(previousTlm, 6, 4).getInt();
156 |             if (previousAdvCnt == advCnt) {
157 |                 String err = "Expected increasing TLM PDU count but unchanged from " + advCnt;
158 |                 beacon.tlmStatus.errPduCnt = err;
159 |                 logDeviceError(deviceAddress, err);
160 |             }
161 |         }
162 | 
163 |         // Check that the time since boot is increasing and is neither too low nor too high.
164 |         int uptime = buf.getInt();
165 |         beacon.tlmStatus.secCnt = String.format("%d (%d days)", uptime, TimeUnit.SECONDS.toDays(uptime / 10));
166 |         if (uptime <= 0) {
167 |             String err = "Expected TLM time since boot to be positive, got " + uptime;
168 |             beacon.tlmStatus.errSecCnt = err;
169 |             logDeviceError(deviceAddress, err);
170 |         }
171 |         if (uptime > MAX_EXPECTED_SEC_COUNT) {
172 |             String err = String.format("TLM time since boot %d is higher than expected max of %d",
173 |                     uptime, MAX_EXPECTED_SEC_COUNT);
174 |             beacon.tlmStatus.errSecCnt = err;
175 |             logDeviceError(deviceAddress, err);
176 |         }
177 |         if (previousTlm != null) {
178 |             int previousUptime = ByteBuffer.wrap(previousTlm, 10, 4).getInt();
179 |             if (previousUptime == uptime) {
180 |                 String err = "Expected increasing TLM time since boot but unchanged from " + uptime;
181 |                 beacon.tlmStatus.errSecCnt = err;
182 |                 logDeviceError(deviceAddress, err);
183 |             }
184 |         }
185 | 
186 |         byte[] rfu = Arrays.copyOfRange(serviceData, 14, 20);
187 |         for (byte b : rfu) {
188 |             if (b != 0x00) {
189 |                 String err = "Expected TLM RFU bytes to be 0x00, were " + Utils.toHexString(rfu);
190 |                 beacon.tlmStatus.errRfu = err;
191 |                 logDeviceError(deviceAddress, err);
192 |                 break;
193 |             }
194 |         }
195 |     }
196 | 
197 |     private static void logDeviceError(String deviceAddress, String err) {
198 |         Log.e(TAG, deviceAddress + ": " + err);
199 |     }
200 | }
201 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/ble/UidValidator.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2015 Google Inc. All rights reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //    http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package com.divertsy.hid.ble;
16 | 
17 | import static com.divertsy.hid.ble.Constants.MAX_EXPECTED_TX_POWER;
18 | import static com.divertsy.hid.ble.Constants.MIN_EXPECTED_TX_POWER;
19 | 
20 | import android.util.Log;
21 | 
22 | import com.divertsy.hid.utils.Utils;
23 | 
24 | import java.util.Arrays;
25 | 
26 | 
27 | /**
28 |  * Basic validation of an Eddystone-UID frame. 
29 |  *
30 |  * @see UID frame specification
31 |  */
32 | public class UidValidator {
33 | 
34 |     private static final String TAG = UidValidator.class.getSimpleName();
35 | 
36 |     private UidValidator() {
37 |     }
38 | 
39 |     static void validate(String deviceAddress, byte[] serviceData, Beacon beacon) {
40 |         beacon.hasUidFrame = true;
41 | 
42 |         // Tx power should have reasonable values.
43 |         int txPower = (int) serviceData[1];
44 |         beacon.uidStatus.txPower = txPower;
45 |         if (txPower < MIN_EXPECTED_TX_POWER || txPower > MAX_EXPECTED_TX_POWER) {
46 |             String err = String
47 |                     .format("Expected UID Tx power between %d and %d, got %d", MIN_EXPECTED_TX_POWER,
48 |                             MAX_EXPECTED_TX_POWER, txPower);
49 |             beacon.uidStatus.errTx = err;
50 |             logDeviceError(deviceAddress, err);
51 |         }
52 | 
53 |         // The namespace and instance bytes should not be all zeroes.
54 |         byte[] uidBytes = Arrays.copyOfRange(serviceData, 2, 18);
55 |         beacon.uidStatus.uidValue = Utils.toHexString(uidBytes);
56 |         if (Utils.isZeroed(uidBytes)) {
57 |             String err = "UID bytes are all 0x00";
58 |             beacon.uidStatus.errUid = err;
59 |             logDeviceError(deviceAddress, err);
60 |         }
61 | 
62 |         // If we have a previous frame, verify the ID isn't changing.
63 |         if (beacon.uidServiceData == null) {
64 |             beacon.uidServiceData = serviceData.clone();
65 |         } else {
66 |             byte[] previousUidBytes = Arrays.copyOfRange(beacon.uidServiceData, 2, 18);
67 |             if (!Arrays.equals(uidBytes, previousUidBytes)) {
68 |                 String err = String.format("UID should be invariant.\nLast: %s\nthis: %s",
69 |                         Utils.toHexString(previousUidBytes),
70 |                         Utils.toHexString(uidBytes));
71 |                 beacon.uidStatus.errUid = err;
72 |                 logDeviceError(deviceAddress, err);
73 |                 beacon.uidServiceData = serviceData.clone();
74 |             }
75 |         }
76 | 
77 |         // Last two bytes in frame are RFU and should be zeroed.
78 |         byte[] rfu = Arrays.copyOfRange(serviceData, 18, 20);
79 |         if (rfu[0] != 0x00 || rfu[1] != 0x00) {
80 |             String err = "Expected UID RFU bytes to be 0x00, were " + Utils.toHexString(rfu);
81 |             beacon.uidStatus.errRfu = err;
82 |             logDeviceError(deviceAddress, err);
83 |         }
84 |     }
85 | 
86 |     private static void logDeviceError(String deviceAddress, String err) {
87 |         Log.e(TAG, deviceAddress + ": " + err);
88 |     }
89 | }
90 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/ble/UrlUtils.java:
--------------------------------------------------------------------------------
  1 | // Copyright 2015 Google Inc. All rights reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //    http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | 
 15 | package com.divertsy.hid.ble;
 16 | 
 17 | import android.util.Log;
 18 | import android.util.SparseArray;
 19 | import android.webkit.URLUtil;
 20 | 
 21 | import java.nio.BufferUnderflowException;
 22 | import java.nio.ByteBuffer;
 23 | import java.nio.ByteOrder;
 24 | import java.util.UUID;
 25 | 
 26 | /**
 27 |  * Helpers for Eddystone-URL frame validation. Copied from
 28 |  * https://github.com/google/uribeacon/android-uribeacon/uribeacon-library
 29 |  */
 30 | public class UrlUtils {
 31 |     private static final String TAG = UrlUtils.class.getSimpleName();
 32 | 
 33 |     private static final SparseArray URI_SCHEMES = new SparseArray() {{
 34 |         put((byte) 0, "http://www.");
 35 |         put((byte) 1, "https://www.");
 36 |         put((byte) 2, "http://");
 37 |         put((byte) 3, "https://");
 38 |         put((byte) 4, "urn:uuid:");
 39 |     }};
 40 | 
 41 |     private static final SparseArray URL_CODES = new SparseArray() {{
 42 |         put((byte) 0, ".com/");
 43 |         put((byte) 1, ".org/");
 44 |         put((byte) 2, ".edu/");
 45 |         put((byte) 3, ".net/");
 46 |         put((byte) 4, ".info/");
 47 |         put((byte) 5, ".biz/");
 48 |         put((byte) 6, ".gov/");
 49 |         put((byte) 7, ".com");
 50 |         put((byte) 8, ".org");
 51 |         put((byte) 9, ".edu");
 52 |         put((byte) 10, ".net");
 53 |         put((byte) 11, ".info");
 54 |         put((byte) 12, ".biz");
 55 |         put((byte) 13, ".gov");
 56 |     }};
 57 | 
 58 |     static String decodeUrl(byte[] serviceData) {
 59 |         StringBuilder url = new StringBuilder();
 60 |         int offset = 2;
 61 |         byte b = serviceData[offset++];
 62 |         String scheme = URI_SCHEMES.get(b);
 63 |         if (scheme != null) {
 64 |             url.append(scheme);
 65 |             if (URLUtil.isNetworkUrl(scheme)) {
 66 |                 return decodeUrl(serviceData, offset, url);
 67 |             } else if ("urn:uuid:".equals(scheme)) {
 68 |                 return decodeUrnUuid(serviceData, offset, url);
 69 |             }
 70 |         }
 71 |         return url.toString();
 72 |     }
 73 | 
 74 |     static String decodeUrl(byte[] serviceData, int offset, StringBuilder urlBuilder) {
 75 |         while (offset < serviceData.length) {
 76 |             byte b = serviceData[offset++];
 77 |             String code = URL_CODES.get(b);
 78 |             if (code != null) {
 79 |                 urlBuilder.append(code);
 80 |             } else {
 81 |                 urlBuilder.append((char) b);
 82 |             }
 83 |         }
 84 |         return urlBuilder.toString();
 85 |     }
 86 | 
 87 |     static String decodeUrnUuid(byte[] serviceData, int offset, StringBuilder urnBuilder) {
 88 |         ByteBuffer bb = ByteBuffer.wrap(serviceData);
 89 |         // UUIDs are ordered as byte array, which means most significant first
 90 |         bb.order(ByteOrder.BIG_ENDIAN);
 91 |         long mostSignificantBytes, leastSignificantBytes;
 92 |         try {
 93 |             bb.position(offset);
 94 |             mostSignificantBytes = bb.getLong();
 95 |             leastSignificantBytes = bb.getLong();
 96 |         } catch (BufferUnderflowException e) {
 97 |             Log.w(TAG, "decodeUrnUuid BufferUnderflowException!");
 98 |             return null;
 99 |         }
100 |         UUID uuid = new UUID(mostSignificantBytes, leastSignificantBytes);
101 |         urnBuilder.append(uuid.toString());
102 |         return urnBuilder.toString();
103 |     }
104 | 
105 | }
106 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/ble/UrlValidator.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2015 Google Inc. All rights reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //    http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package com.divertsy.hid.ble;
16 | 
17 | import android.util.Log;
18 | 
19 | import com.divertsy.hid.utils.Utils;
20 | 
21 | import java.util.Arrays;
22 | 
23 | import static com.divertsy.hid.ble.Constants.MAX_EXPECTED_TX_POWER;
24 | import static com.divertsy.hid.ble.Constants.MIN_EXPECTED_TX_POWER;
25 | 
26 | public class UrlValidator {
27 | 
28 |     private static final String TAG = UrlValidator.class.getSimpleName();
29 | 
30 |     private UrlValidator() {
31 |     }
32 | 
33 |     static void validate(String deviceAddress, byte[] serviceData, Beacon beacon) {
34 |         beacon.hasUrlFrame = true;
35 | 
36 |         // Tx power should have reasonable values.
37 |         int txPower = (int) serviceData[1];
38 |         if (txPower < MIN_EXPECTED_TX_POWER || txPower > MAX_EXPECTED_TX_POWER) {
39 |             String err = String.format("Expected URL Tx power between %d and %d, got %d",
40 |                     MIN_EXPECTED_TX_POWER, MAX_EXPECTED_TX_POWER, txPower);
41 |             beacon.urlStatus.txPower = err;
42 |             logDeviceError(deviceAddress, err);
43 |         }
44 | 
45 |         // The URL bytes should not be all zeroes.
46 |         byte[] urlBytes = Arrays.copyOfRange(serviceData, 2, 20);
47 |         if (Utils.isZeroed(urlBytes)) {
48 |             String err = "URL bytes are all 0x00";
49 |             beacon.urlStatus.urlNotSet = err;
50 |             logDeviceError(deviceAddress, err);
51 |         }
52 | 
53 |         beacon.urlStatus.urlValue = UrlUtils.decodeUrl(serviceData);
54 |     }
55 | 
56 |     private static void logDeviceError(String deviceAddress, String err) {
57 |         Log.e(TAG, deviceAddress + ": " + err);
58 |     }
59 | }
60 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/usb/ScaleMeasurement.java:
--------------------------------------------------------------------------------
  1 | package com.divertsy.hid.usb;
  2 | 
  3 | import android.support.annotation.NonNull;
  4 | import android.support.annotation.Nullable;
  5 | 
  6 | import com.divertsy.hid.ScaleApplication;
  7 | 
  8 | import java.text.SimpleDateFormat;
  9 | import java.util.Date;
 10 | 
 11 | public class ScaleMeasurement {
 12 | 
 13 |     private double scaleWeight;
 14 |     private String unit;
 15 |     private final double rawScaleWeight;
 16 |     private final long now;
 17 |     private final String date;
 18 |     private final String date_time;
 19 |     public static final String[] csv_headers = {"scalename","office","weight", "type",
 20 |             "unit","time","date","date_time","bin_info","floor","location"};
 21 | 
 22 |     public ScaleMeasurement(double scaleWeight, @NonNull String unit, double rawScaleWeight) {
 23 |         this.scaleWeight = scaleWeight;
 24 |         this.unit = unit;
 25 |         this.rawScaleWeight = rawScaleWeight;
 26 | 
 27 |         // Not going to use local format since this could change how data gets encoded
 28 |         // for the backend processing.
 29 |         this.now = System.currentTimeMillis();
 30 |         Date dateObj = new Date(this.now);
 31 |         SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd");
 32 |         this.date = s.format(dateObj);
 33 |         SimpleDateFormat t = new SimpleDateFormat("HH:mm:ss z");
 34 |         this.date_time = t.format(dateObj);
 35 |     }
 36 | 
 37 |     @NonNull
 38 |     public String toJson(@NonNull String office, @NonNull String weightType, @Nullable String floor, @Nullable String location) {
 39 |         return "[{" +
 40 |         "scalename:" + '"' + ScaleApplication.get().getDeviceId() + '"' +
 41 |         ", office:" + '"'+ office +'"' +
 42 |         ", weight:" + Double.toString(scaleWeight) +
 43 |         ", type:" + '"'+ weightType +'"' +
 44 |         ", unit:" + '"'+ unit + '"' +
 45 |         ", time:" + (int)(now / 1000) +
 46 |         ", date:" + '"'+ date + '"' +
 47 |         ", date_time:" + '"'+ date_time + '"' +
 48 |         ", bin_info:" + Double.toString(rawScaleWeight) +
 49 |         (floor == null ? "" : ", floor:" + '"' + floor + '"') +
 50 |         (location == null ? "" : ", location:" + '"' + location + '"') +
 51 |         "}]";
 52 |     }
 53 | 
 54 |     @NonNull
 55 |     private  String cleanForCSV(@Nullable String input){
 56 |         if (input == null)
 57 |             return "";
 58 |         input = input.replace('"', '\'');
 59 |         if(input.contains(",")) {
 60 |             input = '"' + input + '"';
 61 |         }
 62 |         return input;
 63 |     }
 64 | 
 65 |     @NonNull
 66 |     public String toCSV(@NonNull String office, @NonNull String weightType, @Nullable String floor, @Nullable String location) {
 67 |         return ScaleApplication.get().getDeviceId() +
 68 |                 "," + cleanForCSV(office) +
 69 |                 "," + cleanForCSV(Double.toString(scaleWeight)) +
 70 |                 "," + cleanForCSV(weightType)  +
 71 |                 "," + cleanForCSV(unit) +
 72 |                 "," + (int)(now / 1000) +
 73 |                 "," + cleanForCSV(date) +
 74 |                 "," + cleanForCSV(date_time) +
 75 |                 "," + cleanForCSV(Double.toString(rawScaleWeight)) +
 76 |                 "," + cleanForCSV(floor) +
 77 |                 "," + cleanForCSV(location) ;
 78 |     }
 79 | 
 80 |     public long getTime() {
 81 |         return now;
 82 |     }
 83 | 
 84 |     public double getScaleWeight() {
 85 |         return scaleWeight;
 86 |     }
 87 | 
 88 |     public double getRawScaleWeight() {
 89 |         return rawScaleWeight;
 90 |     }
 91 | 
 92 |     public String getScaleUnit() {
 93 |         return unit;
 94 |     }
 95 | 
 96 |     public static class Builder {
 97 |         private String units;
 98 |         public double rawScaleWeight;
 99 |         private double scaleWeight;
100 | 
101 |         public Builder units(String units) {
102 |             this.units = units;
103 |             return this;
104 |         }
105 | 
106 |         public Builder rawScaleWeight(double rawScaleWeight) {
107 |             this.rawScaleWeight = rawScaleWeight;
108 |             return this;
109 |         }
110 | 
111 |         public Builder scaleWeight(double scaleWeight) {
112 |             this.scaleWeight = scaleWeight;
113 |             return this;
114 |         }
115 | 
116 |         public ScaleMeasurement build() {
117 |             return new ScaleMeasurement(this.scaleWeight, this.units, this.rawScaleWeight);
118 |         }
119 |     }
120 | }
121 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/usb/UsbScaleManager.java:
--------------------------------------------------------------------------------
  1 | package com.divertsy.hid.usb;
  2 | 
  3 | import android.app.AlertDialog;
  4 | import android.app.PendingIntent;
  5 | import android.content.BroadcastReceiver;
  6 | import android.content.Context;
  7 | import android.content.DialogInterface;
  8 | import android.content.Intent;
  9 | import android.content.IntentFilter;
 10 | import android.hardware.usb.UsbConstants;
 11 | import android.hardware.usb.UsbDevice;
 12 | import android.hardware.usb.UsbDeviceConnection;
 13 | import android.hardware.usb.UsbEndpoint;
 14 | import android.hardware.usb.UsbInterface;
 15 | import android.hardware.usb.UsbManager;
 16 | import android.os.Bundle;
 17 | import android.os.Handler;
 18 | import android.util.Log;
 19 | 
 20 | import com.divertsy.hid.R;
 21 | import com.divertsy.hid.utils.Utils;
 22 | 
 23 | import java.util.HashMap;
 24 | import java.util.LinkedList;
 25 | import java.util.List;
 26 | import java.util.Timer;
 27 | import java.util.TimerTask;
 28 | 
 29 | public class UsbScaleManager {
 30 | 
 31 |     /*
 32 |       This Array defines the values which the DYMO scales send for the current
 33 |       unit of measurement of the connected scale. A user can change this value
 34 |       at anytime on the scale, so we must check its value when we record weight
 35 |       information. The S100, S250, and S400 scales should only report "KG" and "LBS"
 36 |      */
 37 |     public String WEIGHTUNIT[] = {"UNKNOWN", "MG", "G", "KG", "CD", "TAELS", "GR", "DWT", "TONNES", "TONS", "OZT", "OZ", "LBS"};
 38 | 
 39 |     /*
 40 |       This defines the RAW HID data which the DYMO scales send. These values
 41 |       will most likely need to change when used with other types of scales.
 42 |      */
 43 |     private static int RHID_NEGATIVE_FLAG    = 1;
 44 |     private static int RHID_UNIT_OF_MEASURE  = 2;
 45 |     private static int RHID_WEIGHT_LOW_BYTE  = 4;
 46 |     private static int RHID_WEIGHT_HIGH_BYTE = 5;
 47 | 
 48 |     long USB_READ_RATE = 200; //Time between scale reads in milliseconds
 49 | 
 50 | 
 51 | 
 52 |     private static final String TAG = UsbScaleManager.class.getName();
 53 |     private double mAddToScaleWeight;
 54 |     private final Callbacks mCallbacks;
 55 |     private ScaleMeasurement mLatestMeasurement;
 56 | 
 57 |     public ScaleMeasurement getLatestMeasurement() {
 58 |         return mLatestMeasurement;
 59 |     }
 60 | 
 61 |     public void setAddToScaleWeight(Double newWeight) {
 62 |         mAddToScaleWeight = newWeight;
 63 |     }
 64 | 
 65 |     public double getAddToScaleWeight() {
 66 |         return mAddToScaleWeight;
 67 |     }
 68 | 
 69 |     public interface Callbacks {
 70 |         void onMeasurement(ScaleMeasurement measurement);
 71 |     }
 72 | 
 73 |     private static final String ACTION_USB_PERMISSION = "com.google.android.HID.action.USB_PERMISSION";
 74 | 
 75 |     private UsbDevice device;
 76 |     private UsbManager mUsbManager;
 77 | 
 78 |     private UsbInterface intf;
 79 |     private UsbEndpoint endPointRead;
 80 |     private UsbEndpoint endPointWrite;
 81 |     private UsbDeviceConnection connection;
 82 |     private int packetSize;
 83 |     private PendingIntent mPermissionIntent;
 84 |     private Timer myTimer = new Timer();
 85 |     private final Handler uiHandler = new Handler();
 86 | 
 87 |     private AlertDialog adScaleWarning;
 88 | 
 89 |     /*
 90 |      * This gets called if a USB Device is plugged in or removed while our app is running
 91 |      */
 92 |     private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
 93 |         public void onReceive(Context context, Intent intent) {
 94 |             Log.d(TAG, "Entered BroadcastReceiver onReceive");
 95 |             String action = intent.getAction();
 96 |             Log.d(TAG, "Action was: " + action);
 97 | 
 98 |             if (ACTION_USB_PERMISSION.equals(action)) {
 99 |                 //TODO: maybe check for failure?
100 |                 synchronized (this) {
101 |                     device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
102 |                     setUSBDevice(device);
103 |                 }
104 |             }
105 |             if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
106 |                 synchronized (this) {
107 |                     device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
108 |                     if (mUsbManager.hasPermission(device)) {
109 |                         setUSBDevice(device);
110 |                     } else {
111 |                         mUsbManager.requestPermission(device, mPermissionIntent);
112 |                     }
113 |                 }
114 |                 if (device == null) {
115 |                     Log.d(TAG, "device connected");
116 |                 }
117 |             }
118 |             if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
119 |                 if (device != null) {
120 |                     device = null;
121 |                 }
122 |                 Log.d(TAG, "device disconnected");
123 |             }
124 |         }
125 | 
126 |     };
127 | 
128 |     public UsbScaleManager(Context context, Intent intent, Callbacks callbacks, Bundle savedInstanceState) {
129 |         mCallbacks = callbacks;
130 | 
131 |         mPermissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), 0);
132 | 
133 |         mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
134 |         UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
135 |         if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
136 |             setUSBDevice(device);
137 |         } else {
138 |             searchForDevice(context);
139 |         }
140 |     }
141 | 
142 |     public void onStart(Context context) {
143 |         IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
144 |         filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
145 |         filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
146 |         context.registerReceiver(mUsbReceiver, filter);
147 | 
148 |         setupScaleDataListener();
149 |     }
150 | 
151 |     public void onStop(Context context) {
152 |         try {
153 |             context.unregisterReceiver(mUsbReceiver);
154 |         } catch (Exception e) {
155 |             Log.e(TAG, "OnStop:" + e.getMessage());
156 |         }
157 |     }
158 | 
159 |     public HashMap getDeviceList() {
160 |         return mUsbManager.getDeviceList();
161 |     }
162 | 
163 |     /*
164 |      * This gets called if a USB Device is plugged in or removed while our app is running
165 |      */
166 |     private void setUSBDevice(UsbDevice device) {
167 |         Log.d(TAG, "Selected device VID:" + Integer.toHexString(device.getVendorId()) + " PID:" + Integer.toHexString(device.getProductId()));
168 | 
169 |         // Close this since we should now have a USB device
170 |         if (adScaleWarning != null && adScaleWarning.isShowing()) {
171 |             adScaleWarning.dismiss();
172 |         }
173 | 
174 |         connection = mUsbManager.openDevice(device);
175 |         Log.d(TAG, "USB Interface count: " + device.getInterfaceCount());
176 |         intf = device.getInterface(0);
177 |         if (null == connection) {
178 |             Log.e(TAG, "USB Error - unable to establish connection");
179 |         } else {
180 |             connection.claimInterface(intf, true);
181 |         }
182 |         try {
183 |             Log.d(TAG, "Interface endpoints: " + intf.getEndpointCount());
184 |             if (UsbConstants.USB_DIR_IN == intf.getEndpoint(0).getDirection()) {
185 |                 endPointRead = intf.getEndpoint(0);
186 |                 packetSize = endPointRead.getMaxPacketSize();
187 |                 Log.d(TAG, "USB PacketSIZE: " + packetSize );
188 |             }
189 |         } catch (Exception e) {
190 |             Log.wtf(TAG, "Device have no endPointRead. WHAT DID YOU PLUG IN?", e);
191 |         }
192 |     }
193 | 
194 |     private void searchForDevice(Context context) {
195 |         HashMap devices = mUsbManager.getDeviceList();
196 |         UsbDevice selected = null;
197 |         int num_of_devices = devices.size();
198 | 
199 |         if (num_of_devices == 1) {
200 |             //If there's only one device, go ahead and connect. YOLO to keyboards!
201 |             for (UsbDevice device : devices.values()) {
202 |                 selected = device;
203 |             }
204 | 
205 |             if (mUsbManager.hasPermission(selected)) {
206 |                 setUSBDevice(selected);
207 |             } else {
208 |                 mUsbManager.requestPermission(selected, mPermissionIntent);
209 |             }
210 |         } else {
211 |             if (num_of_devices > 1) {
212 |                 Log.wtf(TAG, "Extra devices are plugged in. Found: " + num_of_devices);
213 |             }
214 |             showListOfDevices(context);
215 |         }
216 | 
217 |     }
218 | 
219 |     /*
220 |      * This should no longer get called unless the user has done something strange
221 |      * to have more than one USB device connect. This could happen if they use a
222 |      * USB Hub.
223 |      */
224 |     void showListOfDevices(Context context) {
225 | 
226 |         AlertDialog.Builder alertBuilder = new AlertDialog.Builder(context);
227 | 
228 |         if (getDeviceList().isEmpty()) {
229 |             alertBuilder.setTitle(R.string.usb_connect_title)
230 |                     .setPositiveButton(R.string.ok, null);
231 |         } else {
232 |             alertBuilder.setTitle(R.string.usb_select_title);
233 |             List list = new LinkedList<>();
234 |             for (UsbDevice usbDevice : getDeviceList().values()) {
235 |                 list.add("devID:" + usbDevice.getDeviceId() + " VID:" + Integer.toHexString(usbDevice.getVendorId()) + " PID:" + Integer.toHexString(usbDevice.getProductId()) + " " + usbDevice.getDeviceName());
236 |             }
237 |             final CharSequence devicesName[] = new CharSequence[getDeviceList().size()];
238 |             list.toArray(devicesName);
239 |             alertBuilder.setItems(devicesName, new DialogInterface.OnClickListener() {
240 |                 @Override
241 |                 public void onClick(DialogInterface dialog, int which) {
242 |                     UsbDevice device = (UsbDevice) getDeviceList().values().toArray()[which];
243 |                     mUsbManager.requestPermission(device, mPermissionIntent);
244 |                 }
245 |             });
246 |         }
247 |         alertBuilder.setCancelable(true);
248 |         adScaleWarning = alertBuilder.show();
249 |     }
250 | 
251 |     /**
252 |      * This handles the raw USB data packet from the scale and decodes it to a readable format.
253 |      * Only tested with 2 scales, so may need to update this if the hardware changes.
254 |      */
255 |     private void setupScaleDataListener() {
256 |         myTimer.schedule(new TimerTask() {
257 |             @Override
258 |             public void run() {
259 |                 try {
260 |                     if (connection != null && endPointRead != null) {
261 |                         final byte[] buffer = new byte[packetSize];
262 |                         final int status = connection.bulkTransfer(endPointRead, buffer, packetSize, 300);
263 |                         uiHandler.post(new Runnable() {
264 |                             @Override
265 |                             public void run() {
266 |                                 ScaleMeasurement.Builder measurementBuilder = new ScaleMeasurement.Builder();
267 |                                 if (status >= 0) {
268 | 
269 |                                     StringBuilder stringBuilder = new StringBuilder();
270 |                                     stringBuilder.append("DEBUG USB IN:");
271 |                                     for (int i = 0; i < packetSize; i++) {
272 |                                         stringBuilder.append(" ").append(String.valueOf(Utils.toInt(buffer[i])));
273 |                                     }
274 | 
275 |                                     if (packetSize >= 5) {
276 |                                         double weight = (256 * Utils.toInt(buffer[RHID_WEIGHT_HIGH_BYTE]))
277 |                                                 + Utils.toInt(buffer[RHID_WEIGHT_LOW_BYTE]);
278 | 
279 |                                         if (Utils.toInt(buffer[RHID_UNIT_OF_MEASURE]) < WEIGHTUNIT.length){
280 |                                             measurementBuilder.units(WEIGHTUNIT[Utils.toInt(buffer[RHID_UNIT_OF_MEASURE])]);
281 |                                             if (!WEIGHTUNIT[Utils.toInt(buffer[RHID_UNIT_OF_MEASURE])].equals("G")) {
282 |                                                 //This is correct for at least LBS, OZ, and KG... maybe others?
283 |                                                 weight = weight * 0.1;
284 |                                             }
285 |                                         } else {
286 |                                             Log.e(TAG, "USB DATA ERROR - RHID_UNIT_OF_MEASURE not a known value in WEIGHTUNIT array");
287 |                                         }
288 | 
289 |                                         //Fix edge cases of double weight
290 |                                         weight = Utils.round(weight, 1);
291 | 
292 |                                         //Check for Negative Numbers
293 |                                         if (Utils.toInt(buffer[RHID_NEGATIVE_FLAG]) == 5) {
294 |                                             // Int 5 seems to indicate a negative value on S250 scales
295 |                                             // however, column 5 is still positive
296 |                                             weight = 0 - weight;
297 |                                         }
298 | 
299 |                                         //Remove any default values from the weight
300 |                                         measurementBuilder.rawScaleWeight(weight);
301 |                                         weight = weight - mAddToScaleWeight;
302 |                                         weight = Utils.round(weight, 1);
303 | 
304 |                                         stringBuilder.append(" Weight: ").append(String.valueOf(weight));
305 | 
306 |                                         measurementBuilder.scaleWeight(weight);
307 | 
308 |                                     } else {
309 |                                         stringBuilder.append("ERROR: USB packetSize too small");
310 |                                     }
311 | 
312 |                                     mLatestMeasurement = measurementBuilder.build();
313 | 
314 |                                     Log.v(TAG, stringBuilder.toString());
315 | 
316 |                                     mCallbacks.onMeasurement(mLatestMeasurement);
317 |                                 }
318 |                             }
319 |                         });
320 |                     }
321 |                 } catch (Exception e) {
322 |                     Log.e(TAG, "Exception: " + e.getLocalizedMessage());
323 |                 }
324 |             }
325 |         }, 0L, USB_READ_RATE);
326 |     }
327 | }
328 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/utils/AppUpdater.java:
--------------------------------------------------------------------------------
 1 | package com.divertsy.hid.utils;
 2 | 
 3 | import android.os.Environment;
 4 | import android.support.annotation.Nullable;
 5 | import android.util.Log;
 6 | 
 7 | import java.io.File;
 8 | import java.util.Arrays;
 9 | import java.util.Comparator;
10 | 
11 | /**
12 |  *  AppUpdater is called when application starts and checks for a new APK in a specific directory.
13 |  *  This allows remote updates if using a 3rd party syncing tool.
14 |  */
15 | public class AppUpdater {
16 | 
17 |     private static final String TAG = AppUpdater.class.getName();
18 | 
19 |     private static final String SD_CARD_PATH = Environment.getExternalStorageDirectory().getPath() + "/Divertsy/";
20 | 
21 |     @Nullable
22 |     public static File checkAppUpdate() {
23 |         Log.d(TAG, "Entering Update Check");
24 | 
25 |         // start at the set path
26 |         String filePath = SD_CARD_PATH;
27 |         File folder = new File(filePath);
28 |         File[] files = folder.listFiles();
29 | 
30 |         if (files != null && files.length > 0) {
31 |             try {
32 |                 Arrays.sort(files, new Comparator() {
33 |                     public int compare(Object o1, Object o2) {
34 | 
35 |                         Integer loc = o1.toString().indexOf(".");
36 |                         String file1tag = o1.toString().substring(loc - 6, loc);
37 |                         loc = o2.toString().indexOf(".");
38 |                         String file2tag = o1.toString().substring(loc - 6, loc);
39 | 
40 |                         if (Integer.parseInt(file1tag) > Integer.parseInt(file2tag)) {
41 |                             return -1;
42 |                         } else if (Integer.parseInt(file1tag) < Integer.parseInt(file2tag)) {
43 |                             return +1;
44 |                         } else {
45 |                             return 0;
46 |                         }
47 |                     }
48 | 
49 |                 });
50 | 
51 |                 if (files[0].exists()) {
52 |                     filePath = files[0].toString();
53 |                     Log.d(TAG, "Found update file:" + filePath);
54 |                     Integer loc = filePath.indexOf(".");
55 |                     if (Integer.parseInt(filePath.substring(loc - 6, loc)) > Utils.getBuildNumber()) {
56 |                         Log.i(TAG, "Starting Update from File: " + filePath);
57 |                         return new File(filePath);
58 |                     }
59 | 
60 |                 } else {
61 |                     Log.d(TAG, "No update files found.");
62 |                 }
63 |             } catch (Exception e) {
64 |                 Log.e(TAG, "UPDATE CHECK FAIL. Check for INVALID file in Update Folder!");
65 |                 Log.e(TAG, e.toString());
66 |             }
67 |         }
68 | 
69 |         return null;
70 |     }
71 | }
72 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/utils/Utils.java:
--------------------------------------------------------------------------------
  1 | package com.divertsy.hid.utils;
  2 | 
  3 | 
  4 | import android.os.Environment;
  5 | import android.util.Log;
  6 | import android.text.TextUtils;
  7 | 
  8 | import com.divertsy.hid.BuildConfig;
  9 | import com.divertsy.hid.usb.ScaleMeasurement;
 10 | 
 11 | import java.io.File;
 12 | import java.io.FileWriter;
 13 | import java.io.IOException;
 14 | import java.io.RandomAccessFile;
 15 | import java.math.BigDecimal;
 16 | import java.math.RoundingMode;
 17 | import java.text.SimpleDateFormat;
 18 | import java.util.Date;
 19 | import java.util.Locale;
 20 | 
 21 | /**
 22 |  *  Utils for conversions and handles saving data to a local file
 23 |  */
 24 | public class Utils {
 25 | 
 26 |     private static final String TAG = "DIVERTSY";
 27 |     private static final String LOG_BASE_DIR = Environment.getExternalStorageDirectory().getPath() + "/Documents/";
 28 |     private static final String LOG_FILENAME = "divertsy";
 29 | 
 30 |     public static String getDivertsyFilePath(String office){
 31 |         return LOG_BASE_DIR + "/" + LOG_FILENAME + "-" + office + ".csv";
 32 |     }
 33 | 
 34 | 
 35 |     public static void saveCSV(String office, String text) {
 36 |         // Every device should have a /sdcard/ but not all will have "Documents"
 37 |         File logFile = new File(getDivertsyFilePath(office));
 38 |         File basedir = logFile.getParentFile();
 39 |         if (!basedir.exists()) {
 40 |             boolean result = basedir.mkdirs();
 41 |             if (!result) {
 42 |                 Log.e(TAG, "Failed to make save directory");
 43 |             }
 44 |         }
 45 |         if (!logFile.exists()) {
 46 |             try {
 47 |                 boolean result = logFile.createNewFile();
 48 |                 if (!result) {
 49 |                     Log.e(TAG, "Failed to make CSV file");
 50 |                 }
 51 |                 // Write CSV file header
 52 |                 String headers = TextUtils.join(",", ScaleMeasurement.csv_headers);
 53 |                 FileWriter file = new FileWriter(logFile,true);
 54 |                 file.write(headers + System.getProperty("line.separator"));
 55 |                 file.close();
 56 | 
 57 |             } catch (IOException e) {
 58 |                 Log.e(TAG, e.getMessage());
 59 |                 e.printStackTrace();
 60 |                 System.exit(-1);
 61 |             }
 62 |         }
 63 |         try {
 64 |             FileWriter file = new FileWriter(logFile,true);
 65 |             file.write(text + System.getProperty("line.separator"));
 66 |             file.close();
 67 | 
 68 |         } catch (Exception e) {
 69 |             Log.e(TAG, e.getMessage());
 70 |         }
 71 |     }
 72 | 
 73 | 
 74 |     public static double round(double value, int places) {
 75 |         if (places < 0) throw new IllegalArgumentException();
 76 | 
 77 |         BigDecimal bd = new BigDecimal(value);
 78 |         bd = bd.setScale(places, RoundingMode.HALF_UP);
 79 |         return bd.doubleValue();
 80 |     }
 81 | 
 82 |     public static int toInt(byte b) {
 83 |         return (int) b & 0xFF;
 84 |     }
 85 | 
 86 |     /**
 87 |      * The BuildNumber as generated by gradle during the build process
 88 |      *
 89 |      * @return The BuildNumber which is a timestamp
 90 |      */
 91 | 
 92 |     public static Integer getBuildNumber(){
 93 |         SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd", Locale.US);
 94 |         return Integer.parseInt(sdf.format(BuildConfig.buildTime));
 95 |     }
 96 | 
 97 |     public static boolean isZeroed(byte[] bytes) {
 98 |         for (byte b : bytes) {
 99 |             if (b != 0x00) {
100 |                 return false;
101 |             }
102 |         }
103 |         return true;
104 |     }
105 | 
106 |     private static final char[] HEX = "0123456789ABCDEF".toCharArray();
107 | 
108 |     public static String toHexString(byte[] bytes) {
109 |         if (bytes.length == 0) {
110 |             return "";
111 |         }
112 |         char[] chars = new char[bytes.length * 2];
113 |         for (int i = 0; i < bytes.length; i++) {
114 |             int c = bytes[i] & 0xFF;
115 |             chars[i * 2] = HEX[c >>> 4];
116 |             chars[i * 2 + 1] = HEX[c & 0x0F];
117 |         }
118 |         return new String(chars).toLowerCase();
119 |     }
120 | 
121 | }
122 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/java/com/divertsy/hid/utils/WeightRecorder.java:
--------------------------------------------------------------------------------
 1 | package com.divertsy.hid.utils;
 2 | 
 3 | import android.content.Context;
 4 | import android.content.SharedPreferences;
 5 | 
 6 | import java.text.SimpleDateFormat;
 7 | import java.util.Date;
 8 | import java.util.Set;
 9 | 
10 | /**
11 |  *  WeightRecorder gets and stores values in shared preferences
12 |  */
13 | public class WeightRecorder {
14 | 
15 |     private static final String TAG = WeightRecorder.class.getName();
16 | 
17 |     public static final String PREFERENCES_NAME = "ScalePrefs";
18 |     public static final String PREF_ADD_TO_SCALE = "add_to_scale";
19 |     public static final String PREF_OFFICE = "office";
20 |     public static final String PREF_USE_BIN_WEIGHT = "use_bin_weight";
21 |     public static final String PREF_WASTE_STREAMS = "waste_streams";
22 |     public static final String PREF_TARE_AFTER_ADD = "tare_after_add";
23 |     public static final String PREF_USE_BEACONS = "use_beacons";
24 |     public static final String PREF_LANGUAGE = "language";
25 | 
26 |     public static final String DEFAULT_OFFICE = "UNKNOWN";
27 |     public static final String NO_SAVED_DATA = "Unknown Last Upload";
28 |     private static final String PREF_LAST_SAVED_DATA = "last_saved_data";
29 | 
30 |     SharedPreferences mSharedPreferences;
31 | 
32 |     public WeightRecorder(Context context) {
33 |         mSharedPreferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
34 |     }
35 | 
36 |     public String getLastRecordedWeight(){
37 |         return mSharedPreferences.getString(PREF_LAST_SAVED_DATA, NO_SAVED_DATA);
38 |     }
39 | 
40 |     public void saveAsLastRecordedWeight(String weight, String trashType){
41 |         String saveText;
42 |         SimpleDateFormat s = new SimpleDateFormat("E MM-dd HH:mm");
43 |         String sdate = s.format(new Date());
44 | 
45 |         saveText = weight + " " + trashType + " @ " + sdate;
46 | 
47 |         mSharedPreferences.edit()
48 |                 .putString(PREF_LAST_SAVED_DATA, saveText)
49 |                 .apply();
50 |     }
51 | 
52 |     public void setUseBeacons(Boolean value){
53 |         mSharedPreferences.edit()
54 |                 .putBoolean(PREF_USE_BEACONS, value)
55 |                 .apply();
56 |     }
57 | 
58 |     private double getSavedDouble(String key, double defaultValue) {
59 |         return Double.valueOf(mSharedPreferences.getString(key, Double.toString(defaultValue)));
60 |     }
61 | 
62 |     public Double getDefaultWeight() {
63 |         if (useBinWeight()) {
64 |             return getSavedDouble(PREF_ADD_TO_SCALE, 0);
65 |         }
66 |         return 0.0;
67 |     }
68 | 
69 |     public String getOffice() {
70 |         return mSharedPreferences.getString(PREF_OFFICE, DEFAULT_OFFICE);
71 |     }
72 | 
73 |     public boolean useBinWeight() {
74 |         return mSharedPreferences.getBoolean(PREF_USE_BIN_WEIGHT, false);
75 |     }
76 | 
77 |     public boolean tareAfterAdd() {
78 |         return mSharedPreferences.getBoolean(PREF_TARE_AFTER_ADD, false);
79 |     }
80 | 
81 |     public boolean useBluetoothBeacons() {
82 |         return mSharedPreferences.getBoolean(PREF_USE_BEACONS, false);
83 |     }
84 | 
85 |     public Set getEnabledStreams() {
86 |         return mSharedPreferences.getStringSet(PREF_WASTE_STREAMS,null);
87 |     }
88 | 
89 |     public boolean isOfficeNameSet(){
90 |         if (getOffice().equalsIgnoreCase(DEFAULT_OFFICE)) {
91 |             if (getLastRecordedWeight().equalsIgnoreCase(NO_SAVED_DATA)) {
92 |                 return false;
93 |             }
94 |         }
95 |         return true;
96 |     }
97 | }
98 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable-hdpi/divertsybg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etsy/divertsy-client/a36f6e978322cc569c22208666fbc44f288c1569/divertsy_client/src/main/res/drawable-hdpi/divertsybg.png
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etsy/divertsy-client/a36f6e978322cc569c22208666fbc44f288c1569/divertsy_client/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable-mdpi/divertsybg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etsy/divertsy-client/a36f6e978322cc569c22208666fbc44f288c1569/divertsy_client/src/main/res/drawable-mdpi/divertsybg.png
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etsy/divertsy-client/a36f6e978322cc569c22208666fbc44f288c1569/divertsy_client/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable-xhdpi/divertsybg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etsy/divertsy-client/a36f6e978322cc569c22208666fbc44f288c1569/divertsy_client/src/main/res/drawable-xhdpi/divertsybg.png
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etsy/divertsy-client/a36f6e978322cc569c22208666fbc44f288c1569/divertsy_client/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable-xxhdpi/divertsybg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etsy/divertsy-client/a36f6e978322cc569c22208666fbc44f288c1569/divertsy_client/src/main/res/drawable-xxhdpi/divertsybg.png
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etsy/divertsy-client/a36f6e978322cc569c22208666fbc44f288c1569/divertsy_client/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable/ic_business_black_24dp.xml:
--------------------------------------------------------------------------------
 1 | 
 6 |     
 9 | 
10 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable/ic_info_black_24dp.xml:
--------------------------------------------------------------------------------
 1 | 
 6 |     
 9 | 
10 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/drawable/ic_sync_black_24dp.xml:
--------------------------------------------------------------------------------
1 | 
6 |     
9 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
  1 | 
 13 | 
 14 |     
 18 | 
 19 |         
 25 | 
 26 |         
 27 | 
 28 |         
 31 |         
 37 | 
 38 |                 
 43 |                     
 50 | 
 51 |                     
 61 |             
 62 |         
 63 | 
 64 |         
 70 | 
 71 | 
 72 |         
 73 | 
 74 |         
 80 | 
 81 |         
 82 | 
 83 |         
 90 | 
 91 |             
101 | 
102 | 
103 | 
104 |             
110 | 
111 |         
112 | 
113 | 
114 |     
115 | 
116 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/layout/manual_weight_entry.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 5 | 
 6 |     
15 | 
16 |     
24 | 
25 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/menu/main_actions.xml:
--------------------------------------------------------------------------------
 1 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/raw/waste_streams.json:
--------------------------------------------------------------------------------
 1 | [{
 2 |   "display_name": "Landfill",
 3 |   "logged_data_name": "trash",
 4 |   "button_color": "#FF222222",
 5 |   "is_default": true,
 6 |   "display_name_de": "Mülldeponie",
 7 |   "display_name_fr": "Déchets",
 8 |   "display_name_es": "Vertedero"
 9 | }, {
10 |   "display_name": "Incinerated Waste",
11 |   "logged_data_name": "incinerated",
12 |   "button_color": "#FF222222",
13 |   "is_default": false
14 | }, {
15 |   "display_name": "Mixed Recycling",
16 |   "logged_data_name": "mixed",
17 |   "button_color": "#FF003DDC",
18 |   "is_default": false,
19 |   "display_name_de": "Gemischt",
20 |   "display_name_fr": "Mixte",
21 |   "display_name_es": "Reciclaje Mixto"
22 | }, {
23 |   "display_name": "Glass",
24 |   "logged_data_name": "glass",
25 |   "button_color": "#FF808080",
26 |   "is_default": false,
27 |   "display_name_de": "Glasflasche",
28 |   "display_name_fr": "Verre",
29 |   "display_name_es": "Vidrio"
30 | }, {
31 |   "display_name": "Metal/Plastic/Glass",
32 |   "logged_data_name": "bottles",
33 |   "button_color": "#FF808080",
34 |   "is_default": true,
35 |   "display_name_de": "Metalle/Glasflasche",
36 |   "display_name_fr": "Métal/canette",
37 |   "display_name_es": "Metal/Plastico/Vidrio"
38 | }, {
39 |   "display_name": "Plastic Packaging",
40 |   "logged_data_name": "plastic_packaging",
41 |   "button_color": "#FFEF4123",
42 |   "is_default": true,
43 |   "display_name_de": "Tasche",
44 |   "display_name_fr": "Sacs",
45 |   "display_name_es": "Embalaje de plástico"
46 | }, {
47 |   "display_name": "Food Waste",
48 |   "logged_data_name": "compost",
49 |   "button_color": "#FF64462B",
50 |   "is_default": true,
51 |   "display_name_de": "Kompost",
52 |   "display_name_fr": "Compost",
53 |   "display_name_es": "Compost"
54 | }, {
55 |   "display_name": "Paper/Cardboard",
56 |   "logged_data_name": "paper",
57 |   "button_color": "#FF00A100",
58 |   "is_default": true,
59 |   "display_name_de": "Papier",
60 |   "display_name_fr": "Papier",
61 |   "display_name_es": "Papel"
62 | }, {
63 |   "display_name": "eWaste",
64 |   "logged_data_name": "ewaste",
65 |   "button_color": "#FF5f2291",
66 |   "is_default": false,
67 |   "display_name_de": "Elektronisch",
68 |   "display_name_fr": "Électronique",
69 |   "display_name_es": "Electrónicos"
70 | }]
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     Divertsy
 4 |     Gewicht geschickt
 5 |     Einstellungen
 6 |     Gewicht hinzufügen
 7 |     Anhängen an E-Mail
 8 |     Synchronisierung mit Drive
 9 |     anschließen waage
10 |     wählen USB-Briefwaage
11 |     Null/Tara
12 |     Fehler
13 |     OK
14 |     Negative Wert -  Nicht berichten
15 |     Fehlerberichterstattung
16 |     Diese App benötigt externen Speicherzugriff
17 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     Divertsy
 4 |     poids envoyé
 5 |     Paramétres
 6 |     Ajouter du Poids
 7 |     Envoyer par email
 8 |     Enregistrer sur Drive
 9 |     brancher la balance
10 |     choisir USB balance
11 |     Zéro/Tare
12 |     Erreur
13 |     D\'accord
14 |     Valeur négative - Ne pas signaler
15 |     Valeur de rapport d\'erreur
16 |     Cette application nécessite un accès de stockage externe
17 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 | 
2 |     
5 |     64dp
6 | 
7 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 | 
2 |     
3 |     16dp
4 |     16dp
5 | 
6 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/values/divertsy_default_strings.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |     
10 |     
11 |         - @string/office_custom_choice
 
12 |     
13 | 
14 |     OFFICE
15 | 
16 | 
17 | 
18 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     Divertsy
 4 |     0.0
 5 |     Zero/Tare
 6 |     
 7 | 
 8 |     Configure your Divertsy App
 9 |     It looks like you have not set your Location information. Please set an Office Name and Waste Streams. The Office Name is used to tag this location.
10 | 
11 |     This app needs external storage access
12 |     Please grant external storage access so this app can save your scale data. This app does not read photos or other data, simply writes the scale data in an area that can be shared.
13 | 
14 |     Enter Manual Data
15 |     Use this to enter a scale weight
16 | 
17 | 
18 |     Weight Data Recorded
19 |     Negative Value - Not Reporting
20 |     Error Reporting Value
21 |     Error
22 |     OK
23 | 
24 |     Please select the USB SCALE
25 |     Please CONNECT the Scale
26 | 
27 |     
28 |     Settings
29 |     General
30 |     Attach to Email
31 |     Sync to Drive
32 | 
33 | 
34 |     
35 |         - KG
 
36 |         - LBS
 
37 |         - OZ
 
38 |         - G
 
39 |         - TONS
 
40 |     
41 | 
42 |     Language (for buttons)
43 |     English
44 |     
45 |         - English
 
46 |         - Spanish
 
47 |         - German
 
48 |         - French
 
49 |     
50 | 
51 |     
52 |          
53 |         - es
 
54 |         - de
 
55 |         - fr
 
56 |     
57 | 
58 |     Location
59 |     Office Name
60 |     Custom (set name)
61 | 
62 |     Waste Streams
63 | 
64 |     Use Beacons
65 |     Will attempt to turn on Bluetooth and look for Eddystone URL location beacons.
66 | 
67 |     Enable Tare After Add
68 |     Tare scale after pressing a waste stream button
69 | 
70 |     Use Bin Weight
71 |     Subtract the value specified for bin weight from the scale reading
72 | 
73 |     Bin Weight
74 | 
75 |     
76 |     Sync
77 |     Use Google Drive
78 |     Save Data to Google Drive
79 |     Last Saved Sync Time (wifi required for update)
80 |     Linked File Identifier
81 |     DISCONNECT GOOGLE DRIVE
82 |     Unlink the current Google Drive Account
83 | 
84 |     
85 |          
86 |     
87 | 
88 | 
89 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |     
4 |     
7 | 
8 | 
9 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/xml/device_filter.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |     
4 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/xml/pref_general.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
 7 | 
 8 |     
12 | 
13 | 
14 |     
18 | 
19 |     
27 | 
28 |     
34 | 
35 | 
36 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/xml/pref_headers.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
 7 | 
 8 |     
12 | 
13 |     
17 | 
18 | 
19 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/xml/pref_location.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
11 | 
12 |     
18 | 
19 |     
27 | 
28 | 
29 | 
--------------------------------------------------------------------------------
/divertsy_client/src/main/res/xml/pref_sync.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
10 | 
11 |     
17 | 
18 |     
24 | 
25 |     
31 | 
32 | 
33 | 
34 | 
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
 1 | # Project-wide Gradle settings.
 2 | 
 3 | # IDE (e.g. Android Studio) users:
 4 | # Settings specified in this file will override any Gradle settings
 5 | # configured through the IDE.
 6 | 
 7 | # For more details on how to configure your build environment visit
 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
 9 | 
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 | 
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etsy/divertsy-client/a36f6e978322cc569c22208666fbc44f288c1569/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Mar 07 17:30:33 EST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
7 | 
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env bash
  2 | 
  3 | ##############################################################################
  4 | ##
  5 | ##  Gradle start up script for UN*X
  6 | ##
  7 | ##############################################################################
  8 | 
  9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
 10 | DEFAULT_JVM_OPTS=""
 11 | 
 12 | APP_NAME="Gradle"
 13 | APP_BASE_NAME=`basename "$0"`
 14 | 
 15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
 16 | MAX_FD="maximum"
 17 | 
 18 | warn ( ) {
 19 |     echo "$*"
 20 | }
 21 | 
 22 | die ( ) {
 23 |     echo
 24 |     echo "$*"
 25 |     echo
 26 |     exit 1
 27 | }
 28 | 
 29 | # OS specific support (must be 'true' or 'false').
 30 | cygwin=false
 31 | msys=false
 32 | darwin=false
 33 | case "`uname`" in
 34 |   CYGWIN* )
 35 |     cygwin=true
 36 |     ;;
 37 |   Darwin* )
 38 |     darwin=true
 39 |     ;;
 40 |   MINGW* )
 41 |     msys=true
 42 |     ;;
 43 | esac
 44 | 
 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
 46 | if $cygwin ; then
 47 |     [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
 48 | fi
 49 | 
 50 | # Attempt to set APP_HOME
 51 | # Resolve links: $0 may be a link
 52 | PRG="$0"
 53 | # Need this for relative symlinks.
 54 | while [ -h "$PRG" ] ; do
 55 |     ls=`ls -ld "$PRG"`
 56 |     link=`expr "$ls" : '.*-> \(.*\)$'`
 57 |     if expr "$link" : '/.*' > /dev/null; then
 58 |         PRG="$link"
 59 |     else
 60 |         PRG=`dirname "$PRG"`"/$link"
 61 |     fi
 62 | done
 63 | SAVED="`pwd`"
 64 | cd "`dirname \"$PRG\"`/" >&-
 65 | APP_HOME="`pwd -P`"
 66 | cd "$SAVED" >&-
 67 | 
 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
 69 | 
 70 | # Determine the Java command to use to start the JVM.
 71 | if [ -n "$JAVA_HOME" ] ; then
 72 |     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
 73 |         # IBM's JDK on AIX uses strange locations for the executables
 74 |         JAVACMD="$JAVA_HOME/jre/sh/java"
 75 |     else
 76 |         JAVACMD="$JAVA_HOME/bin/java"
 77 |     fi
 78 |     if [ ! -x "$JAVACMD" ] ; then
 79 |         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
 80 | 
 81 | Please set the JAVA_HOME variable in your environment to match the
 82 | location of your Java installation."
 83 |     fi
 84 | else
 85 |     JAVACMD="java"
 86 |     which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 87 | 
 88 | Please set the JAVA_HOME variable in your environment to match the
 89 | location of your Java installation."
 90 | fi
 91 | 
 92 | # Increase the maximum file descriptors if we can.
 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
 94 |     MAX_FD_LIMIT=`ulimit -H -n`
 95 |     if [ $? -eq 0 ] ; then
 96 |         if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
 97 |             MAX_FD="$MAX_FD_LIMIT"
 98 |         fi
 99 |         ulimit -n $MAX_FD
100 |         if [ $? -ne 0 ] ; then
101 |             warn "Could not set maximum file descriptor limit: $MAX_FD"
102 |         fi
103 |     else
104 |         warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 |     fi
106 | fi
107 | 
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 |     GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 | 
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 |     APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 |     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 | 
118 |     # We build the pattern for arguments to be converted via cygpath
119 |     ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 |     SEP=""
121 |     for dir in $ROOTDIRSRAW ; do
122 |         ROOTDIRS="$ROOTDIRS$SEP$dir"
123 |         SEP="|"
124 |     done
125 |     OURCYGPATTERN="(^($ROOTDIRS))"
126 |     # Add a user-defined pattern to the cygpath arguments
127 |     if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 |         OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 |     fi
130 |     # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 |     i=0
132 |     for arg in "$@" ; do
133 |         CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 |         CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
135 | 
136 |         if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
137 |             eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 |         else
139 |             eval `echo args$i`="\"$arg\""
140 |         fi
141 |         i=$((i+1))
142 |     done
143 |     case $i in
144 |         (0) set -- ;;
145 |         (1) set -- "$args0" ;;
146 |         (2) set -- "$args0" "$args1" ;;
147 |         (3) set -- "$args0" "$args1" "$args2" ;;
148 |         (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 |         (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 |         (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 |         (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 |         (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 |         (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 |     esac
155 | fi
156 | 
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 |     JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 | 
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 | 
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
 1 | @if "%DEBUG%" == "" @echo off
 2 | @rem ##########################################################################
 3 | @rem
 4 | @rem  Gradle startup script for Windows
 5 | @rem
 6 | @rem ##########################################################################
 7 | 
 8 | @rem Set local scope for the variables with windows NT shell
 9 | if "%OS%"=="Windows_NT" setlocal
10 | 
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 | 
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 | 
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 | 
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 | 
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 | 
32 | goto fail
33 | 
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 | 
38 | if exist "%JAVA_EXE%" goto init
39 | 
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 | 
46 | goto fail
47 | 
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 | 
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 | 
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 | 
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 | 
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 | 
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 | 
69 | :execute
70 | @rem Setup the command line
71 | 
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 | 
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 | 
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 | 
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 | 
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 | 
90 | :omega
91 | 
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':divertsy_client'
2 | 
--------------------------------------------------------------------------------