├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── divertsy_client ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── divertsy │ │ └── hid │ │ ├── AppCompatPreferenceActivity.java │ │ ├── MainActivity.java │ │ ├── ScaleApplication.java │ │ ├── SettingsActivity.java │ │ ├── SyncEventService.java │ │ ├── SyncToDriveService.java │ │ ├── WasteStreams.java │ │ ├── ble │ │ ├── BLEScanner.java │ │ ├── Beacon.java │ │ ├── Constants.java │ │ ├── TlmValidator.java │ │ ├── UidValidator.java │ │ ├── UrlUtils.java │ │ └── UrlValidator.java │ │ ├── usb │ │ ├── ScaleMeasurement.java │ │ └── UsbScaleManager.java │ │ └── utils │ │ ├── AppUpdater.java │ │ ├── Utils.java │ │ └── WeightRecorder.java │ └── res │ ├── drawable-hdpi │ ├── divertsybg.png │ └── ic_launcher.png │ ├── drawable-mdpi │ ├── divertsybg.png │ └── ic_launcher.png │ ├── drawable-xhdpi │ ├── divertsybg.png │ └── ic_launcher.png │ ├── drawable-xxhdpi │ ├── divertsybg.png │ └── ic_launcher.png │ ├── drawable │ ├── ic_business_black_24dp.xml │ ├── ic_info_black_24dp.xml │ └── ic_sync_black_24dp.xml │ ├── layout │ ├── activity_main.xml │ └── manual_weight_entry.xml │ ├── menu │ └── main_actions.xml │ ├── raw │ └── waste_streams.json │ ├── values-de │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-w820dp │ └── dimens.xml │ ├── values │ ├── dimens.xml │ ├── divertsy_default_strings.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── device_filter.xml │ ├── pref_general.xml │ ├── pref_headers.xml │ ├── pref_location.xml │ └── pref_sync.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Release configuration file 24 | release.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | projectFilesBackup/ 38 | 39 | # Intellij 40 | *.iml 41 | .idea/ 42 | 43 | # Keystore files 44 | *.jks 45 | 46 | # OSX files 47 | .DS_Store 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to fostering a welcoming community. Any participant and 4 | contributor is required to adhere to our [Code of 5 | Conduct](http://etsy.github.io/codeofconduct.html). 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | ## Code Contributions 4 | To contribute to ``divertsy-client``, you should follow these steps: 5 | 6 | 1. Fork the repo. 7 | 2. Clone your fork. 8 | 3. Hack on the changes to the code. 9 | 4. Test that the changes run properly on one of the approved tablet devices. 10 | 5. Push the branch up to GitHub. 11 | 6. Send a pull request. 12 | 13 | We'll do our best to merge the changes in if they will be helpful to the community. We realize each organization is different and there will vastly different configurations of Divertsy. In some cases, leaving these configurations as a fork of this repo will be the best option. 14 | 15 | ## Bug and Feature Requests 16 | 17 | If you find a bug in the code or have a feature request that would be helpful to the community, please open an [Issue] (issues) in our repo on Github. 18 | 19 | Since there is a wide range of Android devices, we many not attempt to fix bugs which do not affect the list of supported tablets. 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Divertsy client application for Android 2 | 3 | Divertsy is an open source system developed by the Office Hackers at Etsy to collect weight information about waste streams in local and remote offices. The complete version of Divertsy uses both a client application (found here) and a backend server application. The Divertsy client can work in a "stand alone" mode without the back end service. This is the easiest way to get started with Divertsy. 4 | 5 | ![Divertsy Client on Android](https://cloud.githubusercontent.com/assets/714166/24930736/8bdda552-1ed8-11e7-9eba-660515d9d260.png) 6 | 7 | ## Getting Started 8 | 9 | Ensure you have a supported Android tablet (see [Tablet Setup](https://github.com/etsy/divertsy-client/wiki/2%29-Tablet-Setup)) and working USB scale which is connected to the tablet (see [Scale Setup](https://github.com/etsy/divertsy-client/wiki/3%29-Scale-Setup)). Then compile the Divertsy client and sideload it on to the tablet. 10 | 11 | When the Divertsy client runs the first time, it will ask you to set an "Office Name". This is to help you keep track of which tablet is sending you data. The tablet's "Device ID" will also be recorded in case you setup multiple tablets with the same "Office Name". Click the back arrow to return to the main Divertsy client screen. 12 | 13 | You may see a message to connect the USB scale. If the scale is connected, make sure the power is now on for the scale (remember, the scale should NOT have batteries or a power connect to the wall). You may get a pop-up message asking if you want to use the USB device you just connected. Click Yes to this message (optionally, click the box to always allow this application to use the device). Now when you add weight to the scale, the numbers shown on the Divertsy screen should match the numbers shown on the scale. When you have finished adding items to the scale, push the button on screen of the type of waste. The amount shown and the type chosen will be written to a data file along with the time. 14 | 15 | ## Additional Information 16 | 17 | Please see the [Wiki](https://github.com/etsy/divertsy-client/wiki) for additional information about. 18 | 19 | 20 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | mavenCentral() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:2.3.0' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | mavenCentral() 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /divertsy_client/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /divertsy_client/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'android' 2 | 3 | def getDateAsMillis() { 4 | def Calendar cal = Calendar.getInstance(); 5 | return cal.getTimeInMillis().toString() 6 | } 7 | 8 | android { 9 | signingConfigs { 10 | release {} 11 | 12 | try { 13 | def props = new Properties() 14 | props.load(new FileInputStream(rootProject.file("release.properties"))) 15 | 16 | release { 17 | keyAlias props.keyAlias 18 | keyPassword props.keyAliasPassword 19 | storeFile new File(System.properties['user.home'] + props.keyStore) 20 | storePassword props.keyStorePassword 21 | } 22 | } catch (IOException e){ 23 | logger.warn('Signing Config: ' + e.getLocalizedMessage()); 24 | } 25 | } 26 | 27 | lintOptions { 28 | abortOnError false 29 | } 30 | 31 | compileSdkVersion 25 32 | buildToolsVersion '25.0.2' 33 | defaultConfig { 34 | minSdkVersion 14 35 | targetSdkVersion 24 36 | versionCode 1 37 | versionName "1.0" 38 | vectorDrawables.useSupportLibrary = true 39 | } 40 | buildTypes { 41 | debug { 42 | buildConfigField "java.util.Date", "buildTime", "new java.util.Date(" + getDateAsMillis() + "L)" 43 | } 44 | release { 45 | apply plugin: 'maven' 46 | buildConfigField "java.util.Date", "buildTime", "new java.util.Date(" + getDateAsMillis() + "L)" 47 | signingConfig signingConfigs.release 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | compile fileTree(include: ['*.jar'], dir: 'libs') 54 | compile 'com.android.support:appcompat-v7:24.2.0' 55 | compile 'com.android.support:support-v4:24.2.0' 56 | compile 'com.google.android.gms:play-services-drive:8.4.0' 57 | } 58 | -------------------------------------------------------------------------------- /divertsy_client/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 48 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /divertsy_client/src/main/java/com/divertsy/hid/AppCompatPreferenceActivity.java: -------------------------------------------------------------------------------- 1 | package com.divertsy.hid; 2 | 3 | import android.content.res.Configuration; 4 | import android.os.Bundle; 5 | import android.preference.PreferenceActivity; 6 | import android.support.annotation.LayoutRes; 7 | import android.support.annotation.Nullable; 8 | import android.support.v7.app.ActionBar; 9 | import android.support.v7.app.AppCompatDelegate; 10 | import android.support.v7.widget.Toolbar; 11 | import android.view.MenuInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | 15 | /** 16 | * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls 17 | * to be used with AppCompat. 18 | */ 19 | public abstract class AppCompatPreferenceActivity extends PreferenceActivity { 20 | 21 | private AppCompatDelegate mDelegate; 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | getDelegate().installViewFactory(); 26 | getDelegate().onCreate(savedInstanceState); 27 | super.onCreate(savedInstanceState); 28 | } 29 | 30 | @Override 31 | protected void onPostCreate(Bundle savedInstanceState) { 32 | super.onPostCreate(savedInstanceState); 33 | getDelegate().onPostCreate(savedInstanceState); 34 | } 35 | 36 | public ActionBar getSupportActionBar() { 37 | return getDelegate().getSupportActionBar(); 38 | } 39 | 40 | public void setSupportActionBar(@Nullable Toolbar toolbar) { 41 | getDelegate().setSupportActionBar(toolbar); 42 | } 43 | 44 | @Override 45 | public MenuInflater getMenuInflater() { 46 | return getDelegate().getMenuInflater(); 47 | } 48 | 49 | @Override 50 | public void setContentView(@LayoutRes int layoutResID) { 51 | getDelegate().setContentView(layoutResID); 52 | } 53 | 54 | @Override 55 | public void setContentView(View view) { 56 | getDelegate().setContentView(view); 57 | } 58 | 59 | @Override 60 | public void setContentView(View view, ViewGroup.LayoutParams params) { 61 | getDelegate().setContentView(view, params); 62 | } 63 | 64 | @Override 65 | public void addContentView(View view, ViewGroup.LayoutParams params) { 66 | getDelegate().addContentView(view, params); 67 | } 68 | 69 | @Override 70 | protected void onPostResume() { 71 | super.onPostResume(); 72 | getDelegate().onPostResume(); 73 | } 74 | 75 | @Override 76 | protected void onTitleChanged(CharSequence title, int color) { 77 | super.onTitleChanged(title, color); 78 | getDelegate().setTitle(title); 79 | } 80 | 81 | @Override 82 | public void onConfigurationChanged(Configuration newConfig) { 83 | super.onConfigurationChanged(newConfig); 84 | getDelegate().onConfigurationChanged(newConfig); 85 | } 86 | 87 | @Override 88 | protected void onStop() { 89 | super.onStop(); 90 | getDelegate().onStop(); 91 | } 92 | 93 | @Override 94 | protected void onDestroy() { 95 | super.onDestroy(); 96 | getDelegate().onDestroy(); 97 | } 98 | 99 | public void invalidateOptionsMenu() { 100 | getDelegate().invalidateOptionsMenu(); 101 | } 102 | 103 | private AppCompatDelegate getDelegate() { 104 | if (mDelegate == null) { 105 | mDelegate = AppCompatDelegate.create(this, null); 106 | } 107 | return mDelegate; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /divertsy_client/src/main/java/com/divertsy/hid/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.divertsy.hid; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.AlertDialog; 6 | import android.app.PendingIntent; 7 | import android.content.DialogInterface; 8 | import android.content.Intent; 9 | import android.content.IntentFilter; 10 | import android.content.SharedPreferences; 11 | import android.graphics.Color; 12 | import android.graphics.Paint; 13 | import android.net.Uri; 14 | import android.os.Build; 15 | import android.os.Bundle; 16 | import android.os.Handler; 17 | import android.os.Message; 18 | import android.support.annotation.NonNull; 19 | import android.support.v4.app.ActivityCompat; 20 | import android.support.v7.app.AppCompatActivity; 21 | import android.support.v7.widget.AppCompatButton; 22 | import android.content.pm.PackageManager; 23 | import android.content.BroadcastReceiver; 24 | import android.content.Context; 25 | import android.util.Log; 26 | import android.view.LayoutInflater; 27 | import android.view.Menu; 28 | import android.view.MenuItem; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | import android.view.WindowManager; 32 | import android.widget.ArrayAdapter; 33 | import android.widget.Button; 34 | import android.widget.EditText; 35 | import android.widget.LinearLayout; 36 | import android.widget.Spinner; 37 | import android.widget.TextView; 38 | 39 | import com.divertsy.hid.ble.BLEScanner; 40 | import com.divertsy.hid.ble.Beacon; 41 | import com.divertsy.hid.usb.ScaleMeasurement; 42 | import com.divertsy.hid.usb.UsbScaleManager; 43 | import com.divertsy.hid.utils.AppUpdater; 44 | import com.divertsy.hid.utils.Utils; 45 | import com.divertsy.hid.utils.WeightRecorder; 46 | import com.google.android.gms.common.GoogleApiAvailability; 47 | 48 | import java.io.File; 49 | import java.lang.ref.WeakReference; 50 | import java.util.Date; 51 | import java.util.List; 52 | import java.util.Set; 53 | 54 | import static com.google.android.gms.common.ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED; 55 | 56 | /** 57 | * MainActivity this is the main Divertsy class. It handles UI updates and button presses. 58 | */ 59 | public class MainActivity extends AppCompatActivity implements UsbScaleManager.Callbacks, BLEScanner.OnClosestChangedListener { 60 | 61 | private static final String TAG = "DIVERTSY"; 62 | 63 | public static final long SEND_DELAY_MILLIS = 1000; // Helps to prevent double taps 64 | private static final int BUTTON_HEIGHT_PIXELS = 90; 65 | private static final int SETTINGS_RESULT = 0; 66 | private static final int REQUEST_ENABLE_BLUETOOTH = 1; 67 | private static final int PERMISSION_REQUEST_COARSE_LOCATION = 2; 68 | private static final int REQUEST_CODE_RESOLUTION = 1; 69 | private static final String KEY_FLOOR = "Floor"; 70 | private static final String KEY_PLACE = "Place"; 71 | private static final String KEY_URL_TEXT = "url_text"; 72 | 73 | private static final int REQUEST_WRITE_STORAGE = 112; 74 | 75 | private Handler mHandler = new MainHandler(this); 76 | private Handler mDialogDismissHandler = new ActivityHandler(this); 77 | 78 | public static class ActivityHandler extends Handler { 79 | protected final WeakReference mRef; 80 | public ActivityHandler(MainActivity activity) { 81 | mRef = new WeakReference<>(activity); 82 | } 83 | } 84 | 85 | /** 86 | * Sets the new beacon after 4 seconds so it doesn't fluctuate incorrectly 87 | */ 88 | public static class MainHandler extends ActivityHandler { 89 | public MainHandler(MainActivity activity) { 90 | super(activity); 91 | } 92 | 93 | @Override 94 | public void handleMessage(Message msg) { 95 | MainActivity mActivity = mRef.get(); 96 | if (mActivity != null) { 97 | Beacon closest = (Beacon) msg.obj; 98 | List segments = closest.urlStatus.getUrl().getPathSegments(); 99 | mActivity.updateClosestBeacon(closest.urlStatus.toString(), segments.get(0), segments.get(1)); 100 | } 101 | } 102 | } 103 | 104 | private long mLastSendTime = 0; 105 | private UsbScaleManager mUsbScaleManager; 106 | private WeightRecorder mWeightRecorder; 107 | private String mFloor; 108 | private String mPlace; 109 | private String SAVED_WEIGHT_TYPE; 110 | private boolean ZeroWeightAfterAdd = false; 111 | 112 | private TextView mWeight; 113 | private TextView mWeightUnit; 114 | private TextView mLocation; 115 | 116 | private BLEScanner mBLEScanner; 117 | 118 | // Required for Remote Scales 119 | private BroadcastReceiver mRemoteScaleReceiver; 120 | private ScaleMeasurement mLatestScaleMeasurement; 121 | 122 | @Override 123 | public boolean onCreateOptionsMenu(Menu menu) { 124 | getMenuInflater().inflate(R.menu.main_actions, menu); 125 | return super.onCreateOptionsMenu(menu); 126 | } 127 | 128 | @Override 129 | protected void onNewIntent(Intent intent) { 130 | 131 | // This is used to handle items that need an Activity from the Sync Service 132 | try { 133 | // See if we got a Google API error 134 | int errorCode = intent.getIntExtra("apiErrorCode", 0); 135 | if (errorCode != 0) { 136 | Log.e(TAG, "Google API Availability Error: " + errorCode); 137 | GoogleApiAvailability.getInstance().getErrorDialog(this, errorCode, 0).show(); 138 | return; 139 | } 140 | 141 | if(intent.getBooleanExtra("PlayServicesUpdate",false)) { 142 | GoogleApiAvailability.getInstance().getErrorDialog(this, SERVICE_VERSION_UPDATE_REQUIRED, 0).show(); 143 | } 144 | 145 | // If this is the first Google Drive request, we need to prompt for a login to use 146 | PendingIntent pI = intent.getParcelableExtra("resolution"); 147 | if (pI != null) { 148 | startIntentSenderForResult(pI.getIntentSender(), 1, null, 0, 0, 0); 149 | } else { 150 | Log.w(TAG, "No resolution in received Intent."); 151 | } 152 | } catch(Exception e) { 153 | Log.e(TAG, "Error starting new intent:" + e.getMessage()); 154 | } 155 | } 156 | 157 | 158 | @Override 159 | protected void onCreate(Bundle savedInstanceState) { 160 | super.onCreate(savedInstanceState); 161 | Log.v(TAG, "Starting Divertsy - OnCreate"); 162 | 163 | // These flags let the activity turn on the screen 164 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | 165 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | 166 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | 167 | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON); 168 | 169 | mWeightRecorder = new WeightRecorder(this); 170 | 171 | checkIfBluetoothEnabled(); 172 | 173 | setContentView(R.layout.activity_main); 174 | mWeight = (TextView) findViewById(R.id.weight); 175 | mWeightUnit = (TextView) findViewById(R.id.weight_unit); 176 | mLocation = (TextView) findViewById(R.id.location); 177 | 178 | 179 | if (savedInstanceState == null) { 180 | 181 | // check for App Update 182 | File updateFile = AppUpdater.checkAppUpdate(); 183 | if (updateFile != null) { 184 | Intent intent = new Intent(Intent.ACTION_VIEW); 185 | intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive"); 186 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 187 | startActivity(intent); 188 | } 189 | } else { 190 | updateClosestBeacon( 191 | savedInstanceState.getString(KEY_URL_TEXT), 192 | savedInstanceState.getString(KEY_FLOOR), 193 | savedInstanceState.getString(KEY_PLACE) 194 | ); 195 | } 196 | 197 | mUsbScaleManager = new UsbScaleManager(this, getIntent(), this, savedInstanceState); 198 | 199 | initView(); 200 | 201 | if (! mWeightRecorder.isOfficeNameSet()){ 202 | Log.v(TAG, "No Office Name Set"); 203 | new AlertDialog.Builder(this) 204 | .setTitle(R.string.msg_set_office_name_warning_title) 205 | .setMessage(R.string.msg_set_office_name_warning) 206 | .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { 207 | @Override 208 | public void onClick(DialogInterface dialog, int id) { 209 | OpenLocationSettings(); 210 | } 211 | }) 212 | .show(); 213 | } 214 | } 215 | 216 | @Override 217 | protected void onStart() { 218 | super.onStart(); 219 | mUsbScaleManager.onStart(this); 220 | } 221 | 222 | @Override 223 | protected void onPause() { 224 | super.onPause(); 225 | unregisterReceiver(mRemoteScaleReceiver); 226 | if (mBLEScanner != null) { 227 | mBLEScanner.onPause(); 228 | } 229 | } 230 | 231 | @Override 232 | protected void onResume() { 233 | // This could have changed via settings menu 234 | checkIfBluetoothEnabled(); 235 | 236 | if (mBLEScanner != null) { 237 | mBLEScanner.onResume(); 238 | } 239 | super.onResume(); 240 | 241 | // This sets us up to listen for remote scale data 242 | IntentFilter filter = new IntentFilter(); 243 | filter.addAction("com.divertsy.REMOTE_SCALE_WEIGHT"); 244 | mRemoteScaleReceiver = new RemoteScaleReceiver(); 245 | registerReceiver(mRemoteScaleReceiver, filter); 246 | 247 | // The view might change via settings, this should refresh it 248 | initView(); 249 | } 250 | 251 | @Override 252 | protected void onStop() { 253 | mUsbScaleManager.onStop(this); 254 | super.onStop(); 255 | } 256 | 257 | @Override 258 | protected void onDestroy() { 259 | mDialogDismissHandler.removeCallbacksAndMessages(null); 260 | super.onDestroy(); 261 | } 262 | 263 | @Override 264 | protected void onSaveInstanceState(Bundle outState) { 265 | super.onSaveInstanceState(outState); 266 | outState.putString(KEY_FLOOR, mFloor); 267 | outState.putString(KEY_PLACE, mPlace); 268 | outState.putString(KEY_URL_TEXT, mLocation.getText().toString()); 269 | } 270 | 271 | @Override 272 | public boolean onOptionsItemSelected(MenuItem item) { 273 | // Handle presses on the action bar items 274 | Intent sharingIntent; 275 | switch (item.getItemId()) { 276 | case R.id.action_settings: 277 | startActivityForResult(new Intent(this, SettingsActivity.class), SETTINGS_RESULT); 278 | return true; 279 | case R.id.action_share_email: 280 | sharingIntent = new Intent(android.content.Intent.ACTION_SEND); 281 | sharingIntent.setType("text/csv"); 282 | String subject_line = "Divertsy Data: " + mWeightRecorder.getOffice(); 283 | String email_body = "Divertsy data attachement below. Last update: " + mWeightRecorder.getLastRecordedWeight(); 284 | 285 | File csv = new File(Utils.getDivertsyFilePath(mWeightRecorder.getOffice())); 286 | sharingIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(csv)); 287 | sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject_line ); 288 | sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, email_body); 289 | startActivity(Intent.createChooser(sharingIntent, "Send CSV File")); 290 | 291 | return true; 292 | default: 293 | Log.i(TAG,"No Menu Option Found. Check the list in onOptionsItemSelected."); 294 | return super.onOptionsItemSelected(item); 295 | } 296 | } 297 | 298 | 299 | @Override 300 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 301 | super.onActivityResult(requestCode, resultCode, data); 302 | if (requestCode == SETTINGS_RESULT) { 303 | // We might need to update the scale weight 304 | mUsbScaleManager.setAddToScaleWeight(mWeightRecorder.getDefaultWeight()); 305 | initView(); 306 | } else if (requestCode == REQUEST_ENABLE_BLUETOOTH && mBLEScanner != null) { 307 | if (resultCode == Activity.RESULT_OK) { 308 | mBLEScanner.init(this); 309 | } 310 | } else if (requestCode == REQUEST_CODE_RESOLUTION && resultCode == RESULT_OK) { 311 | // Connects the chosen account to our Google Drive API connector 312 | // mGoogleApiClient.connect(); 313 | startService(new Intent(this, SyncToDriveService.class)); 314 | } else { 315 | Log.e(TAG, "Activity Result Not Handled: " + requestCode ); 316 | } 317 | } 318 | 319 | public void OpenLocationSettings() { 320 | startActivityForResult(new Intent(this, SettingsActivity.class), SETTINGS_RESULT); 321 | } 322 | 323 | @Override 324 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 325 | if (requestCode == PERMISSION_REQUEST_COARSE_LOCATION) { 326 | if (mBLEScanner != null) { 327 | // If we get denied, we should stop trying. 328 | if ((grantResults.length > 0) && (grantResults[0] == -1)){ 329 | Log.e(TAG, "Coarse Permission denied. Turning off Beacon setting."); 330 | mWeightRecorder.setUseBeacons(false); 331 | } else{ 332 | mBLEScanner.onRequestPermissionsResult(requestCode, grantResults); 333 | } 334 | } 335 | } 336 | if (requestCode == REQUEST_WRITE_STORAGE){ 337 | saveWeight(SAVED_WEIGHT_TYPE); 338 | } 339 | } 340 | 341 | private void checkIfBluetoothEnabled(){ 342 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { 343 | if (mWeightRecorder.useBluetoothBeacons()){ 344 | mBLEScanner = new BLEScanner(this, REQUEST_ENABLE_BLUETOOTH, this); 345 | } 346 | } 347 | } 348 | 349 | private void initView() { 350 | setTitleBar(); 351 | WasteStreams wasteStreams = new WasteStreams(); 352 | wasteStreams.loadWasteStreams(getApplicationContext()); 353 | 354 | // Only show the enabled streams or default if not set 355 | Set enabledStreams = mWeightRecorder.getEnabledStreams(); 356 | if ((enabledStreams == null) || (enabledStreams.size() == 0)){ 357 | enabledStreams = wasteStreams.getDefaultStreamValuesSet(); 358 | } 359 | List sortedStreams = wasteStreams.getSortedStreams(enabledStreams); 360 | 361 | // Buttons rows are hardcoded in the acitivity_main layout for now 362 | LinearLayout[] buttonRows = { 363 | (LinearLayout) findViewById(R.id.button_row_1), 364 | (LinearLayout) findViewById(R.id.button_row_2), 365 | (LinearLayout) findViewById(R.id.button_row_3) 366 | }; 367 | 368 | // Remove all current buttons. This can happen after a settings change. 369 | for(LinearLayout row: buttonRows){ 370 | if(row.getChildCount() > 0) row.removeAllViews(); 371 | } 372 | 373 | int current_row = 0; 374 | for (final String stream : sortedStreams) { 375 | Log.d(TAG,"Enabled Stream: " + stream); 376 | 377 | AppCompatButton button = new AppCompatButton(this); 378 | button.setText(wasteStreams.getDisplayNameFromValue(stream)); 379 | LinearLayout.LayoutParams lparams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT,1f); 380 | lparams.setMargins(10,10,10,10); 381 | button.setLayoutParams(lparams); 382 | button.setBackgroundColor(wasteStreams.getButtonColorFromValue(stream)); 383 | button.setTextColor(Color.WHITE); 384 | button.setOnClickListener(new View.OnClickListener() { 385 | @Override 386 | public void onClick(View view) { 387 | saveWeight(stream); 388 | } 389 | } 390 | ); 391 | buttonRows[current_row++ % buttonRows.length].addView(button); 392 | } 393 | 394 | 395 | 396 | // This sets up the manual weight input pop-up when the digits are tapped 397 | TextView tvWeight = (TextView) findViewById(R.id.weight); 398 | 399 | LayoutInflater factory = LayoutInflater.from(this); 400 | final View manualEntryView = factory.inflate(R.layout.manual_weight_entry, null); 401 | final EditText input = (EditText) manualEntryView.findViewById(R.id.manual_weight_input); 402 | 403 | final AlertDialog.Builder builder = new AlertDialog.Builder(this); 404 | builder.setTitle(R.string.msg_manual_entry_title); 405 | builder.setMessage(R.string.msg_manual_entry_info); 406 | builder.setView(manualEntryView); 407 | 408 | // Load the manual unit weight list 409 | final Spinner manualUnitPicker = (Spinner) manualEntryView.findViewById(R.id.manual_weight_units); 410 | ArrayAdapter adapter = ArrayAdapter.createFromResource(this, 411 | R.array.manual_weight_units, android.R.layout.simple_spinner_item); 412 | adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 413 | manualUnitPicker.setAdapter(adapter); 414 | 415 | builder.setCancelable(true); 416 | builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { 417 | public void onClick(DialogInterface dialog, int whichButton) { 418 | try { 419 | Double inputWeight = Double.parseDouble(input.getText().toString()); 420 | String inputUnits = manualUnitPicker.getSelectedItem().toString(); 421 | 422 | // Update the on Screen Display 423 | mWeight.setText(Double.toString(inputWeight)); 424 | mWeightUnit.setText(inputUnits); 425 | // Underline the Unit to show it was a manual entry 426 | mWeightUnit.setPaintFlags(mWeightUnit.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); 427 | 428 | // Save the data so we can record it if the user taps a waste stream button 429 | ScaleMeasurement.Builder measurementBuilder = new ScaleMeasurement.Builder(); 430 | measurementBuilder.rawScaleWeight(inputWeight); 431 | measurementBuilder.scaleWeight(inputWeight); 432 | measurementBuilder.units(inputUnits); 433 | mLatestScaleMeasurement = measurementBuilder.build(); 434 | 435 | ZeroWeightAfterAdd = true; 436 | 437 | } catch (Exception e){ 438 | Log.e(TAG, "Error from Manual Input:" + e.getMessage()); 439 | } 440 | 441 | dialog.dismiss(); 442 | } 443 | }); 444 | 445 | 446 | 447 | final AlertDialog manualWeightDialog = builder.create(); 448 | 449 | tvWeight.setOnClickListener(new View.OnClickListener() { 450 | @Override 451 | public void onClick(View view) { 452 | Log.d(TAG, "Weight View Tap!"); 453 | manualWeightDialog.show(); 454 | }; 455 | }); 456 | 457 | Button mTare; 458 | mTare = (Button) findViewById(R.id.button_zero); 459 | if (mWeightRecorder.tareAfterAdd()){ 460 | mTare.setBackgroundColor(Color.GRAY); 461 | mTare.setText(R.string.btn_zero); 462 | mTare.setOnClickListener(new View.OnClickListener() { 463 | @Override 464 | public void onClick(View view) { 465 | Log.d(TAG, "Call Zero Tare"); 466 | ScaleMeasurement sm = mUsbScaleManager.getLatestMeasurement(); 467 | if(sm != null) { 468 | mUsbScaleManager.setAddToScaleWeight(sm.getRawScaleWeight()); 469 | } else { 470 | Log.e(TAG, "Null ScaleMeasurement on Tare"); 471 | } 472 | } 473 | }); 474 | } else { 475 | // Make the button transparent so it still takes up the space, but not in use. 476 | mTare.setBackgroundColor(Color.TRANSPARENT); 477 | mTare.setText(""); 478 | mTare.setOnClickListener(null); 479 | } 480 | 481 | 482 | } 483 | 484 | private void setTitleBar() { 485 | Date buildDate = BuildConfig.buildTime; 486 | Log.i(TAG, "This App was built on " + buildDate.toString()); 487 | 488 | try { 489 | getSupportActionBar().setTitle(getString(R.string.app_name) + ": " 490 | + mWeightRecorder.getOffice() + " (" + Utils.getBuildNumber().toString() + ") ∆ " + mWeightRecorder.getLastRecordedWeight()); 491 | } catch(Exception e) { 492 | Log.e(TAG, "Error setting Title:" + e.getMessage()); 493 | } 494 | } 495 | 496 | public void requestStoragePermission(){ 497 | ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 498 | REQUEST_WRITE_STORAGE); 499 | } 500 | 501 | 502 | public void saveWeight(String weightType) { 503 | ScaleMeasurement measurement = mLatestScaleMeasurement; 504 | 505 | // New Android M+ permission check requirement. 506 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 507 | if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) 508 | != PackageManager.PERMISSION_GRANTED) { 509 | SAVED_WEIGHT_TYPE = weightType; 510 | final AlertDialog.Builder builder = new AlertDialog.Builder(this); 511 | builder.setTitle(R.string.msg_perm_external_storage_title); 512 | builder.setMessage(R.string.msg_perm_external_storage); 513 | builder.setPositiveButton(android.R.string.ok, null); 514 | builder.setOnDismissListener(new DialogInterface.OnDismissListener() { 515 | @Override 516 | public void onDismiss(DialogInterface dialog) { 517 | requestStoragePermission(); 518 | } 519 | }); 520 | builder.show(); 521 | 522 | return; 523 | } 524 | } 525 | 526 | // This could happen if the scale is not connected. 527 | // We are now allowing "Zero" entries to be recorded, so we'll need to build 528 | // an empty measurement item if there was not one 529 | if (measurement == null) { 530 | ScaleMeasurement.Builder zSMBuilder = new ScaleMeasurement.Builder(); 531 | zSMBuilder.rawScaleWeight(0.0f); 532 | zSMBuilder.scaleWeight(0.0f); 533 | // Defaulting to KG which is the 3rd item in the WeightUnit array 534 | zSMBuilder.units(mUsbScaleManager.WEIGHTUNIT[3]); 535 | measurement = zSMBuilder.build(); 536 | } 537 | 538 | //Block to help prevent double sends 539 | long now = measurement.getTime(); 540 | if (now < (mLastSendTime + SEND_DELAY_MILLIS)) { 541 | Log.v(TAG, "Double Send Block Triggered"); 542 | return; 543 | } else { 544 | mLastSendTime = now; 545 | } 546 | 547 | //Block Negative values 548 | if (measurement.getScaleWeight() < 0) { 549 | showError(getString(R.string.error_negative_value)); 550 | return; 551 | } 552 | 553 | Log.d(TAG, "Saving Weight - Type: " + weightType + " Value: " + Double.toString(measurement.getScaleWeight())); 554 | 555 | try { 556 | 557 | String sOffice = mWeightRecorder.getOffice(); 558 | Utils.saveCSV(sOffice, measurement.toCSV(mWeightRecorder.getOffice(), weightType, mFloor, mPlace)); 559 | 560 | mWeightRecorder.saveAsLastRecordedWeight(Double.toString(measurement.getScaleWeight()), weightType); 561 | setTitleBar(); 562 | 563 | // Send an Intent to start Syncing if this is enabled 564 | SharedPreferences syncPreferences = getApplicationContext().getSharedPreferences(SyncToDriveService.PREFERENCES_NAME, Context.MODE_PRIVATE); 565 | if(syncPreferences.getBoolean(SyncToDriveService.PREF_USE_GOOGLE_DRIVE, false)){ 566 | startService(new Intent(this, SyncToDriveService.class)); 567 | } 568 | 569 | final AlertDialog dialog = new AlertDialog.Builder(this) 570 | .setMessage(R.string.msg_weightsent) 571 | .show(); 572 | 573 | // Auto dismiss dialog 574 | mDialogDismissHandler.postDelayed(new Runnable() { 575 | @Override 576 | public void run() { 577 | if (dialog.isShowing()) { 578 | dialog.dismiss(); 579 | } 580 | } 581 | }, 2000); 582 | 583 | if (mWeightRecorder.tareAfterAdd()) { 584 | Log.d(TAG, "Call Zero Tare"); 585 | mUsbScaleManager.setAddToScaleWeight(measurement.getRawScaleWeight()); 586 | } 587 | 588 | if (ZeroWeightAfterAdd){ 589 | Log.i(TAG, "Zeroing Display"); 590 | // Update the on Screen Display 591 | mWeight.setText(R.string.weight); 592 | mWeightUnit.setText(""); 593 | mWeightUnit.setPaintFlags(mWeightUnit.getPaintFlags() & (~ Paint.UNDERLINE_TEXT_FLAG)); 594 | mLatestScaleMeasurement = null; 595 | ZeroWeightAfterAdd = false; 596 | } 597 | 598 | } catch (Exception e) { 599 | showError(getString(R.string.error_reporting)); 600 | e.printStackTrace(); 601 | } 602 | } 603 | 604 | @Override 605 | public void onMeasurement(final ScaleMeasurement measurement) { 606 | Double weight = measurement.getScaleWeight(); 607 | mWeight.setText(Double.toString(weight)); 608 | 609 | // If zero, hide the units since the USB data won't always show the correct setting 610 | if (weight == 0){ 611 | mWeightUnit.setText(""); 612 | } else { 613 | mWeightUnit.setText(measurement.getScaleUnit()); 614 | mWeightUnit.setPaintFlags(mWeightUnit.getPaintFlags() & (~ Paint.UNDERLINE_TEXT_FLAG)); 615 | } 616 | 617 | // Save this in case the user presses a waste stream button 618 | mLatestScaleMeasurement = mUsbScaleManager.getLatestMeasurement(); 619 | 620 | } 621 | 622 | public void showError(@NonNull String errorMessage) { 623 | new AlertDialog.Builder(this) 624 | .setTitle(R.string.error) 625 | .setMessage(errorMessage) 626 | .setPositiveButton(R.string.ok, null) 627 | .show(); 628 | } 629 | 630 | @Override 631 | public void onClosestChanged(final Beacon closest) { 632 | // Remove the last message if there was one, since we just updated to a new beacon 633 | mHandler.removeMessages(0); 634 | 635 | if (closest != null && closest.urlStatus != null && closest.urlStatus.getUrl() != null) { 636 | Message msg = Message.obtain(mHandler, 0, closest); 637 | // If we didn't have a saved floor or place and we just got one, set them immediately 638 | // otherwise wait 4 seconds to make sure it's stabilized 639 | if (mFloor == null || mPlace == null) { 640 | mHandler.sendMessageAtFrontOfQueue(msg); 641 | } else { 642 | mHandler.sendMessageDelayed(msg, 4000); 643 | } 644 | } else { 645 | mFloor = null; 646 | mPlace = null; 647 | mLocation.setText(null); 648 | } 649 | } 650 | 651 | private void updateClosestBeacon(String url, String floor, String place) { 652 | mFloor = floor; 653 | mPlace = place; 654 | mLocation.setText(url); 655 | } 656 | 657 | 658 | // use this as an inner class like here or as a top-level class 659 | public class RemoteScaleReceiver extends BroadcastReceiver { 660 | 661 | @Override 662 | public void onReceive(Context context, Intent intent) { 663 | try { 664 | float fRemoteWeight = intent.getFloatExtra("floatScaleWeight", 0.0f); 665 | String sRemoteUnit = intent.getStringExtra("stringScaleUnit"); 666 | 667 | // Assume KG as the default unit 668 | if ((sRemoteUnit == null ) || (sRemoteUnit.length() < 1)){ 669 | sRemoteUnit = "KG"; 670 | } else { 671 | sRemoteUnit = sRemoteUnit.toUpperCase(); 672 | } 673 | Log.d(TAG, "RemoteScale Data Received: " + fRemoteWeight + " " + sRemoteUnit); 674 | 675 | // Update the on Screen Display 676 | mWeight.setText(Float.toString(fRemoteWeight)); 677 | mWeightUnit.setText(sRemoteUnit); 678 | 679 | // Save the data so we can record it if the user taps a waste stream button 680 | ScaleMeasurement.Builder measurementBuilder = new ScaleMeasurement.Builder(); 681 | measurementBuilder.rawScaleWeight(fRemoteWeight); 682 | measurementBuilder.scaleWeight(fRemoteWeight); 683 | measurementBuilder.units(sRemoteUnit); 684 | mLatestScaleMeasurement = measurementBuilder.build(); 685 | 686 | } catch (Exception e) { 687 | Log.e(TAG, "REMOTE DATA BROADCAST RECEIVER ERROR: " + e.getMessage()); 688 | } 689 | } 690 | 691 | // constructor 692 | public RemoteScaleReceiver(){ 693 | Log.i(TAG, "Creating Broadcast Receiver"); 694 | } 695 | 696 | } 697 | 698 | } 699 | -------------------------------------------------------------------------------- /divertsy_client/src/main/java/com/divertsy/hid/ScaleApplication.java: -------------------------------------------------------------------------------- 1 | package com.divertsy.hid; 2 | 3 | import android.app.Application; 4 | import android.provider.Settings; 5 | 6 | /** 7 | * ScaleApplication for getting self and the DeviceID 8 | */ 9 | public class ScaleApplication extends Application { 10 | 11 | private static ScaleApplication self; 12 | 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | self = this; 17 | } 18 | 19 | public static ScaleApplication get() { 20 | return self; 21 | } 22 | 23 | public String getDeviceId() { 24 | return Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /divertsy_client/src/main/java/com/divertsy/hid/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.divertsy.hid; 2 | 3 | 4 | import android.annotation.TargetApi; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.content.SharedPreferences; 9 | import android.content.res.Configuration; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.preference.EditTextPreference; 13 | import android.preference.ListPreference; 14 | import android.preference.MultiSelectListPreference; 15 | import android.preference.Preference; 16 | import android.preference.PreferenceActivity; 17 | import android.preference.PreferenceFragment; 18 | import android.preference.PreferenceManager; 19 | import android.preference.SwitchPreference; 20 | import android.support.v4.app.NavUtils; 21 | import android.support.v7.app.ActionBar; 22 | import android.view.MenuItem; 23 | import android.util.Log; 24 | 25 | import com.divertsy.hid.utils.WeightRecorder; 26 | 27 | import java.util.HashSet; 28 | import java.util.List; 29 | 30 | /** 31 | * SettingsActivity handles inflating XML preference files and updating data when 32 | * the user changes settings in the application. 33 | */ 34 | public class SettingsActivity extends AppCompatPreferenceActivity { 35 | /** 36 | * A preference value change listener that updates the preference's summary 37 | * to reflect its new value. 38 | */ 39 | private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { 40 | @Override 41 | public boolean onPreferenceChange(Preference preference, Object value) { 42 | String stringValue = value.toString(); 43 | 44 | if (preference instanceof MultiSelectListPreference) { 45 | // For multi select list preferences we should show a list of the selected options 46 | MultiSelectListPreference listPreference = (MultiSelectListPreference) preference; 47 | CharSequence[] values = listPreference.getEntries(); 48 | StringBuilder options = new StringBuilder(); 49 | for(String stream : (HashSet) value) { 50 | int index = listPreference.findIndexOfValue(stream); 51 | if (index >= 0) { 52 | if (options.length() != 0) { 53 | options.append(", "); 54 | } 55 | options.append(values[index]); 56 | } 57 | } 58 | 59 | preference.setSummary(options); 60 | } else { 61 | // For all other preferences, set the summary to the value's 62 | // simple string representation. 63 | preference.setSummary(stringValue); 64 | } 65 | return true; 66 | } 67 | }; 68 | 69 | /** 70 | * Helper method to determine if the device has an extra-large screen. For 71 | * example, 10" tablets are extra-large. 72 | */ 73 | private static boolean isXLargeTablet(Context context) { 74 | return (context.getResources().getConfiguration().screenLayout 75 | & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE; 76 | } 77 | 78 | /** 79 | * Binds a preference's summary to its value. More specifically, when the 80 | * preference's value is changed, its summary (line of text below the 81 | * preference title) is updated to reflect the value. The summary is also 82 | * immediately updated upon calling this method. The exact display format is 83 | * dependent on the type of preference. 84 | * 85 | * @see #sBindPreferenceSummaryToValueListener 86 | */ 87 | private static void bindPreferenceSummaryToValue(Preference preference) { 88 | // Set the listener to watch for value changes. 89 | preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); 90 | 91 | // Trigger the listener immediately with the preference's 92 | // current value. 93 | SharedPreferences prefs = preference.getSharedPreferences(); 94 | Object value; 95 | if (preference instanceof MultiSelectListPreference) { 96 | value = prefs.getStringSet(preference.getKey(), new HashSet()); 97 | } else { 98 | value = prefs.getString(preference.getKey(), ""); 99 | } 100 | sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, value); 101 | } 102 | 103 | @Override 104 | protected void onCreate(Bundle savedInstanceState) { 105 | super.onCreate(savedInstanceState); 106 | setupActionBar(); 107 | } 108 | 109 | /** 110 | * Set up the {@link android.app.ActionBar}, if the API is available. 111 | */ 112 | private void setupActionBar() { 113 | ActionBar actionBar = getSupportActionBar(); 114 | if (actionBar != null) { 115 | // Show the Up button in the action bar. 116 | actionBar.setDisplayHomeAsUpEnabled(true); 117 | } 118 | } 119 | 120 | @Override 121 | public boolean onMenuItemSelected(int featureId, MenuItem item) { 122 | int id = item.getItemId(); 123 | if (id == android.R.id.home) { 124 | if (!super.onMenuItemSelected(featureId, item)) { 125 | NavUtils.navigateUpFromSameTask(this); 126 | } 127 | return true; 128 | } 129 | return super.onMenuItemSelected(featureId, item); 130 | } 131 | 132 | /** 133 | * {@inheritDoc} 134 | */ 135 | @Override 136 | public boolean onIsMultiPane() { 137 | return isXLargeTablet(this); 138 | } 139 | 140 | /** 141 | * {@inheritDoc} 142 | */ 143 | @Override 144 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 145 | public void onBuildHeaders(List
target) { 146 | loadHeadersFromResource(R.xml.pref_headers, target); 147 | } 148 | 149 | /** 150 | * This method stops fragment injection in malicious applications. 151 | * Make sure to deny any unknown fragments here. 152 | */ 153 | protected boolean isValidFragment(String fragmentName) { 154 | return PreferenceFragment.class.getName().equals(fragmentName) 155 | || GeneralPreferenceFragment.class.getName().equals(fragmentName) 156 | || LocationPreferenceFragment.class.getName().equals(fragmentName) 157 | || SyncPreferenceFragment.class.getName().equals(fragmentName); 158 | } 159 | 160 | /** 161 | * This fragment shows general preferences only. It is used when the 162 | * activity is showing a two-pane settings UI. 163 | */ 164 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 165 | public static class GeneralPreferenceFragment extends PreferenceFragment { 166 | @Override 167 | public void onCreate(Bundle savedInstanceState) { 168 | super.onCreate(savedInstanceState); 169 | PreferenceManager prefMgr = getPreferenceManager(); 170 | prefMgr.setSharedPreferencesName(WeightRecorder.PREFERENCES_NAME); 171 | addPreferencesFromResource(R.xml.pref_general); 172 | setHasOptionsMenu(true); 173 | 174 | Preference addToScalePref = findPreference(WeightRecorder.PREF_ADD_TO_SCALE); 175 | bindPreferenceSummaryToValue(addToScalePref); 176 | addToScalePref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { 177 | @Override 178 | public boolean onPreferenceChange(Preference preference, Object value) { 179 | ((SwitchPreference) findPreference(WeightRecorder.PREF_USE_BIN_WEIGHT)).setChecked(true); 180 | 181 | return sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, value); 182 | } 183 | }); 184 | 185 | findPreference("device_id").setSummary(ScaleApplication.get().getDeviceId()); 186 | } 187 | 188 | 189 | 190 | @Override 191 | public boolean onOptionsItemSelected(MenuItem item) { 192 | int id = item.getItemId(); 193 | if (id == android.R.id.home) { 194 | NavUtils.navigateUpFromSameTask(this.getActivity()); 195 | return true; 196 | } 197 | return super.onOptionsItemSelected(item); 198 | } 199 | } 200 | 201 | /** 202 | * This fragment shows notification preferences only. It is used when the 203 | * activity is showing a two-pane settings UI. 204 | */ 205 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 206 | public static class LocationPreferenceFragment extends PreferenceFragment { 207 | @Override 208 | public void onCreate(Bundle savedInstanceState) { 209 | super.onCreate(savedInstanceState); 210 | PreferenceManager prefMgr = getPreferenceManager(); 211 | prefMgr.setSharedPreferencesName(WeightRecorder.PREFERENCES_NAME); 212 | addPreferencesFromResource(R.xml.pref_location); 213 | setHasOptionsMenu(true); 214 | 215 | Preference office = findPreference(WeightRecorder.PREF_OFFICE); 216 | bindPreferenceSummaryToValue(office); 217 | office.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { 218 | @Override 219 | public boolean onPreferenceChange(Preference preference, Object value) { 220 | // Set the correct waste streams for this office 221 | String stringValue = value.toString(); 222 | 223 | if (getString(R.string.office_custom_choice).equals(stringValue)) { 224 | OfficePreference customPicker = new OfficePreference(getActivity(), getPreferenceManager()); 225 | customPicker.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { 226 | @Override 227 | public boolean onPreferenceChange(Preference preference, Object newValue) { 228 | Preference officePref = findPreference(WeightRecorder.PREF_OFFICE); 229 | return officePref.getOnPreferenceChangeListener().onPreferenceChange(officePref, newValue); 230 | } 231 | }); 232 | customPicker.showDialog(); 233 | return false; 234 | } 235 | 236 | setDriveIdFromOffice(stringValue); 237 | 238 | return sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, value); 239 | } 240 | }); 241 | 242 | loadWasteStreamSettings(); 243 | 244 | final ListPreference language = (ListPreference) findPreference(WeightRecorder.PREF_LANGUAGE); 245 | 246 | if (language.getEntry() == null){ 247 | language.setSummary(R.string.default_language); 248 | } else { 249 | language.setSummary(language.getEntry()); 250 | } 251 | 252 | language.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { 253 | @Override 254 | public boolean onPreferenceChange(Preference preference, Object value) { 255 | language.setValue(value.toString()); 256 | preference.setSummary(language.getEntry()); 257 | loadWasteStreamSettings(); 258 | return false; 259 | } 260 | }); 261 | 262 | } 263 | 264 | public void loadWasteStreamSettings(){ 265 | final MultiSelectListPreference streamPrefs = (MultiSelectListPreference) findPreference(WeightRecorder.PREF_WASTE_STREAMS); 266 | WasteStreams wasteStreams = new WasteStreams(); 267 | wasteStreams.loadWasteStreams(getActivity().getApplicationContext()); 268 | 269 | streamPrefs.setDefaultValue(wasteStreams.getDefaultStreamValues()); 270 | streamPrefs.setEntries(wasteStreams.getAllStreamNames()); 271 | streamPrefs.setEntryValues(wasteStreams.getAllStreamValues()); 272 | bindPreferenceSummaryToValue(streamPrefs); 273 | } 274 | 275 | // Places an older Drive ID file which might be associated with an office to the current Drive ID 276 | public void setDriveIdFromOffice(String office){ 277 | PreferenceManager prefMgr = getPreferenceManager(); 278 | String spf = prefMgr.getSharedPreferencesName(); 279 | prefMgr.setSharedPreferencesName(SyncToDriveService.PREFERENCES_NAME); 280 | 281 | String officeDriveIDkey = SyncToDriveService.PREF_DRIVE_ID + ":" + office; 282 | String sDriveID = prefMgr.getSharedPreferences().getString(officeDriveIDkey, ""); 283 | 284 | prefMgr.getSharedPreferences().edit() 285 | .putString(SyncToDriveService.PREF_DRIVE_ID, sDriveID) 286 | .apply(); 287 | 288 | prefMgr.setSharedPreferencesName(spf); 289 | 290 | } 291 | 292 | @Override 293 | public boolean onOptionsItemSelected(MenuItem item) { 294 | int id = item.getItemId(); 295 | if (id == android.R.id.home) { 296 | NavUtils.navigateUpFromSameTask(this.getActivity()); 297 | return true; 298 | } 299 | return super.onOptionsItemSelected(item); 300 | } 301 | } 302 | 303 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 304 | public static class SyncPreferenceFragment extends PreferenceFragment { 305 | @Override 306 | public void onCreate(Bundle savedInstanceState) { 307 | super.onCreate(savedInstanceState); 308 | 309 | //Make sure to call this since we're using a different 310 | PreferenceManager prefMgr = getPreferenceManager(); 311 | prefMgr.setSharedPreferencesName(SyncToDriveService.PREFERENCES_NAME); 312 | addPreferencesFromResource(R.xml.pref_sync); 313 | setHasOptionsMenu(true); 314 | SetDriveStringDetails(); 315 | 316 | Preference disconButton = findPreference("clear_drive_data"); 317 | if(disconButton != null){ 318 | disconButton.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener(){ 319 | @Override 320 | public boolean onPreferenceClick(Preference p){ 321 | Log.v("SETTING", "Clear Google Login"); 322 | 323 | ClearDriveID(); 324 | 325 | // Need to call service 326 | Intent i = new Intent(getActivity(), SyncToDriveService.class).putExtra("clear_drive_data", true); 327 | getActivity().startService(i); 328 | 329 | return true; 330 | } 331 | }); 332 | } 333 | 334 | } 335 | 336 | private void SetDriveStringDetails(){ 337 | PreferenceManager prefMgr = getPreferenceManager(); 338 | prefMgr.setSharedPreferencesName(SyncToDriveService.PREFERENCES_NAME); 339 | 340 | findPreference(SyncToDriveService.PREF_DRIVE_ID).setSummary( 341 | prefMgr.getSharedPreferences().getString(SyncToDriveService.PREF_DRIVE_ID, "none")); 342 | findPreference(SyncToDriveService.PREF_DRIVE_ID_LAST_SAVE_TIME).setSummary( 343 | prefMgr.getSharedPreferences().getString(SyncToDriveService.PREF_DRIVE_ID_LAST_SAVE_TIME, "never")); 344 | 345 | if (prefMgr.getSharedPreferences().getString(SyncToDriveService.PREF_DRIVE_ID, "").length() < 1){ 346 | findPreference("clear_drive_data").setEnabled(false); 347 | } 348 | } 349 | 350 | public String getCurrentOfficeSaveIDPref(){ 351 | PreferenceManager prefMgr = getPreferenceManager(); 352 | String spf = prefMgr.getSharedPreferencesName(); 353 | prefMgr.setSharedPreferencesName(WeightRecorder.PREFERENCES_NAME); 354 | String sOffice = prefMgr.getSharedPreferences().getString(WeightRecorder.PREF_OFFICE, WeightRecorder.DEFAULT_OFFICE); 355 | prefMgr.setSharedPreferencesName(spf); 356 | return SyncToDriveService.PREF_DRIVE_ID + ":" + sOffice; 357 | } 358 | 359 | // Clears the old Drive Data. Need to commit this for the GUI menu update. 360 | public void ClearDriveID(){ 361 | Log.v("SETTINGS", "Clearing Drive ID"); 362 | 363 | PreferenceManager prefMgr = getPreferenceManager(); 364 | prefMgr.setSharedPreferencesName(SyncToDriveService.PREFERENCES_NAME); 365 | SharedPreferences mSharedPreferences = prefMgr.getSharedPreferences(); 366 | mSharedPreferences.edit() 367 | .putBoolean(SyncToDriveService.PREF_USE_GOOGLE_DRIVE, false) 368 | .apply(); 369 | 370 | mSharedPreferences.edit() 371 | .putString(SyncToDriveService.PREF_DRIVE_ID, "") 372 | .apply(); 373 | 374 | 375 | 376 | mSharedPreferences.edit() 377 | .putString(getCurrentOfficeSaveIDPref(), "") 378 | .apply(); 379 | 380 | // Commit here so that we make sure these items are changed before 381 | // we read the strings again and update the settings page 382 | 383 | mSharedPreferences.edit() 384 | .putString(SyncToDriveService.PREF_DRIVE_ID_LAST_SAVE_TIME, "") 385 | .commit(); 386 | 387 | SetDriveStringDetails(); 388 | 389 | } 390 | 391 | @Override 392 | public boolean onOptionsItemSelected(MenuItem item) { 393 | int id = item.getItemId(); 394 | if (id == android.R.id.home) { 395 | NavUtils.navigateUpFromSameTask(this.getActivity()); 396 | return true; 397 | } 398 | return super.onOptionsItemSelected(item); 399 | } 400 | } 401 | 402 | 403 | /** 404 | * An edit text preference which is used when the custom option is selected for the office 405 | */ 406 | public static class OfficePreference extends EditTextPreference { 407 | 408 | public OfficePreference(Context context, PreferenceManager preferenceManager) { 409 | super(context); 410 | setKey(WeightRecorder.PREF_OFFICE); 411 | onAttachedToHierarchy(preferenceManager); 412 | getEditText().setSelectAllOnFocus(true); 413 | } 414 | 415 | public void showDialog() { 416 | showDialog(null); 417 | } 418 | 419 | @Override 420 | public void onDismiss(DialogInterface dialog) { 421 | super.onDismiss(dialog); 422 | } 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /divertsy_client/src/main/java/com/divertsy/hid/SyncEventService.java: -------------------------------------------------------------------------------- 1 | package com.divertsy.hid; 2 | 3 | import android.util.Log; 4 | import android.widget.Toast; 5 | 6 | import com.google.android.gms.common.api.ResultCallback; 7 | import com.google.android.gms.drive.DriveId; 8 | import com.google.android.gms.drive.DriveResource; 9 | import com.google.android.gms.drive.Metadata; 10 | import com.google.android.gms.drive.events.CompletionEvent; 11 | import com.google.android.gms.drive.events.DriveEventService; 12 | 13 | /** 14 | * SyncEventService is used to respond to Events from the Google Drive API. 15 | * At this time, Divertsy only uses it to capture the ResourceId of the 16 | * file which gets saved into Google Drive. Before this event, we have a FileID 17 | * but the ResourceId is needed so we can access and update the same file each time. 18 | * 19 | */ 20 | public class SyncEventService extends DriveEventService { 21 | private static final String TAG = "SyncEventService"; 22 | 23 | @Override 24 | public void onCompletion(CompletionEvent event) { super.onCompletion(event); 25 | Log.i(TAG, "New SyncEventService completion triggered"); 26 | 27 | try{ 28 | DriveId driveId = event.getDriveId(); 29 | String driveResourceID = driveId.getResourceId(); 30 | SyncToDriveService sd = new SyncToDriveService(); 31 | 32 | switch (event.getStatus()) { 33 | case CompletionEvent.STATUS_CONFLICT: 34 | Log.e(TAG, "STATUS_CONFLICT"); 35 | event.dismiss(); 36 | break; 37 | case CompletionEvent.STATUS_FAILURE: 38 | Log.e(TAG, "STATUS_FAILURE"); 39 | String message = "Divertsy Sync Failed. You may need to reconnect Google Drive in Sync Settings."; 40 | Toast.makeText(this, message, Toast.LENGTH_LONG).show(); 41 | event.dismiss(); 42 | break; 43 | case CompletionEvent.STATUS_SUCCESS: 44 | sd.SaveDriveID(driveResourceID, getApplicationContext()); 45 | event.dismiss(); 46 | break; 47 | } 48 | } catch (Exception e){ 49 | Log.e(TAG, "Failed:" + e.getMessage()); 50 | } 51 | 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /divertsy_client/src/main/java/com/divertsy/hid/SyncToDriveService.java: -------------------------------------------------------------------------------- 1 | package com.divertsy.hid; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.IntentSender; 6 | import android.content.SharedPreferences; 7 | import android.os.Bundle; 8 | import android.app.Service; 9 | import android.app.PendingIntent; 10 | import android.os.IBinder; 11 | import android.util.Log; 12 | import android.widget.Toast; 13 | 14 | import com.divertsy.hid.utils.Utils; 15 | import com.divertsy.hid.utils.WeightRecorder; 16 | import com.google.android.gms.common.ConnectionResult; 17 | import com.google.android.gms.common.GoogleApiAvailability; 18 | import com.google.android.gms.common.api.GoogleApiClient; 19 | import com.google.android.gms.common.api.ResultCallback; 20 | import com.google.android.gms.drive.Drive; 21 | import com.google.android.gms.drive.DriveApi; 22 | import com.google.android.gms.drive.DriveContents; 23 | import com.google.android.gms.drive.DriveFile; 24 | import com.google.android.gms.drive.DriveFolder; 25 | import com.google.android.gms.drive.DriveResource; 26 | import com.google.android.gms.drive.ExecutionOptions; 27 | import com.google.android.gms.drive.Metadata; 28 | import com.google.android.gms.drive.MetadataChangeSet; 29 | import com.google.android.gms.drive.metadata.CustomPropertyKey; 30 | 31 | import java.io.BufferedReader; 32 | import java.io.File; 33 | import java.io.FileInputStream; 34 | import java.io.IOException; 35 | import java.io.InputStreamReader; 36 | import java.io.OutputStream; 37 | import java.io.OutputStreamWriter; 38 | import java.io.Writer; 39 | import java.text.SimpleDateFormat; 40 | import java.util.Date; 41 | 42 | import static com.google.android.gms.common.ConnectionResult.SERVICE_MISSING; 43 | import static com.google.android.gms.common.ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED; 44 | 45 | /** 46 | * SyncToDriveService handles creating the file which will be saved to Drive. 47 | * If you build from source, you will need to register your signing keys with 48 | * Google in order to use this API on a device. More information on the wiki. 49 | * https://developers.google.com/drive/android/auth 50 | */ 51 | public class SyncToDriveService extends Service implements 52 | GoogleApiClient.ConnectionCallbacks, 53 | GoogleApiClient.OnConnectionFailedListener { 54 | 55 | private static final String TAG = "SyncToDriveService"; 56 | public static final String PREFERENCES_NAME = "DrivePrefs"; 57 | public static final String PREF_USE_GOOGLE_DRIVE = "use_google_drive"; 58 | public static final String PREF_DRIVE_ID = "drive_id"; 59 | public static final String PREF_DRIVE_ID_LAST_SAVE_TIME = "last_save_time"; 60 | String sPreviousDriveID; 61 | 62 | protected static final int REQUEST_CODE_RESOLUTION = 1; 63 | 64 | protected GoogleApiClient mGoogleApiClient; 65 | protected boolean shouldClearAccount = false; 66 | 67 | SharedPreferences mSharedPreferences; 68 | protected int mStartID; 69 | 70 | @Override 71 | public IBinder onBind(Intent intent) { 72 | return null; 73 | } 74 | 75 | @Override 76 | public int onStartCommand(Intent intent, int flags, int startId) { 77 | mStartID = startId; 78 | 79 | // Check if we got an intent to clear the current login 80 | if (intent.getBooleanExtra("clear_drive_data", false)){ 81 | // Could be a race condition here, thus we need to set a flag to clear the account 82 | // in case we're not connected yet, but also call ClearAccount if we are connected. 83 | shouldClearAccount = true; 84 | if (getGoogleApiClient().isConnected()) { 85 | ClearAccount(); 86 | } 87 | } else { 88 | // Only sync if the use Google Drive setting is true 89 | if (mSharedPreferences.getBoolean(PREF_USE_GOOGLE_DRIVE, false)) { 90 | Toast.makeText(this, "Starting Google Drive Sync", Toast.LENGTH_SHORT).show(); 91 | FindOrCreateDriveFile(); 92 | } 93 | } 94 | 95 | return Service.START_NOT_STICKY; 96 | } 97 | 98 | @Override 99 | public void onCreate(){ 100 | super.onCreate(); 101 | mSharedPreferences = getApplicationContext().getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); 102 | connectClient(); 103 | } 104 | 105 | public String getCurrentOffice(){ 106 | Context context = getApplicationContext(); 107 | if(context != null){ 108 | return getCurrentOffice(context); 109 | } 110 | Log.e(TAG, "Could not get Application Context when calling getCurrentOffice"); 111 | return WeightRecorder.DEFAULT_OFFICE; 112 | } 113 | 114 | public String getCurrentOffice(Context context){ 115 | SharedPreferences officePrefs = context.getSharedPreferences(WeightRecorder.PREFERENCES_NAME, Context.MODE_PRIVATE); 116 | return officePrefs.getString(WeightRecorder.PREF_OFFICE, WeightRecorder.DEFAULT_OFFICE); 117 | } 118 | 119 | protected void connectClient() { 120 | Log.i(TAG, "Calling connectClient"); 121 | if (mGoogleApiClient == null) { 122 | mGoogleApiClient = new GoogleApiClient.Builder(this) 123 | .addApi(Drive.API) 124 | .addScope(Drive.SCOPE_FILE) 125 | .addConnectionCallbacks(this) 126 | .addOnConnectionFailedListener(this) 127 | .build(); 128 | } 129 | mGoogleApiClient.connect(); 130 | } 131 | 132 | public void ClearAccount(){ 133 | Log.i(TAG, "Clearing Google API Account"); 134 | GoogleApiClient mAPI = getGoogleApiClient(); 135 | if (mAPI != null) { 136 | if(mAPI.isConnected()){ 137 | mAPI.clearDefaultAccountAndReconnect(); 138 | Log.i(TAG, "Clear account and reconnect called"); 139 | } else { 140 | Log.w(TAG, "Google API client not connected when attempting disconnect"); 141 | Toast.makeText(this, "Google API not connected. Make sure WiFi is On.", Toast.LENGTH_LONG).show(); 142 | } 143 | } else { 144 | Log.w(TAG, "Google API was null when attempting to disconnect account"); 145 | } 146 | stopSelf(mStartID); 147 | } 148 | 149 | 150 | /** 151 | * Called when activity gets invisible. Connection to Drive service needs to 152 | * be disconnected as soon as an activity is invisible. 153 | */ 154 | @Override 155 | public void onDestroy() { 156 | if (mGoogleApiClient != null) { 157 | mGoogleApiClient.disconnect(); 158 | } 159 | super.onDestroy(); 160 | } 161 | 162 | 163 | @Override 164 | public void onConnected(Bundle connectionHint) { 165 | Log.i(TAG, "GoogleApiClient connected"); 166 | if (shouldClearAccount){ 167 | ClearAccount(); 168 | } 169 | } 170 | 171 | @Override 172 | public void onConnectionSuspended(int cause) { 173 | Log.i(TAG, "GoogleApiClient connection suspended"); 174 | } 175 | 176 | @Override 177 | public void onConnectionFailed(ConnectionResult result) { 178 | Log.i(TAG, "GoogleApiClient connection failed: " + result.toString()); 179 | 180 | if (result.getErrorCode() == SERVICE_MISSING){ 181 | showMessage("Google Drive API not Found on this Device"); 182 | Log.e(TAG, "Google Drive SERVICE_MISSING"); 183 | } 184 | 185 | if (result.getErrorCode() == SERVICE_VERSION_UPDATE_REQUIRED){ 186 | showMessage("Google Play Services update required. Please update in Google Play store."); 187 | Log.e(TAG, "Google Play Services update required"); 188 | mGoogleApiClient.getContext().startActivity(new Intent(mGoogleApiClient.getContext(), MainActivity.class) 189 | .putExtra("PlayServicesUpdate", true).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 190 | } 191 | 192 | PendingIntent pI = result.getResolution(); 193 | if (pI != null) { 194 | Log.v(TAG, "PendingIntent: " + pI.getIntentSender().toString()); 195 | mGoogleApiClient.getContext().startActivity(new Intent(mGoogleApiClient.getContext(), MainActivity.class) 196 | .putExtra("resolution", pI).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 197 | } else { 198 | Log.e(TAG, "Pending Intent resolution was Null"); 199 | } 200 | 201 | // If we don't stop this service, we tend to have an invalid API client 202 | // when we call the first Create new Drive file command 203 | stopSelf(mStartID); 204 | } 205 | 206 | 207 | public void showMessage(String message) { 208 | Toast.makeText(this, message, Toast.LENGTH_LONG).show(); 209 | Log.d(TAG,"SHOW MESSAGE: " + message); 210 | } 211 | 212 | 213 | public GoogleApiClient getGoogleApiClient() { 214 | return mGoogleApiClient; 215 | } 216 | 217 | void FindOrCreateDriveFile(){ 218 | // Check if we already have a driveID file 219 | Context context = getApplicationContext(); 220 | SharedPreferences mSharedPreferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); 221 | sPreviousDriveID = mSharedPreferences.getString(PREF_DRIVE_ID,""); 222 | if (sPreviousDriveID.length() > 0){ 223 | Log.v(TAG, "Using saved drive ID: " + sPreviousDriveID); 224 | Drive.DriveApi.fetchDriveId(getGoogleApiClient(),sPreviousDriveID) 225 | .setResultCallback(driveFetchIDCallback); 226 | } else { 227 | // create new contents resource 228 | Log.v(TAG, "Attempting to create new drive file"); 229 | Drive.DriveApi.newDriveContents(getGoogleApiClient()) 230 | .setResultCallback(driveContentsCallback); 231 | } 232 | } 233 | 234 | /** 235 | * WriteDriveFile will pull in the CSV file for the current office and 236 | * write its data to the drive file, which is saved locally when the call back 237 | * is received. 238 | * 239 | * @param driveContents where the CSV data will go in Google Drive 240 | */ 241 | void WriteDriveFile(final DriveContents driveContents){ 242 | new Thread() { 243 | @Override 244 | public void run() { 245 | // write content to DriveContents 246 | 247 | try { 248 | Log.v(TAG, "Starting Write to Drive File"); 249 | OutputStream outputStream = driveContents.getOutputStream(); 250 | Writer writer = new OutputStreamWriter(outputStream); 251 | 252 | File csv = new File(Utils.getDivertsyFilePath(getCurrentOffice())); 253 | FileInputStream instream = new FileInputStream(csv); 254 | BufferedReader reader = new BufferedReader(new InputStreamReader(instream)); 255 | String line = reader.readLine(); 256 | while(line != null){ 257 | writer.write(line + System.getProperty("line.separator")); 258 | line = reader.readLine(); 259 | } 260 | writer.close(); 261 | Log.v(TAG, "Drive File Closed"); 262 | } catch (IOException e) { 263 | Log.v(TAG, "Error writing to drive file"); 264 | Log.e(TAG, e.getMessage()); 265 | } 266 | 267 | // Check if we're writing a brand new file or updating an old one. 268 | if (sPreviousDriveID.length() > 0 ) { 269 | driveContents.commit(getGoogleApiClient(), null, 270 | new ExecutionOptions.Builder() 271 | .setNotifyOnCompletion(true) 272 | .build() 273 | ); 274 | } else { 275 | 276 | // New file, so we'll set the file name and properties 277 | String driveFileName = "DivertsyData-" + getCurrentOffice() + ".csv"; 278 | CustomPropertyKey officePropertyKey = new CustomPropertyKey("DivertsyOffice", CustomPropertyKey.PRIVATE); 279 | 280 | MetadataChangeSet changeSet = new MetadataChangeSet.Builder() 281 | .setTitle(driveFileName) 282 | .setMimeType("text/csv") 283 | .setDescription("Divertsy Waste Stream Data File") 284 | .setCustomProperty(officePropertyKey,getCurrentOffice()) 285 | .setStarred(false).build(); 286 | 287 | Drive.DriveApi.getRootFolder(getGoogleApiClient()) 288 | .createFile(getGoogleApiClient(), changeSet, driveContents, 289 | new ExecutionOptions.Builder() 290 | .setNotifyOnCompletion(true) 291 | .build() 292 | ) 293 | .setResultCallback(fileCallback); 294 | } 295 | 296 | } 297 | }.start(); 298 | } 299 | 300 | 301 | // Used to clear our old drive ID file name, then try to save again to a new file 302 | private void ResetDriveFile(){ 303 | Log.e(TAG, "Resetting saved Drive file ID. This may make a new file in Drive."); 304 | SaveDriveID("", getApplicationContext()); 305 | sPreviousDriveID = ""; 306 | FindOrCreateDriveFile(); 307 | } 308 | 309 | // Opens a file that was previously created and then writes data to it 310 | private void OpenDriveFile(final DriveFile file){ 311 | new Thread() { 312 | @Override 313 | public void run() { 314 | DriveApi.DriveContentsResult driveContentsResult = 315 | file.open(getGoogleApiClient(), DriveFile.MODE_WRITE_ONLY, null).await(); 316 | if (!driveContentsResult.getStatus().isSuccess()) { 317 | showMessage("Error while trying to get previous Drive File"); 318 | return; 319 | } 320 | DriveContents driveContents = driveContentsResult.getDriveContents(); 321 | WriteDriveFile(driveContents); 322 | } 323 | }.start(); 324 | } 325 | 326 | 327 | final private ResultCallback driveFetchIDCallback = new 328 | ResultCallback() { 329 | @Override 330 | public void onResult(final DriveApi.DriveIdResult result) { 331 | if (!result.getStatus().isSuccess()) { 332 | ResetDriveFile(); 333 | return; 334 | } 335 | OpenDriveFile(result.getDriveId().asDriveFile()); 336 | } 337 | }; 338 | 339 | // Creates a new Drive file then writes data to it 340 | final private ResultCallback driveContentsCallback = new 341 | ResultCallback() { 342 | @Override 343 | public void onResult(DriveApi.DriveContentsResult result) { 344 | if (!result.getStatus().isSuccess()) { 345 | showMessage("Error while trying to create new file contents"); 346 | return; 347 | } 348 | Log.v(TAG, "driveContentsCallback got valid result"); 349 | final DriveContents driveContents = result.getDriveContents(); 350 | WriteDriveFile(driveContents); 351 | } 352 | }; 353 | 354 | // Called after data is written to the drive file 355 | final ResultCallback fileCallback = new 356 | ResultCallback() { 357 | @Override 358 | public void onResult(DriveFolder.DriveFileResult result) { 359 | if (!result.getStatus().isSuccess()) { 360 | showMessage("Error while trying to create the file"); 361 | return; 362 | } 363 | String sDriveID = result.getDriveFile().getDriveId().encodeToString(); 364 | showMessage("Local Save to Google Drive"); 365 | } 366 | }; 367 | 368 | // After we get an Event from the SyncEvent Service, look up the meta data and store the office ID 369 | ResultCallback metadataRetrievedCallback = new 370 | ResultCallback() { 371 | 372 | @Override 373 | public void onResult(DriveResource.MetadataResult result) { 374 | if (!result.getStatus().isSuccess()) { 375 | showMessage("Problem while trying to fetch metadata"); 376 | return; 377 | } 378 | Metadata metadata = result.getMetadata(); 379 | showMessage("Metadata successfully fetched. Title: " + metadata.getTitle()); 380 | } 381 | }; 382 | 383 | 384 | // Save the ID and time of the Drive file so that we can write to the same one next time. 385 | final public void SaveDriveID(String driveID, Context context){ 386 | Log.v(TAG, "Saving Drive ID: " + driveID); 387 | 388 | SharedPreferences mSharedPreferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); 389 | mSharedPreferences.edit() 390 | .putString(PREF_DRIVE_ID, driveID) 391 | .apply(); 392 | 393 | // Save with Office Name 394 | mSharedPreferences.edit() 395 | .putString(PREF_DRIVE_ID + ":" + getCurrentOffice(context), driveID) 396 | .apply(); 397 | 398 | SimpleDateFormat s = new SimpleDateFormat("E MMM dd, yyyy HH:mm:ss z"); 399 | String sdate = s.format(new Date()); 400 | 401 | // If the name got cleared, then also clear the last saved date/time 402 | if(driveID.length()<1){ 403 | sdate = ""; 404 | } 405 | 406 | mSharedPreferences.edit() 407 | .putString(PREF_DRIVE_ID_LAST_SAVE_TIME, sdate) 408 | .apply(); 409 | } 410 | 411 | } 412 | -------------------------------------------------------------------------------- /divertsy_client/src/main/java/com/divertsy/hid/WasteStreams.java: -------------------------------------------------------------------------------- 1 | package com.divertsy.hid; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.graphics.Color; 6 | import android.util.Log; 7 | 8 | 9 | import com.divertsy.hid.utils.WeightRecorder; 10 | 11 | import org.json.JSONArray; 12 | import org.json.JSONException; 13 | import org.json.JSONObject; 14 | import org.json.JSONTokener; 15 | 16 | import java.io.BufferedReader; 17 | import java.io.InputStreamReader; 18 | import java.util.ArrayList; 19 | import java.util.HashSet; 20 | import java.util.List; 21 | import java.util.Set; 22 | 23 | /** 24 | * SettingsActivity handles parsing the waste_streams.json file for button information 25 | * and returning that data to the main and settings activity. 26 | */ 27 | public class WasteStreams { 28 | 29 | private static final String TAG = "DIVERTSY"; 30 | private List StreamNames; 31 | private List StreamValues; 32 | private List DefaultStreamValues; 33 | private List ButtonColors; 34 | private String LanguageSetting; 35 | 36 | public static final String JSON_DISPLAY_NAME = "display_name"; 37 | 38 | public void loadWasteStreams(Context context){ 39 | StringBuilder json = new StringBuilder(); 40 | StreamNames = new ArrayList(); 41 | StreamValues = new ArrayList(); 42 | ButtonColors = new ArrayList(); 43 | DefaultStreamValues = new ArrayList(); 44 | String displayNameField = JSON_DISPLAY_NAME; 45 | 46 | SharedPreferences prefs = context.getSharedPreferences(WeightRecorder.PREFERENCES_NAME, Context.MODE_PRIVATE); 47 | LanguageSetting = prefs.getString(WeightRecorder.PREF_LANGUAGE, ""); 48 | if (LanguageSetting.length() > 0){ 49 | displayNameField = JSON_DISPLAY_NAME + "_" + LanguageSetting; 50 | } 51 | 52 | try { 53 | BufferedReader reader = new BufferedReader(new InputStreamReader( 54 | context.getResources().openRawResource(R.raw.waste_streams))); 55 | String line; 56 | while((line = reader.readLine()) != null) { 57 | json.append(line); 58 | } 59 | JSONArray waste_streams = (JSONArray) new JSONTokener(json.toString()).nextValue(); 60 | 61 | 62 | for (int i = 0; i < waste_streams.length(); i++) { 63 | JSONObject waste_stream = waste_streams.getJSONObject(i); 64 | 65 | // Check if this tag has the proper language, otherwise get the default 66 | String streamName; 67 | try { 68 | streamName = waste_stream.getString(displayNameField); 69 | } catch (JSONException e) { 70 | streamName = waste_stream.getString(JSON_DISPLAY_NAME); 71 | } 72 | 73 | Log.d(TAG, "Loading Stream: " + streamName); 74 | 75 | 76 | StreamNames.add(streamName); 77 | 78 | StreamValues.add(waste_stream.getString("logged_data_name")); 79 | ButtonColors.add(waste_stream.getString("button_color")); 80 | if(waste_stream.getBoolean("is_default")){ 81 | DefaultStreamValues.add(waste_stream.getString("logged_data_name")); 82 | } 83 | } 84 | 85 | } catch (Exception e){ 86 | Log.e(TAG, e.getLocalizedMessage()); 87 | } 88 | 89 | // If there are no saved waste streams, set the value to the default streams 90 | Set savedStreams = prefs.getStringSet(WeightRecorder.PREF_WASTE_STREAMS, null); 91 | if ((savedStreams == null) || (savedStreams.size() == 0)){ 92 | prefs.edit().putStringSet(WeightRecorder.PREF_WASTE_STREAMS,getDefaultStreamValuesSet()).apply(); 93 | } 94 | 95 | } 96 | 97 | public CharSequence[] getAllStreamNames(){ 98 | return StreamNames.toArray(new CharSequence[StreamNames.size()]); 99 | } 100 | 101 | public CharSequence[] getAllStreamValues(){ 102 | return StreamValues.toArray(new CharSequence[StreamValues.size()]); 103 | } 104 | 105 | public CharSequence[] getDefaultStreamValues(){ 106 | return DefaultStreamValues.toArray(new CharSequence[DefaultStreamValues.size()]); 107 | } 108 | 109 | public Set getDefaultStreamValuesSet(){ 110 | return new HashSet(DefaultStreamValues); 111 | } 112 | 113 | public String getDisplayNameFromValue(String value){ 114 | int index = StreamValues.indexOf(value); 115 | if (index < 0){ 116 | Log.e(TAG, "Stream value not found: " + value); 117 | return ""; 118 | } 119 | return StreamNames.get(index); 120 | } 121 | 122 | public Integer getButtonColorFromValue(String value){ 123 | Integer color = Color.parseColor("#FF555555"); 124 | int index = StreamValues.indexOf(value); 125 | if (index < 0){ 126 | Log.e(TAG, "Stream value not found: " + value); 127 | return color; 128 | } 129 | String input_color = ButtonColors.get(index); 130 | try{ 131 | color = Color.parseColor(input_color); 132 | } catch(Exception e) { 133 | Log.e(TAG, "button_color decoded failed: " + input_color); 134 | } 135 | return color; 136 | } 137 | 138 | // Sorts the buttons so the order matches the JSON file 139 | // This will also silently drop saved streams if they are no longer in the JSON file 140 | public List getSortedStreams(Set unsorted){ 141 | List sorted = new ArrayList(); 142 | for (String streamValue: StreamValues){ 143 | if (unsorted.contains(streamValue)){ 144 | sorted.add(streamValue); 145 | } 146 | } 147 | return sorted; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /divertsy_client/src/main/java/com/divertsy/hid/ble/BLEScanner.java: -------------------------------------------------------------------------------- 1 | package com.divertsy.hid.ble; 2 | 3 | import android.Manifest; 4 | import android.annotation.TargetApi; 5 | import android.app.Activity; 6 | import android.app.AlertDialog; 7 | import android.bluetooth.BluetoothAdapter; 8 | import android.bluetooth.BluetoothManager; 9 | import android.bluetooth.le.BluetoothLeScanner; 10 | import android.bluetooth.le.ScanCallback; 11 | import android.bluetooth.le.ScanFilter; 12 | import android.bluetooth.le.ScanRecord; 13 | import android.bluetooth.le.ScanResult; 14 | import android.bluetooth.le.ScanSettings; 15 | import android.content.Context; 16 | import android.content.DialogInterface; 17 | import android.content.Intent; 18 | import android.content.pm.PackageManager; 19 | import android.net.Uri; 20 | import android.os.Build; 21 | import android.os.Handler; 22 | import android.os.Looper; 23 | import android.os.ParcelUuid; 24 | import android.support.annotation.NonNull; 25 | import android.support.annotation.Nullable; 26 | import android.support.v4.app.ActivityCompat; 27 | import android.util.Log; 28 | import android.widget.Toast; 29 | 30 | import java.util.ArrayList; 31 | import java.util.HashMap; 32 | import java.util.Iterator; 33 | import java.util.List; 34 | import java.util.Map; 35 | 36 | import static android.Manifest.permission.ACCESS_FINE_LOCATION; 37 | 38 | /** 39 | * 40 | * This code will look for Eddystone URL beacons in order to pull in location data. 41 | * The URL in the beacon must use the hostname defined below, otherwise it will be ignored. 42 | * There is no server side lookup of beacon information, nor will we try to fetch the URL 43 | * sent from the beacon. Instead, we parse the URL to pull back the "floor" and "location" 44 | * data and save it along with the next weight that is collected. 45 | * 46 | * The Beacon URL should follow the following structure (and is limited to 64 bytes total) 47 | * http://HAX/F1/KITCHEN 48 | * where "HAX" is the hostname we have defined below to look for, "F1" is the floor information 49 | * to record, and "KITCHEN" is the location data to record. 50 | * 51 | */ 52 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 53 | public class BLEScanner { 54 | 55 | // This is the HOST name we'll look for in Beacons 56 | // If it is found, we'll try to parse out location data from the URL 57 | private static final String BEACON_HOST_NAME = "HAX"; 58 | 59 | // Assume the last Beacon location until we've lost the signal 60 | // fro the amount of milli-seconds defined here 61 | private static final int ON_LOST_TIMEOUT_MS = 5000; 62 | 63 | private static final String TAG = "BLEScanner"; 64 | private static final int PERMISSION_REQUEST_COARSE_LOCATION = 2; 65 | 66 | 67 | public interface OnClosestChangedListener { 68 | void onClosestChanged(@Nullable Beacon closest); 69 | } 70 | 71 | // An aggressive scan for nearby devices that reports immediately. 72 | private static final ScanSettings SCAN_SETTINGS = 73 | new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).setReportDelay(0) 74 | .build(); 75 | 76 | private static final Handler handler = new Handler(Looper.getMainLooper()); 77 | 78 | // The Eddystone Service UUID, 0xFEAA. 79 | private static final ParcelUuid EDDYSTONE_SERVICE_UUID = 80 | ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB"); 81 | private final int mRequestEnableBluetooth; 82 | private final OnClosestChangedListener mOnClosestChangedListener; 83 | 84 | private BluetoothLeScanner scanner; 85 | 86 | private List scanFilters; 87 | private ScanCallback scanCallback; 88 | 89 | private Map deviceToBeaconMap = new HashMap<>(); 90 | 91 | private Beacon mClosest; 92 | 93 | public BLEScanner(final Activity activity, int requestEnableBluetooth, @NonNull OnClosestChangedListener onClosestChangedListener) { 94 | mRequestEnableBluetooth = requestEnableBluetooth; 95 | mOnClosestChangedListener = onClosestChangedListener; 96 | init(activity); 97 | scanFilters = new ArrayList<>(); 98 | scanFilters.add(new ScanFilter.Builder().setServiceUuid(EDDYSTONE_SERVICE_UUID).build()); 99 | scanCallback = new ScanCallback() { 100 | @Override 101 | public void onScanResult(int callbackType, ScanResult result) { 102 | ScanRecord scanRecord = result.getScanRecord(); 103 | if (scanRecord == null) { 104 | return; 105 | } 106 | 107 | String deviceAddress = result.getDevice().getAddress(); 108 | Beacon beacon; 109 | if (!deviceToBeaconMap.containsKey(deviceAddress)) { 110 | beacon = new Beacon(deviceAddress, result.getRssi()); 111 | deviceToBeaconMap.put(deviceAddress, beacon); 112 | } else { 113 | deviceToBeaconMap.get(deviceAddress).lastSeenTimestamp = System.currentTimeMillis(); 114 | deviceToBeaconMap.get(deviceAddress).rssi = result.getRssi(); 115 | } 116 | 117 | byte[] serviceData = scanRecord.getServiceData(EDDYSTONE_SERVICE_UUID); 118 | validateServiceData(deviceAddress, serviceData); 119 | 120 | findClosest(); 121 | } 122 | 123 | @Override 124 | public void onScanFailed(int errorCode) { 125 | switch (errorCode) { 126 | case SCAN_FAILED_ALREADY_STARTED: 127 | logErrorAndShowToast(activity, "SCAN_FAILED_ALREADY_STARTED"); 128 | break; 129 | case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED: 130 | logErrorAndShowToast(activity, "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED"); 131 | break; 132 | case SCAN_FAILED_FEATURE_UNSUPPORTED: 133 | logErrorAndShowToast(activity, "SCAN_FAILED_FEATURE_UNSUPPORTED"); 134 | break; 135 | case SCAN_FAILED_INTERNAL_ERROR: 136 | logErrorAndShowToast(activity, "SCAN_FAILED_INTERNAL_ERROR"); 137 | break; 138 | default: 139 | logErrorAndShowToast(activity, "Scan failed, unknown error code"); 140 | break; 141 | } 142 | } 143 | }; 144 | } 145 | 146 | public void onPause() { 147 | if (scanner != null) { 148 | scanner.stopScan(scanCallback); 149 | } 150 | } 151 | 152 | public void onResume() { 153 | handler.removeCallbacksAndMessages(null); 154 | 155 | setOnLostRunnable(); 156 | 157 | if (scanner != null) { 158 | scanner.startScan(scanFilters, SCAN_SETTINGS, scanCallback); 159 | } 160 | } 161 | 162 | public void onRequestPermissionsResult(int requestCode, int[] grantResults) { 163 | switch (requestCode) { 164 | case PERMISSION_REQUEST_COARSE_LOCATION: { 165 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 166 | Log.d(TAG, "PERMISSION_REQUEST_COARSE_LOCATION granted"); 167 | } 168 | } 169 | 170 | } 171 | } 172 | 173 | @Nullable 174 | public String getClosestLocation() { 175 | return mClosest == null ? null : mClosest.urlStatus.toString(); 176 | } 177 | 178 | private void setOnLostRunnable() { 179 | Runnable removeLostDevices = new Runnable() { 180 | @Override 181 | public void run() { 182 | long time = System.currentTimeMillis(); 183 | Iterator> 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 |

5 | 13 | 19 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------