From 2be50e6643b4cef7c56629835d44da8f33a931bf Mon Sep 17 00:00:00 2001 From: ktind Date: Mon, 1 Sep 2014 23:13:50 -0500 Subject: [PATCH] New features -Pebble support as a companion app (It should work with the 4.0 branch but will need some modifications to work efficiently with accurate timing data) -Quick dial - Associate a contact to a device (right now only supports device_1) and it will call the contact associated with that device. Implemented as a button in the app as well as actions in the notification. Bug fixes: -Trying to cleanup crashes on stop due to null pointer exceptions in the android notification - I'm not sure I covered all of them. -Moved some values to Constants.java -Cleaned up settings activity and added more summary bindings -Fixed an issue with the SQLiteMonitor that leaked a cursor during exceptions. --- .idea/gradle.xml | 1 + .idea/libraries/guava_14_0_1.xml | 11 + .idea/modules.xml | 1 + mobile/build.gradle | 1 + mobile/mobile.iml | 2 + mobile/src/main/AndroidManifest.xml | 21 +- .../com/ktind/cgm/bgscout/AbstractDevice.java | 59 +- .../bgscout/AndroidNotificationMonitor.java | 116 ++- .../java/com/ktind/cgm/bgscout/Constants.java | 5 + .../com/ktind/cgm/bgscout/DeviceActivity.java | 501 +++++++++- .../cgm/bgscout/DeviceDownloadService.java | 4 +- .../cgm/bgscout/DexcomG4/G4EGVRecord.java | 1 + .../com/ktind/cgm/bgscout/MainActivity.java | 154 ++- .../ktind/cgm/bgscout/MongoUploadMonitor.java | 4 +- .../com/ktind/cgm/bgscout/PebbleMonitor.java | 149 +++ .../ktind/cgm/bgscout/RemoteMQTTDevice.java | 33 +- .../ktind/cgm/bgscout/RemoteMongoDevice.java | 4 +- .../com/ktind/cgm/bgscout/SQLiteMonitor.java | 3 +- .../ktind/cgm/bgscout/SettingsActivity.java | 189 ++-- .../bgscout/model/DownloadSQLiteHelper.java | 22 +- .../com/ktind/cgm/bgscout/mqtt/MQTTMgr.java | 71 +- .../src/main/res/drawable-hdpi/ic_snooze.png | Bin 0 -> 1880 bytes .../src/main/res/layout/activity_device.xml | 3 +- mobile/src/main/res/layout/activity_main.xml | 32 +- .../src/main/res/layout/fragment_device.xml | 26 +- mobile/src/main/res/values/strings.xml | 3 +- mobile/src/main/res/xml/pref_general.xml | 20 + pEBBLE_KIT/build.gradle | 21 + .../getpebble/android/kit/BuildConfig.java | 13 + .../getpebble/android/kit/BuildConfig.java | 13 + .../r/debug/com/getpebble/android/kit/R.java | 19 + .../release/com/getpebble/android/kit/R.java | 19 + pEBBLE_KIT/pEBBLE_KIT.iml | 87 ++ pEBBLE_KIT/src/main/AndroidManifest.xml | 15 + .../com/getpebble/android/kit/Constants.java | 366 +++++++ .../com/getpebble/android/kit/PebbleKit.java | 914 ++++++++++++++++++ .../android/kit/util/PebbleDictionary.java | 370 +++++++ .../android/kit/util/PebbleTuple.java | 121 +++ pEBBLE_KIT/src/main/res/layout/main.xml | 13 + pEBBLE_KIT/src/main/res/values/strings.xml | 4 + settings.gradle | 1 + 41 files changed, 3109 insertions(+), 303 deletions(-) create mode 100644 .idea/libraries/guava_14_0_1.xml create mode 100644 mobile/src/main/java/com/ktind/cgm/bgscout/PebbleMonitor.java create mode 100644 mobile/src/main/res/drawable-hdpi/ic_snooze.png create mode 100644 pEBBLE_KIT/build.gradle create mode 100644 pEBBLE_KIT/build/generated/source/buildConfig/debug/com/getpebble/android/kit/BuildConfig.java create mode 100644 pEBBLE_KIT/build/generated/source/buildConfig/release/com/getpebble/android/kit/BuildConfig.java create mode 100644 pEBBLE_KIT/build/generated/source/r/debug/com/getpebble/android/kit/R.java create mode 100644 pEBBLE_KIT/build/generated/source/r/release/com/getpebble/android/kit/R.java create mode 100644 pEBBLE_KIT/pEBBLE_KIT.iml create mode 100644 pEBBLE_KIT/src/main/AndroidManifest.xml create mode 100644 pEBBLE_KIT/src/main/java/com/getpebble/android/kit/Constants.java create mode 100644 pEBBLE_KIT/src/main/java/com/getpebble/android/kit/PebbleKit.java create mode 100644 pEBBLE_KIT/src/main/java/com/getpebble/android/kit/util/PebbleDictionary.java create mode 100644 pEBBLE_KIT/src/main/java/com/getpebble/android/kit/util/PebbleTuple.java create mode 100644 pEBBLE_KIT/src/main/res/layout/main.xml create mode 100644 pEBBLE_KIT/src/main/res/values/strings.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml index a1d7cb3..f100aa7 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -9,6 +9,7 @@ diff --git a/.idea/libraries/guava_14_0_1.xml b/.idea/libraries/guava_14_0_1.xml new file mode 100644 index 0000000..5275114 --- /dev/null +++ b/.idea/libraries/guava_14_0_1.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 078e96c..f46ce42 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -4,6 +4,7 @@ + diff --git a/mobile/build.gradle b/mobile/build.gradle index f57d847..b059d8b 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -30,4 +30,5 @@ dependencies { compile 'com.android.support:support-v4:20.0.0' compile 'com.google.code.gson:gson:2.3' compile 'com.android.support:support-v13:20.0.0' + compile project(':pEBBLE_KIT') } diff --git a/mobile/mobile.iml b/mobile/mobile.iml index c153e03..1cfd26d 100644 --- a/mobile/mobile.iml +++ b/mobile/mobile.iml @@ -88,11 +88,13 @@ + + diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 7b1e7e1..9b64c53 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -12,6 +12,8 @@ + + @@ -26,15 +28,6 @@ android:enabled="true" android:exported="true" > - - - - - - - - - + + + + + + + + + + (); + String contactDataUri=sharedPref.getString(deviceIDStr+"_contact_data_uri",Uri.EMPTY.toString()); + phoneNum=getPhone(contactDataUri); + } + + public String getContactNum() { + return phoneNum; } public String getDeviceIDStr() { @@ -81,13 +93,19 @@ public void start(){ AbstractMonitor mon; monitors=new ArrayList(); - // FIXME - Mandatory monitor - mon=new SQLiteMonitor(getName(),deviceID,getAppContext()); - monitors.add(mon); + monitors.add(new SQLiteMonitor(getName(),deviceID,getAppContext())); + + if (sharedPref.getBoolean(deviceIDStr + "_pebble_monitor", false)) { + Log.i(TAG, "Adding a Pebble monitor"); + monitors.add(new PebbleMonitor(getName(),deviceID,getAppContext())); + } if (sharedPref.getBoolean(deviceIDStr + "_android_monitor", false)) { Log.i(TAG, "Adding a local android monitor"); mon = new AndroidNotificationMonitor(getName(), deviceID, getAppContext()); + if (phoneNum!=null) { + ((AndroidNotificationMonitor) mon).setPhoneNum(phoneNum); + } monitors.add(mon); } if (!isRemote()) { @@ -111,8 +129,12 @@ public void start(){ } Log.d(TAG, "Number of monitors created: " + monitors.size()); started=true; + IntentFilter intentFilter=new IntentFilter(Constants.UIDO_QUERY); + uiQuery=new AlarmReceiver(); + appContext.registerReceiver(uiQuery,intentFilter); } + // public void mainloop(){ // while(started){ // @@ -273,6 +295,7 @@ protected void onDownload(){ Log.d(TAG, "Not on the MAIN Thread ("+Thread.currentThread().getName()+"/"+Thread.currentThread().getState()+")"); SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(appContext); SharedPreferences.Editor editor = sharedPref.edit(); + // FIXME should this really be "last_g4_download"? Perhaps it should be "driver" editor.putLong(deviceIDStr + appContext.getText(R.string.last_g4_download), getLastDownloadObject().getLastReadingDate().getTime()); editor.apply(); } catch (NoDataException e) { @@ -283,14 +306,16 @@ protected void onDownload(){ } public void sendToUI(){ - Intent uiIntent = new Intent("com.ktind.cgm.UI_READING_UPDATE"); + Intent uiIntent = new Intent(Constants.UI_UPDATE); uiIntent.putExtra("deviceID",deviceIDStr); DownloadObject downloadObject=null; try { downloadObject=getLastDownloadObject(); + Log.d(TAG,"Name: "+downloadObject.getDeviceName()); } catch (NoDataException e) { downloadObject=new DownloadObject(); - e.printStackTrace(); + Log.e(TAG,"Sending empty DownloadObject",e); +// e.printStackTrace(); } finally { if (downloadObject!=null) { downloadObject.setDeviceID(deviceIDStr); @@ -300,7 +325,7 @@ public void sendToUI(){ } } // Log.d(TAG,"Sending broadcast to UI: "+uiIntent.getExtras().getString("download","")); - Log.d(TAG,"Name: "+downloadObject.getDeviceName()); + appContext.sendBroadcast(uiIntent); } @@ -311,6 +336,8 @@ public void setRemote(boolean remote) { @Override public void stop() { this.stopMonitors(); + if (appContext!=null && uiQuery!=null) + appContext.unregisterReceiver(uiQuery); started=false; } @@ -324,4 +351,24 @@ public void onReceive(Context context, Intent intent) { } } + private String getPhone(String uriString){ + return getPhone(Uri.parse(uriString)); + } + + private String getPhone(Uri dataUri){ + String id=dataUri.getLastPathSegment(); + Log.d(TAG,"id="+id); + Log.d(TAG,"URI="+dataUri); + // TODO Limit fields returned to specific field that we want? Phone? + Cursor cursor = appContext.getContentResolver().query(ContactsContract.Data.CONTENT_URI, null, ContactsContract.Data._ID + " = ?", new String[]{id}, null); + int numIdx = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); + Log.d(TAG, "cursor.getCount(): " + cursor.getCount()); + String phoneNum=null; + if (cursor.moveToFirst()){ + phoneNum=cursor.getString(numIdx); + } + cursor.close(); + return phoneNum; + } + } diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/AndroidNotificationMonitor.java b/mobile/src/main/java/com/ktind/cgm/bgscout/AndroidNotificationMonitor.java index 820df1a..c24be10 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/AndroidNotificationMonitor.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/AndroidNotificationMonitor.java @@ -4,17 +4,23 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; +import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.preference.PreferenceManager; +import android.provider.ContactsContract; import android.provider.Settings; import android.util.Log; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Date; @@ -43,8 +49,6 @@ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * TODO This is a HORRIBLE class. Needs to be refactored majorly - Completely violates DRY - * FIXME most of this needs to be passed in from the device itself as a message - that way the logic stays with the device and can be easily passed to the UI */ public class AndroidNotificationMonitor extends AbstractMonitor { private static final String TAG = AndroidNotificationMonitor.class.getSimpleName(); @@ -53,19 +57,19 @@ public class AndroidNotificationMonitor extends AbstractMonitor { final protected String monitorType="android notification"; protected boolean isSilenced=false; protected Date timeSilenced; - protected AlarmReceiver alarmReceiver=new AlarmReceiver(); + protected AlarmReceiver alarmReceiver; protected DownloadObject lastDownload; protected ArrayList previousDownloads=new ArrayList(); protected final int MAXPREVIOUS=3; private PendingIntent contentIntent = PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0); - private Bitmap bm = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon); +// private Bitmap bm = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon); + private Bitmap bm; private final int SNOOZEDURATION=1800000; private SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); // Good is defined as one that has all data that we need to convey our message private DownloadObject lastKnownGood; - private final int MINUPLOADERBATTERY=40; - private final int MINDEVICEBATTERY=20; - + protected String phoneNum=null; + protected AnalyzedDownload analyzedDownload; public void setNotifBuilder(Notification.Builder notifBuilder) { @@ -75,9 +79,19 @@ public void setNotifBuilder(Notification.Builder notifBuilder) { AndroidNotificationMonitor(String name,int devID,Context contxt){ super(name, devID, contxt, "android_notification"); init(); + + Uri uri=Uri.parse(sharedPref.getString(deviceIDStr+Constants.CONTACTDATAURISUFFIX,Uri.EMPTY.toString())); + if (! uri.equals(Uri.EMPTY)) { + InputStream inputStream = openDisplayPhoto(uri); + bm = Bitmap.createScaledBitmap(BitmapFactory.decodeStream(inputStream),200,200,true); + } + else { + bm = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon); + } } public void init(){ + Log.d(TAG,"Android notification monitor init called"); mNotifyMgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0); Bitmap bm = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon); @@ -92,10 +106,18 @@ public void init(){ mNotifyMgr.notify(deviceID, notification); sharedPref = PreferenceManager.getDefaultSharedPreferences(context); this.setAllowVirtual(true); -// alarmReceiver=new AlarmReceiver(); + alarmReceiver = new AlarmReceiver(); context.registerReceiver(alarmReceiver, new IntentFilter(Constants.SNOOZE_INTENT)); } + public String getPhoneNum() { + return phoneNum; + } + + public void setPhoneNum(String phoneNum) { + this.phoneNum = phoneNum; + } + @Override public void doProcess(DownloadObject dl) { if (previousDownloads!=null) { @@ -117,7 +139,7 @@ public void doProcess(DownloadObject dl) { // TODO add devicetype to the download object so that we can instantiate the proper analyzer AbstractDownloadAnalyzer downloadAnalyzer=new G4DownloadAnalyzer(dl, context); - AnalyzedDownload analyzedDownload=downloadAnalyzer.analyze(); + analyzedDownload=downloadAnalyzer.analyze(); if (isSilenced){ long duration=new Date().getTime()-timeSilenced.getTime(); @@ -151,6 +173,8 @@ private Notification buildNotification(AnalyzedDownload dl){ return notifBuilder.build(); } protected void setSound(AnalyzedDownload dl){ + if (isSilenced) + return; ArrayList conditions=dl.getConditions(); Uri uri = Uri.EMPTY; // allows us to give some sounds higher precedence than others @@ -317,12 +341,24 @@ protected void setActions(AnalyzedDownload dl){ snoozeIntent.putExtra("device", deviceIDStr); PendingIntent snoozePendIntent = PendingIntent.getBroadcast(context, deviceID, snoozeIntent, 0); // TODO make the snooze time configurable - String snoozeActionText="Snooze for "+(SNOOZEDURATION/1000)/60+" minutes"; - notifBuilder.addAction(android.R.drawable.ic_popup_reminder, snoozeActionText, snoozePendIntent); + // TODO dont hardcode this value - move it to strings.xml + String snoozeActionText="Snooze"; + notifBuilder.addAction(R.drawable.ic_snooze, snoozeActionText, snoozePendIntent); } } + } + if (phoneNum!=null) { + Intent callIntent = new Intent(Intent.ACTION_CALL); + callIntent.setData(Uri.parse("tel:" + phoneNum)); + // TODO switch over messages to strings.xml to be localalized easier. + PendingIntent callPendingIntent = PendingIntent.getActivity(context, 42, callIntent, 0); + notifBuilder.addAction(android.R.drawable.sym_action_call, "Call", callPendingIntent); + Intent smsIntent = new Intent(Intent.ACTION_VIEW, Uri.fromParts("sms",phoneNum,null)); + PendingIntent smsPendingIntent = PendingIntent.getActivity(context,43,smsIntent,0); + notifBuilder.addAction(android.R.drawable.sym_action_chat,"Text",smsPendingIntent); } + } protected void setContent(AnalyzedDownload dl){ @@ -375,7 +411,7 @@ protected void setIcon(AnalyzedDownload dl){ @Override public void start() { super.start(); - init(); +// init(); } @Override @@ -398,12 +434,64 @@ public void onReceive(Context context, Intent intent) { isSilenced = true; timeSilenced = new Date(); } - if (lastDownload!=null) - doProcess(lastDownload); + if (analyzedDownload!=null) + mNotifyMgr.notify(deviceID, buildNotification(analyzedDownload)); }else{ Log.d(TAG,deviceIDStr+": Ignored a request to snooze alarm on "+intent.getExtras().get("device")); } } } } + + private Bitmap getThumbnailByPhoneDataUri(Uri phoneDataUri){ + String id=phoneDataUri.getLastPathSegment(); +// Cursor cursor = getContentResolver().query(ContactsContract.Data.CONTENT_URI,null, ContactsContract.Data._ID+" = ? ",new String[]{id},null); +// int rawContactIdx=cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID); +// String rawContactId=null; +// if (cursor.moveToFirst()){ +// rawContactId=cursor.getString(rawContactIdx); +// } +// cursor.close(); + Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,null, ContactsContract.Data._ID+" = ? ",new String[]{id},null); + int thumbnailUriIdx=cursor.getColumnIndex(ContactsContract.Data.PHOTO_ID); + String thumbnailId=null; + if (cursor.moveToFirst()){ + thumbnailId=cursor.getString(thumbnailUriIdx); + } + cursor.close(); + Uri uri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, Long.valueOf(thumbnailId)); + cursor = context.getContentResolver().query(uri, new String[] {ContactsContract.CommonDataKinds.Photo.PHOTO},null,null,null); + Bitmap thumbnail=null; + if (cursor.moveToFirst()){ + final byte[] thumbnailBytes = cursor.getBlob(0); + if (thumbnailBytes!=null){ + thumbnail= BitmapFactory.decodeByteArray(thumbnailBytes,0,thumbnailBytes.length); + } + } +// InputStream photoDataStream = ContactsContract.Contacts.openContactPhotoInputStream(getContentResolver(),uri); +// Bitmap photo=BitmapFactory.decodeStream(photoDataStream); + return thumbnail; + } + + + public InputStream openDisplayPhoto(Uri phoneDataUri) { + String id=phoneDataUri.getLastPathSegment(); + Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,null, ContactsContract.Data._ID+" = ? ",new String[]{id},null); + int thumbnailUriIdx=cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID); + String contactId=null; + if (cursor.moveToFirst()){ + contactId=cursor.getString(thumbnailUriIdx); + } + cursor.close(); + Log.d(TAG,"ContactId=>"+contactId); + Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.valueOf(contactId)); + Uri displayPhotoUri = Uri.withAppendedPath(contactUri, ContactsContract.Contacts.Photo.DISPLAY_PHOTO); + try { + AssetFileDescriptor fd = + context.getContentResolver().openAssetFileDescriptor(displayPhotoUri, "r"); + return fd.createInputStream(); + } catch (IOException e) { + return null; + } + } } \ No newline at end of file diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/Constants.java b/mobile/src/main/java/com/ktind/cgm/bgscout/Constants.java index 1c17506..5ba212d 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/Constants.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/Constants.java @@ -35,4 +35,9 @@ public class Constants { public final static String START_DOWNLOAD_SVC="com.ktind.cgm.STARTSERVICE"; public final static String SNOOZE_INTENT="com.ktind.cgm.SNOOZE_ALARM"; public final static String DEVICE_POLL="com.ktind.cgm.DEVICE_POLL"; + public final static String UI_UPDATE="com.ktind.cgm.UI_READING_UPDATE"; + // UPDATE the DATABASE VERSION and FIX the update routine if this value is modified (increased)! + public final static String[] DEVICES={"device_1","device_2","device_3","device_4"}; + public final static int CONTACTREQUESTCODE=600; + public final static String CONTACTDATAURISUFFIX="_contact_data_uri"; } diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/DeviceActivity.java b/mobile/src/main/java/com/ktind/cgm/bgscout/DeviceActivity.java index a105af9..497050b 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/DeviceActivity.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/DeviceActivity.java @@ -1,15 +1,27 @@ package com.ktind.cgm.bgscout; +import java.sql.SQLException; import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; import java.util.Locale; +import android.annotation.TargetApi; import android.app.Activity; -import android.app.ActionBar; import android.app.Fragment; import android.app.FragmentManager; -import android.app.FragmentTransaction; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Color; +import android.os.Build; +import android.os.IBinder; import android.preference.PreferenceManager; import android.support.v13.app.FragmentPagerAdapter; import android.os.Bundle; @@ -17,7 +29,6 @@ import android.support.v4.view.ViewPager; import android.support.v4.widget.DrawerLayout; import android.util.Log; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -25,8 +36,14 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; + +import com.ktind.cgm.bgscout.model.Battery; +import com.ktind.cgm.bgscout.model.DownloadDataSource; +import com.ktind.cgm.bgscout.model.EGV; public class DeviceActivity extends Activity { @@ -36,6 +53,12 @@ public class DeviceActivity extends Activity { private String[] mDrawerMenuItems; private ActionBarDrawerToggle mDrawerToggle; private int numItemsInMenu; + private boolean mBounded=false; + private DeviceDownloadService mServer; + private UIReceiver uiReceiver; + // FIXME might need to make this hashmap otherwise we won't be able retrieve the lastdown for the different devices - it would only be a single lastDownload + static private DownloadObject ld; + static private HashMap ui=new HashMap(); /** @@ -57,23 +80,12 @@ public class DeviceActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_device); - Log.d("Main", "Created"); - - - - // Create the adapter that will return a fragment for each of the three - // primary sections of the activity. - mSectionsPagerAdapter = new SectionsPagerAdapter(getFragmentManager()); - - // Set up the ViewPager with the sections adapter. - mViewPager = (ViewPager) findViewById(R.id.pager); - mViewPager.setAdapter(mSectionsPagerAdapter); mDrawerLayout= (DrawerLayout) findViewById(R.id.drawer_layout2); mDrawerList=(ListView) findViewById(R.id.left_drawer2); SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); - String[] devices={"device_1","device_2","device_3","device_4"}; +// String[] devices={"device_1","device_2","device_3","device_4"}; ArrayList mDrawerMenuItemsArrList=new ArrayList(); - for (String device:devices) { + for (String device:Constants.DEVICES) { if (sharedPref.getBoolean(device+"_enable",false)){ Log.d(TAG,device+" is enabled"); mDrawerMenuItemsArrList.add(sharedPref.getString(device+"_name",device)); @@ -83,14 +95,16 @@ protected void onCreate(Bundle savedInstanceState) { mDrawerMenuItemsArrList.add("Stop"); mDrawerMenuItemsArrList.add("Dump stats to log"); mDrawerMenuItemsArrList.add("Settings"); + mDrawerMenuItemsArrList.add("Dump EGV"); numItemsInMenu=mDrawerMenuItemsArrList.size(); mDrawerMenuItems=mDrawerMenuItemsArrList.toArray(new String[mDrawerMenuItemsArrList.size()]); -// mDrawerMenuItems=getResources().getStringArray(R.array.devices); -// ActionBar actionBar = getActionBar(); -// actionBar.hide(); mDrawerList.setAdapter(new ArrayAdapter(this,R.layout.drawer_list_item,mDrawerMenuItems)); mDrawerList.setOnItemClickListener(new DrawerItemClickListener()); + + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); + mDrawerToggle = new ActionBarDrawerToggle( this, /* host Activity */ mDrawerLayout, /* DrawerLayout object */ @@ -101,14 +115,42 @@ protected void onCreate(Bundle savedInstanceState) { public void onDrawerClosed(View view) { super.onDrawerClosed(view); + invalidateOptionsMenu(); } public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); + invalidateOptionsMenu(); } }; mDrawerLayout.setDrawerListener(mDrawerToggle); + mSectionsPagerAdapter = new SectionsPagerAdapter(getFragmentManager()); + + + // Set up the ViewPager with the sections adapter. + mViewPager = (ViewPager) findViewById(R.id.pager); + mViewPager.setAdapter(mSectionsPagerAdapter); + mViewPager.setPageTransformer(true, new ZoomOutPageTransformer()); + + uiReceiver=new UIReceiver(); + IntentFilter intentFilter=new IntentFilter(Constants.UI_UPDATE); + getBaseContext().registerReceiver(uiReceiver,intentFilter); + if (savedInstanceState!=null){ + ld=savedInstanceState.getParcelable("lastDownload"); + if (ld==null){ + Log.d(TAG,"Its null..."); + } else { + if (ui.containsKey(ld.getDeviceID())){ + ui.get(ld.getDeviceID()).update(ld); + } else { + Log.w("XXX","UI does not contain "+ld.getDeviceID()); + } + } +// UIDeviceList.get(0).update((DownloadObject) savedInstanceState.getSerializable("lastDownload")); + } else { + Log.w("XXX", "Saved instance does not contain anything"); + } } @@ -116,30 +158,97 @@ private class DrawerItemClickListener implements ListView.OnItemClickListener { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { selectItem(position); + mDrawerLayout.closeDrawers(); } } + public void buildUI(){ + // Build the UI objects + Log.d("XXX","Number of fragments here: "+PlaceholderFragment.fragments.size()); + for (String key:PlaceholderFragment.fragments.keySet()) { + TextView egvValue = (TextView) PlaceholderFragment.fragments.get(key).getView().findViewById(R.id.reading_text); + TextView uploaderBatteryLabel = (TextView) PlaceholderFragment.fragments.get(key).getView().findViewById(R.id.uploader_battery_label); + TextView deviceBatteryLabel = (TextView) PlaceholderFragment.fragments.get(key).getView().findViewById(R.id.device_battery_label); + TextView name = (TextView) PlaceholderFragment.fragments.get(key).getView().findViewById(R.id.app_name); + ImageView main = (ImageView) PlaceholderFragment.fragments.get(key).getView().findViewById(R.id.main_display); + ImageView direction = (ImageView) PlaceholderFragment.fragments.get(key).getView().findViewById(R.id.direction_image); + ImageView uploaderBat = (ImageView) PlaceholderFragment.fragments.get(key).getView().findViewById(R.id.uploader_battery_indicator); + ImageView deviceBat = (ImageView) PlaceholderFragment.fragments.get(key).getView().findViewById(R.id.device_battery_indicator); + UIDevice uid=new UIDevice(main,direction,egvValue,name,uploaderBat,uploaderBatteryLabel,deviceBat,deviceBatteryLabel); + Log.d("XXX", "Adding "+key+" to ui elements map"); + ui.put(key, uid); + } + + } + // FIXME breaks the rules - order here is must match the order the items were put into the string array(list) private void selectItem(int position){ Log.d(TAG,"Position: "+position+ " number of items in menu: "+numItemsInMenu); - if (position==(numItemsInMenu-1)) { + if (position==(numItemsInMenu-2)) { Log.d(TAG,"Starting settings"); Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); } - if (position==(numItemsInMenu-2)) { + if (position==(numItemsInMenu-3)) { Log.d(TAG,"Dumping stats"); BGScout.statsMgr.logStats(); } - if (position==(numItemsInMenu-3)) { + if (position==(numItemsInMenu-4)) { Log.d(TAG,"Stopping service"); + Intent intent=new Intent(Constants.STOP_DOWNLOAD_SVC); + getBaseContext().sendBroadcast(intent); +// Intent mIntent = new Intent(MainActivity.this, DeviceDownloadService.class); +// bindSvc(); +// stopService(mIntent); } - if (position==(numItemsInMenu-4)) { + if (position==(numItemsInMenu-5)) { Log.d(TAG,"Starting service"); + Intent mIntent = new Intent(DeviceActivity.this, DeviceDownloadService.class); + startService(mIntent); + bindSvc(); + buildUI(); + } + if (position==(numItemsInMenu-1)) { + DownloadDataSource downloadDataSource=new DownloadDataSource(this); + try { + downloadDataSource.open(); + for (EGV egv:downloadDataSource.getEGVHistory("device_1")) + Log.d(TAG,"Date: "+new Date(egv.getEpoch())+" EGV: "+egv.getEgv()+" Trend: "+Trend.values()[egv.getTrend()].toString()+" Unit: "+GlucoseUnit.values()[egv.getUnit()]); + for (Battery battery:downloadDataSource.getBatteryHistory("device_1")) + Log.d(TAG,"Date: "+new Date(battery.getEpoch())+" Battery Level:"+battery.getBatterylevel()+" Device: "+downloadDataSource.getDevice(battery.getDeviceid()).getName()+" Role: "+downloadDataSource.getRole(battery.getRoleid()).getRole()); + downloadDataSource.close(); + } catch (SQLException e) { + Log.e(TAG,"Caught exception: ",e); + } + + Intent mIntent = new Intent(DeviceActivity.this, DeviceDownloadService.class); + startService(mIntent); + bindSvc(); } + } + public void bindSvc(){ + Intent mIntent = new Intent(DeviceActivity.this, DeviceDownloadService.class); + bindService(mIntent, mConnection, BIND_AUTO_CREATE); } + ServiceConnection mConnection = new ServiceConnection() { + + public void onServiceDisconnected(ComponentName name) { + Toast.makeText(DeviceActivity.this, "Service is disconnected", Toast.LENGTH_LONG).show(); + mBounded = false; + mServer = null; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + Toast.makeText(DeviceActivity.this, "Service is connected", Toast.LENGTH_LONG).show(); + mBounded = true; + DeviceDownloadService.LocalBinder mLocalBinder = (DeviceDownloadService.LocalBinder)service; + mServer = mLocalBinder.getServerInstance(); + } + }; + + @Override public boolean onCreateOptionsMenu(Menu menu) { @@ -153,8 +262,14 @@ public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. + if (mDrawerToggle.onOptionsItemSelected(item)) { + return true; + } + int id = item.getItemId(); if (id == R.id.action_settings) { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); return true; } return super.onOptionsItemSelected(item); @@ -176,14 +291,17 @@ public SectionsPagerAdapter(FragmentManager fm) { public Fragment getItem(int position) { // getItem is called to instantiate the fragment for the given page. // Return a PlaceholderFragment (defined as a static inner class below). - Log.d("Main","getItem called"); - return PlaceholderFragment.newInstance(position + 1); + int devNum=position+1; + String devID="device_"+devNum; + Log.d("Main","adding "+devID); + return PlaceholderFragment.newInstance(position + 1,devID); } @Override public int getCount() { // Show 3 total pages. - Log.d("main","getCount called"); +// Log.d("main","getCount called"); +// return numItemsInMenu-5; return 3; } @@ -211,28 +329,357 @@ public static class PlaceholderFragment extends Fragment { * fragment. */ private static final String ARG_SECTION_NUMBER = "section_number"; +// static public ArrayList fragments= new ArrayList(); + static public HashMap fragments=new HashMap(); /** * Returns a new instance of this fragment for the given section * number. */ - public static PlaceholderFragment newInstance(int sectionNumber) { + public static PlaceholderFragment newInstance(int sectionNumber,String deviceID) { PlaceholderFragment fragment = new PlaceholderFragment(); Bundle args = new Bundle(); args.putInt(ARG_SECTION_NUMBER, sectionNumber); fragment.setArguments(args); + + + fragments.put(deviceID,fragment); +// fragments.add(fragment); return fragment; } public PlaceholderFragment() { } + public void updateView(DownloadObject dl){ + TextView egvValue=(TextView) this.getView().findViewById(R.id.reading_text); + TextView uploaderBattery=(TextView) this.getView().findViewById(R.id.uploader_battery_label); + TextView deviceBattery=(TextView) this.getView().findViewById(R.id.device_battery_label); + TextView name=(TextView) this.getView().findViewById(R.id.app_name); + + try { + egvValue.setText(String.valueOf(dl.getLastReading())); + uploaderBattery.setText(String.valueOf(dl.getUploaderBattery())); + deviceBattery.setText(String.valueOf(dl.getDeviceBattery())); + name.setText(dl.getDeviceName()); + } catch (NoDataException e) { + e.printStackTrace(); + } + } + + @Override + public void onDestroyView() { + Log.d("XXX","onDestoryView called"); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + Log.d("XXX","onDestory called"); + super.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + Log.d("XXX","saving instance state"); + outState.putParcelable("lastDownload", ld); + super.onSaveInstanceState(outState); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public void onViewStateRestored(Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Log.d("XXX","onCreateView called"); + if (savedInstanceState!=null){ + ld=savedInstanceState.getParcelable("lastDownload"); + if (ld==null){ + Log.d(TAG,"Its null..."); + } else { + if (ui.containsKey(ld.getDeviceID())){ + ui.get(ld.getDeviceID()).update(ld); + } else { + Log.w("XXX","UI does not contain "+ld.getDeviceID()); + } + } +// UIDeviceList.get(0).update((DownloadObject) savedInstanceState.getSerializable("lastDownload")); + } else { + Log.w("XXX", "Saved instance does not contain anything"); + } View rootView = inflater.inflate(R.layout.fragment_device, container, false); return rootView; } } + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + // Sync the toggle state after onRestoreInstanceState has occurred. + mDrawerToggle.syncState(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDrawerToggle.onConfigurationChanged(newConfig); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // If the nav drawer is open, hide action items related to the content view + boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList); +// menu.findItem(R.id.action_websearch).setVisible(!drawerOpen); + return super.onPrepareOptionsMenu(menu); + } + + public class DepthPageTransformer implements ViewPager.PageTransformer { + private static final float MIN_SCALE = 0.75f; + + public void transformPage(View view, float position) { + int pageWidth = view.getWidth(); + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + view.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + // Use the default slide transition when moving to the left page + view.setAlpha(1); + view.setTranslationX(0); + view.setScaleX(1); + view.setScaleY(1); + + } else if (position <= 1) { // (0,1] + // Fade the page out. + view.setAlpha(1 - position); + + // Counteract the default slide transition + view.setTranslationX(pageWidth * -position); + + // Scale the page down (between MIN_SCALE and 1) + float scaleFactor = MIN_SCALE + + (1 - MIN_SCALE) * (1 - Math.abs(position)); + view.setScaleX(scaleFactor); + view.setScaleY(scaleFactor); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + view.setAlpha(0); + } + } + } + + public class ZoomOutPageTransformer implements ViewPager.PageTransformer { + private static final float MIN_SCALE = 0.85f; + private static final float MIN_ALPHA = 0.5f; + + public void transformPage(View view, float position) { + int pageWidth = view.getWidth(); + int pageHeight = view.getHeight(); + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + view.setAlpha(0); + + } else if (position <= 1) { // [-1,1] + // Modify the default slide transition to shrink the page as well + float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position)); + float vertMargin = pageHeight * (1 - scaleFactor) / 2; + float horzMargin = pageWidth * (1 - scaleFactor) / 2; + if (position < 0) { + view.setTranslationX(horzMargin - vertMargin / 2); + } else { + view.setTranslationX(-horzMargin + vertMargin / 2); + } + + // Scale the page down (between MIN_SCALE and 1) + view.setScaleX(scaleFactor); + view.setScaleY(scaleFactor); + + // Fade the page relative to its size. + view.setAlpha(MIN_ALPHA + + (scaleFactor - MIN_SCALE) / + (1 - MIN_SCALE) * (1 - MIN_ALPHA)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + view.setAlpha(0); + } + } + } + + protected class UIDevice{ + protected TextView name; + protected ImageView main_display; + protected ImageView direction; + protected TextView bg; + protected DownloadObject lastDownload; + protected ImageView uploaderBattery; + protected TextView uploaderBatteryLabel; + protected ImageView deviceBattery; + protected TextView deviceBatteryLabel; + int mainBGColor; + int currentBGColor; + + public UIDevice(ImageView main, ImageView dir, TextView reading, TextView n, ImageView ubat, TextView ubatl, ImageView dbat, TextView dbatl){ + setMain_display(main); +// main_display.setBackgroundColor(mainBGColor); + setDirection(dir); + setBg(reading); + setName(n); + setUploaderBattery(ubat); + setDeviceBattery(dbat); + setUploaderBatteryLabel(ubatl); + setDeviceBatteryLabel(dbatl); + deviceBattery.setImageResource(R.drawable.battery); + uploaderBattery.setImageResource(R.drawable.battery); + } + + public TextView getUploaderBatteryLabel() { + return uploaderBatteryLabel; + } + + public void setUploaderBatteryLabel(TextView uploaderBatteryLabel) { + this.uploaderBatteryLabel = uploaderBatteryLabel; + } + + public TextView getDeviceBatteryLabel() { + return deviceBatteryLabel; + } + + public void setDeviceBatteryLabel(TextView deviceBatteryLabel) { + this.deviceBatteryLabel = deviceBatteryLabel; + } + + public ImageView getUploaderBattery() { + return uploaderBattery; + } + + public void setUploaderBattery(ImageView uploaderBattery) { + this.uploaderBattery = uploaderBattery; + } + + public ImageView getDeviceBattery() { + return deviceBattery; + } + + public void setDeviceBattery(ImageView deviceBattery) { + this.deviceBattery = deviceBattery; + } + + public void update(DownloadObject dl){ + lastDownload=dl; + name.setText(dl.getDeviceName()); + direction.setImageResource(R.drawable.trendarrows); + + try { + direction.setImageLevel(dl.getLastTrend().getVal()); + int r=dl.getLastReading(); + SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + Resources res=getBaseContext().getResources(); + int lowThreshold=Integer.valueOf(sharedPref.getString(dl.getDeviceID() + "_low_threshold", String.valueOf(res.getInteger(R.integer.pref_default_device_low)))); + int highThreshold=Integer.valueOf(sharedPref.getString(dl.getDeviceID() + "_high_threshold", String.valueOf(res.getInteger(R.integer.pref_default_device_high)))); + +// int newColor=Color.WHITE; + if (r>highThreshold) { + currentBGColor= Color.rgb(255, 199, 0); + }else if (r(); SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); - String[] device_list={"device_1","device_2","device_3","device_4"}; +// String[] device_list={"device_1","device_2","device_3","device_4"}; int devCount=1; - for (String dev:device_list) { + for (String dev:Constants.DEVICES) { boolean enabled = sharedPref.getBoolean(dev+"_enable", false); if (enabled){ String name = sharedPref.getString(dev+"_name",""); diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/DexcomG4/G4EGVRecord.java b/mobile/src/main/java/com/ktind/cgm/bgscout/DexcomG4/G4EGVRecord.java index edbc543..7113e34 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/DexcomG4/G4EGVRecord.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/DexcomG4/G4EGVRecord.java @@ -118,6 +118,7 @@ public ArrayList parse(G4DBPage page) { long dtime=(long) BitTools.byteArraytoInt(dispTimeArray)*1000; Calendar mCalendar = new GregorianCalendar(); TimeZone mTimeZone = mCalendar.getTimeZone(); + //FIXME - Is this logic right? long displayTimeLong=G4Constants.RECEIVERBASEDATE+dtime; if (mTimeZone.inDaylightTime(new Date())){ displayTimeLong-=3600000L; diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/MainActivity.java b/mobile/src/main/java/com/ktind/cgm/bgscout/MainActivity.java index 01cc453..68e84f0 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/MainActivity.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/MainActivity.java @@ -5,17 +5,23 @@ import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.res.Configuration; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Color; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.preference.PreferenceManager; +import android.provider.ContactsContract; import android.support.v4.app.ActionBarDrawerToggle; import android.support.v4.widget.DrawerLayout; import android.util.Log; @@ -24,17 +30,19 @@ import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ListView; +import android.widget.QuickContactBadge; import android.widget.TextView; import android.widget.Toast; import com.ktind.cgm.bgscout.model.Battery; -import com.ktind.cgm.bgscout.model.Device; import com.ktind.cgm.bgscout.model.DownloadDataSource; import com.ktind.cgm.bgscout.model.EGV; -import com.ktind.cgm.bgscout.model.Role; +import java.io.InputStream; import java.sql.SQLException; import java.util.ArrayList; import java.util.Date; @@ -91,12 +99,15 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); mDrawerLayout= (DrawerLayout) findViewById(R.id.drawer_layout); mDrawerList=(ListView) findViewById(R.id.left_drawer); - SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); - String[] devices={"device_1","device_2","device_3","device_4"}; +// Button callButton = (Button) findViewById(R.id.call_button); + + + final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); +// String[] devices={"device_1","device_2","device_3","device_4"}; ArrayList mDrawerMenuItemsArrList=new ArrayList(); - for (String device:devices) { + for (String device:Constants.DEVICES) { if (sharedPref.getBoolean(device+"_enable",false)){ - Log.d(TAG,device+" is enabled"); +// Log.d(TAG,device+" is enabled"); mDrawerMenuItemsArrList.add(sharedPref.getString(device+"_name",device)); } } @@ -105,24 +116,20 @@ protected void onCreate(Bundle savedInstanceState) { mDrawerMenuItemsArrList.add("Dump stats to log"); mDrawerMenuItemsArrList.add("Settings"); mDrawerMenuItemsArrList.add("Dump EGV"); + mDrawerMenuItemsArrList.add("Set contact"); numItemsInMenu=mDrawerMenuItemsArrList.size(); mDrawerMenuItems=mDrawerMenuItemsArrList.toArray(new String[mDrawerMenuItemsArrList.size()]); -// mDrawerMenuItems=getResources().getStringArray(R.array.devices); -// ActionBar actionBar = getActionBar(); -// actionBar.hide(); mDrawerList.setAdapter(new ArrayAdapter(this,R.layout.drawer_list_item,mDrawerMenuItems)); mDrawerList.setOnItemClickListener(new DrawerItemClickListener()); // SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); - UIDevice uiDevice = new UIDevice((ImageView) findViewById(R.id.main_display), (ImageView) findViewById(R.id.direction_image), (TextView) findViewById(R.id.reading_text), (TextView) findViewById(R.id.app_name), (ImageView) findViewById(R.id.uploader_battery_indicator), (TextView) findViewById(R.id.uploader_battery_label), (ImageView) findViewById(R.id.deviceBattery), (TextView) findViewById(R.id.device_battery_label)); + UIDevice uiDevice = new UIDevice((ImageView) findViewById(R.id.main_display), (ImageView) findViewById(R.id.direction_image), (TextView) findViewById(R.id.reading_text), (TextView) findViewById(R.id.app_name), (ImageView) findViewById(R.id.uploader_battery_indicator), (TextView) findViewById(R.id.uploader_battery_label), (ImageView) findViewById(R.id.device_battery_indicator), (TextView) findViewById(R.id.device_battery_label)); UIDeviceList.add(uiDevice); alarmReceiver = new AlarmReceiver(); - registerReceiver(alarmReceiver, new IntentFilter("com.ktind.cgm.UI_READING_UPDATE")); + registerReceiver(alarmReceiver, new IntentFilter(Constants.UI_UPDATE)); if (savedInstanceState!=null){ ld=savedInstanceState.getParcelable("lastDownload"); - if (ld==null){ - Log.d(TAG,"Its null..."); - } else { + if (ld!=null){ for (UIDevice uid:UIDeviceList){ uid.update(ld); } @@ -149,33 +156,80 @@ public void onDrawerOpened(View drawerView) { } }; mDrawerLayout.setDrawerListener(mDrawerToggle); - if (isServiceRunning(DeviceDownloadService.class)) { bindSvc(); } + + ImageButton contact = (ImageButton) findViewById(R.id.imageButton); + contact.setImageBitmap(getThumbnailByPhoneDataUri(Uri.parse(sharedPref.getString("device_1_contact_data_uri", "")))); + contact.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String phoneNum=getPhone(sharedPref.getString("device_1_contact_data_uri", "")); + if (phoneNum!=null && phoneNum!="") { + Intent callIntent = new Intent(Intent.ACTION_CALL); + callIntent.setData(Uri.parse("tel:" + phoneNum)); + startActivity(callIntent); + } + } + }); + +// contact.setOnClickListener(new View.OnClickListener() { +// @Override +// public void onClick(View v) { +//// Intent contactPickerIntent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); +// Intent contactPickerIntent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI); +// // hardcoding this to device_1 for now +// startActivityForResult(contactPickerIntent,Constants.CONTACTREQUESTCODE+1); +// } +// }); + } private class DrawerItemClickListener implements ListView.OnItemClickListener { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { selectItem(position); + mDrawerLayout.closeDrawers(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + Log.d(TAG,"Result code="+resultCode); + Log.d(TAG,"Request code="+requestCode); + if (resultCode!=-1) + return; + // Multiple devices will need to rely on different code offsets to distinguish itself. + // Start at 601 for device_1, device_2 is 602, etc. + if (requestCode>Constants.CONTACTREQUESTCODE && requestCode downloadObjects=new ArrayList(); + + public PebbleMonitor(String n, int devID, Context context) { + super(n, devID, context, "pebble_monitor"); + init(); + } + + protected void init(){ + PebbleKit.registerReceivedDataHandler(context, new PebbleKit.PebbleDataReceiver(PEBBLEAPP_UUID) { + @Override + public void receiveData(final Context mContext, final int transactionId, final PebbleDictionary data) { + Log.i(TAG, "Received value=" + data.getString(0) + " for key: 0"); + Log.d(TAG," Data size="+data.size()); + PebbleKit.sendAckToPebble(mContext, transactionId); + if (data.getString(0) !=null && data.getString(0).equals("lastdownload")) { + if (downloadObjects.size() > 0){ + PebbleDictionary out=buildMsg(downloadObjects.get(downloadObjects.size()-1)); + PebbleKit.sendDataToPebble(context,PEBBLEAPP_UUID,out); + } + + } + + } + }); + } + + @Override + protected void doProcess(DownloadObject d) { + downloadObjects.add(d); + if (downloadObjects.size()>2) + downloadObjects.remove(0); + + boolean connected = PebbleKit.isWatchConnected(context); + Log.i(TAG, "Pebble is " + (connected ? "connected" : "not connected")); + if (connected){ + PebbleDictionary data=buildMsg(d); + Log.d(TAG,"Trying to send message to pebble.. Here goes nothing"); + PebbleKit.sendDataToPebble(context,PEBBLEAPP_UUID,data); + } + } + + protected PebbleDictionary buildMsg(DownloadObject dl){ + String delta=""; + if (downloadObjects.size()>1) { + try { + int deltaInt=downloadObjects.get(1).getLastReading() - downloadObjects.get(0).getLastReading(); + if (deltaInt>0) + delta="+"; + delta += String.valueOf(deltaInt); + } catch (NoDataException e) { + e.printStackTrace(); + } + } + PebbleDictionary data=new PebbleDictionary(); + Log.d(TAG,"Building the dictionary"); + byte alert=0x00; + try { + Log.d(TAG, "Trend arrow: " + dl.getLastTrend().getNsString()); + data.addString(ICON_KEY, dl.getLastTrend().getNsString()); + data.addString(BG_KEY, String.valueOf(dl.getLastReading())); +// data.addString(READTIME, new SimpleDateFormat("HH:mm:ss MM/dd").format(dl.getLastRecordReadingDate())); + Calendar cal = Calendar.getInstance(); + TimeZone tz = cal.getTimeZone(); + int rawOffset=tz.getRawOffset()/1000; + if (tz.inDaylightTime(new Date())) + rawOffset+=3600; // 1 hour for daylight time if it is observed + long readTimeUTC=dl.getLastRecordReadingDate().getTime()/1000; + long readTimeLocal=readTimeUTC+rawOffset; + + data.addString(READTIME, String.valueOf(readTimeLocal)); + if (dl.getLastReading() > 180) + alert = 0x02; + if (dl.getLastReading() < 70) + alert = 0x01; + data.addUint8(ALERT, alert); +// String tm = String.valueOf(((new Date().getTime() - dl.getLastRecordReadingDate().getTime()) / 1000) / 60); +// rawOffset+=720000; + long epochUTC=new Date().getTime()/1000; + long epochlocalSeconds=(epochUTC+rawOffset); +// String tm = String.valueOf(dl.getLastRecordReadingDate().getTime()/1000); + String tm = String.valueOf(epochlocalSeconds); + Log.d(TAG, "tm=" + tm); + data.addString(TIME, tm); + Log.d(TAG, "Delta=" + delta); + data.addString(DELTA, delta); + data.addString(BATT, String.valueOf((int) dl.getUploaderBattery())); + data.addString(NAME, dl.getDeviceName()); + } catch (NoDataException e) { + e.printStackTrace(); + } + return data; + } +} diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/RemoteMQTTDevice.java b/mobile/src/main/java/com/ktind/cgm/bgscout/RemoteMQTTDevice.java index 79b78e8..bc8c240 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/RemoteMQTTDevice.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/RemoteMQTTDevice.java @@ -3,6 +3,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Handler; +import android.os.Looper; import android.preference.PreferenceManager; import android.util.Log; @@ -70,17 +71,37 @@ public void start() { @Override public void connect() { Log.d(TAG,"Connect started"); + if (Looper.getMainLooper().getThread()==Thread.currentThread()) + Log.w(TAG,"On main thread!"); + else + Log.i(TAG,"On background thread"); SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(appContext); - String url=sharedPref.getString(deviceIDStr+"_mqtt_endpoint",""); + final String url=sharedPref.getString(deviceIDStr+"_mqtt_endpoint",""); String usr=sharedPref.getString(deviceIDStr+"_mqtt_user",""); String pw=sharedPref.getString(deviceIDStr+"_mqtt_pass",""); mqttMgr=new MQTTMgr(appContext,usr,pw,getDeviceIDStr()); - mqttMgr.initConnect(url); - mqttMgr.registerObserver(this); - Log.d(TAG, "Subscribe start"); +// mqttMgr.initConnect(url); +// Log.d(TAG, "Subscribe start"); +// mqttMgr.subscribe("/entries/sgv"); +// Log.d(TAG,"Connect ended"); + + new Thread(new Runnable() { + @Override + public void run() { + mqttMgr.initConnect(url); + Log.d(TAG, "Subscribe start"); // mqttMgr.subscribe("/entries/sgv", "/uploader"); - mqttMgr.subscribe("/entries/sgv"); - Log.d(TAG,"Connect ended"); + mqttMgr.subscribe("/entries/sgv"); + Log.d(TAG,"Connect ended"); + } + }).start(); +// thread.start(); +// try { +// thread.join(5000); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } + mqttMgr.registerObserver(this); } @Override diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/RemoteMongoDevice.java b/mobile/src/main/java/com/ktind/cgm/bgscout/RemoteMongoDevice.java index 3aaf042..e303106 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/RemoteMongoDevice.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/RemoteMongoDevice.java @@ -58,8 +58,8 @@ public RemoteMongoDevice(String n,int deviceID,Context appContext,Handler mH){ // Give it some time to settle. If not it'll try again in 45 seconds. this.setPollInterval(304000); SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(appContext); - String[] device_list={"device_1","device_2","device_3","device_4"}; - for (String dev:device_list) { +// String[] device_list={"device_1","device_2","device_3","device_4"}; + for (String dev:Constants.DEVICES) { if (sharedPref.getString(dev+"_name","").equals(getName())){ mongoURI=sharedPref.getString(dev+"_mongo_uri",""); collectionName=sharedPref.getString(dev+"_mongo_col",""); diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/SQLiteMonitor.java b/mobile/src/main/java/com/ktind/cgm/bgscout/SQLiteMonitor.java index 169226e..96fac8d 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/SQLiteMonitor.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/SQLiteMonitor.java @@ -59,12 +59,13 @@ protected void doProcess(DownloadObject d) { downloadDataSource.createBattery(d.getDeviceBattery(),deviceIDStr,"cgm",d.getLastRecordReadingDate().getTime()); downloadDataSource.createBattery(d.getUploaderBattery(),deviceIDStr,"uploader",d.getLastRecordReadingDate().getTime()); // TODO add battery for this device? - downloadDataSource.close(); savelastSuccessDate(d.getLastRecordReadingDate().getTime()); } catch (SQLException e) { Log.e(TAG,"Caught SQLException: ",e); } catch (NoDataException e) { Log.e(TAG, "No data in download. Unable to save last reading date"); + } finally { + downloadDataSource.close(); } } } diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/SettingsActivity.java b/mobile/src/main/java/com/ktind/cgm/bgscout/SettingsActivity.java index c281cfb..60e74a1 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/SettingsActivity.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/SettingsActivity.java @@ -15,6 +15,7 @@ import android.preference.PreferenceManager; import android.preference.RingtonePreference; import android.text.TextUtils; +import android.util.Log; import android.view.MenuItem; import android.support.v4.app.NavUtils; @@ -30,34 +31,9 @@ * Android Design: Settings for design guidelines and the Settings * API Guide for more information on developing a Settings UI. - * Copyright (C) 2014 Kevin Lee - - Copyright (c) 2014, Kevin Lee (klee24@gmail.com) - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ public class SettingsActivity extends PreferenceActivity { + private static final String TAG = SettingsActivity.class.getSimpleName(); /** * Determines whether to always show the simplified settings UI, where * settings are presented in a single list. When false, settings are shown @@ -125,45 +101,74 @@ private void setupSimplePreferencesScreen() { // Add 'general' preferences. addPreferencesFromResource(R.xml.pref_general); -// // Add 'notifications' preferences, and a corresponding header. -// PreferenceCategory fakeHeader = new PreferenceCategory(this); -// fakeHeader.setTitle(R.string.pref_header_notifications); -// getPreferenceScreen().addPreference(fakeHeader); -// addPreferencesFromResource(R.xml.pref_notification); -// -// // Add 'data and sync' preferences, and a corresponding header. -// fakeHeader = new PreferenceCategory(this); -// fakeHeader.setTitle(R.string.pref_header_data_sync); -// getPreferenceScreen().addPreference(fakeHeader); -// addPreferencesFromResource(R.xml.pref_data_sync); - // Bind the summaries of EditText/List/Dialog/Ringtone preferences to // their values. When their values change, their summaries are updated // to reflect the new value, per the Android Design guidelines. - bindPreferenceSummaryToValue(findPreference("device_1_name")); - bindPreferenceSummaryToValue(findPreference("device_2_name")); - bindPreferenceSummaryToValue(findPreference("device_3_name")); - bindPreferenceSummaryToValue(findPreference("device_4_name")); - bindPreferenceSummaryToValue(findPreference("device_1_high_threshold")); - bindPreferenceSummaryToValue(findPreference("device_1_low_threshold")); - bindPreferenceSummaryToValue(findPreference("device_2_high_threshold")); - bindPreferenceSummaryToValue(findPreference("device_2_low_threshold")); - bindPreferenceSummaryToValue(findPreference("device_3_high_threshold")); - bindPreferenceSummaryToValue(findPreference("device_3_low_threshold")); - bindPreferenceSummaryToValue(findPreference("device_4_high_threshold")); - bindPreferenceSummaryToValue(findPreference("device_4_low_threshold")); - bindPreferenceSummaryToValue(findPreference("device_1_high_ringtone")); - bindPreferenceSummaryToValue(findPreference("device_1_low_ringtone")); - bindPreferenceSummaryToValue(findPreference("device_2_high_ringtone")); - bindPreferenceSummaryToValue(findPreference("device_2_low_ringtone")); - bindPreferenceSummaryToValue(findPreference("device_3_high_ringtone")); - bindPreferenceSummaryToValue(findPreference("device_3_low_ringtone")); - bindPreferenceSummaryToValue(findPreference("device_4_high_ringtone")); - bindPreferenceSummaryToValue(findPreference("device_4_low_ringtone")); - bindPreferenceSummaryToValue(findPreference("device_1_mongo_uri")); - bindPreferenceSummaryToValue(findPreference("device_2_mongo_uri")); - bindPreferenceSummaryToValue(findPreference("device_3_mongo_uri")); - bindPreferenceSummaryToValue(findPreference("device_4_mongo_uri")); +// String[] devices={"device_1","device_2","device_3","device_4"}; + for (String device:Constants.DEVICES){ + Log.d(TAG, "Trying to setup "+device+"_name"); + bindPreferenceSummaryToValue(findPreference(device + "_name")); + Log.d(TAG, "Trying to setup "+device+"_high_threshold"); + bindPreferenceSummaryToValue(findPreference(device+"_high_threshold")); + Log.d(TAG, "Trying to setup "+device+"_critical_high_threshold"); + bindPreferenceSummaryToValue(findPreference(device+"_critical_high_threshold")); + Log.d(TAG, "Trying to setup "+device+"_low_threshold"); + bindPreferenceSummaryToValue(findPreference(device+"_low_threshold")); + Log.d(TAG, "Trying to setup "+device+"_critical_low_threshold"); + bindPreferenceSummaryToValue(findPreference(device+"_critical_low_threshold")); + Log.d(TAG, "Trying to setup "+device+"_high_ringtone"); + bindPreferenceSummaryToValue(findPreference(device+"_high_ringtone")); + Log.d(TAG, "Trying to setup "+device+"_critical_high_ringtone"); + bindPreferenceSummaryToValue(findPreference(device+"_critical_high_ringtone")); + Log.d(TAG, "Trying to setup "+device+"_low_ringtone"); + bindPreferenceSummaryToValue(findPreference(device+"_low_ringtone")); + Log.d(TAG, "Trying to setup "+device+"_critical_ringtone"); + bindPreferenceSummaryToValue(findPreference(device+"_critical_low_ringtone")); + Log.d(TAG, "Trying to setup "+device+"_mongo_uri"); + bindPreferenceSummaryToValue(findPreference(device+"_mongo_uri")); + Log.d(TAG, "Trying to setup "+device+"_nsapi"); + bindPreferenceSummaryToValue(findPreference(device+"_nsapi")); + Log.d(TAG, "Trying to setup "+device+"_mqtt_endpoint"); + bindPreferenceSummaryToValue(findPreference(device+"_mqtt_endpoint")); + Log.d(TAG, "Trying to setup "+device+"_mqtt_user"); + bindPreferenceSummaryToValue(findPreference(device+"_mqtt_user")); + Log.d(TAG, "Trying to setup button_"+device); + Preference buttonPref=findPreference("button_"+device); + sBindPreferenceSummaryToValueListener.onPreferenceChange(buttonPref,findPreference(device+"_name")); + } +// bindPreferenceSummaryToValue(findPreference("device_1_name")); +// bindPreferenceSummaryToValue(findPreference("device_2_name")); +// bindPreferenceSummaryToValue(findPreference("device_3_name")); +// bindPreferenceSummaryToValue(findPreference("device_4_name")); +// bindPreferenceSummaryToValue(findPreference("device_1_high_threshold")); +// bindPreferenceSummaryToValue(findPreference("device_1_low_threshold")); +// bindPreferenceSummaryToValue(findPreference("device_2_high_threshold")); +// bindPreferenceSummaryToValue(findPreference("device_2_low_threshold")); +// bindPreferenceSummaryToValue(findPreference("device_3_high_threshold")); +// bindPreferenceSummaryToValue(findPreference("device_3_low_threshold")); +// bindPreferenceSummaryToValue(findPreference("device_4_high_threshold")); +// bindPreferenceSummaryToValue(findPreference("device_4_low_threshold")); +// bindPreferenceSummaryToValue(findPreference("device_1_high_ringtone")); +// bindPreferenceSummaryToValue(findPreference("device_1_low_ringtone")); +// bindPreferenceSummaryToValue(findPreference("device_2_high_ringtone")); +// bindPreferenceSummaryToValue(findPreference("device_2_low_ringtone")); +// bindPreferenceSummaryToValue(findPreference("device_3_high_ringtone")); +// bindPreferenceSummaryToValue(findPreference("device_3_low_ringtone")); +// bindPreferenceSummaryToValue(findPreference("device_4_high_ringtone")); +// bindPreferenceSummaryToValue(findPreference("device_4_low_ringtone")); +// bindPreferenceSummaryToValue(findPreference("device_1_mongo_uri")); +// bindPreferenceSummaryToValue(findPreference("device_2_mongo_uri")); +// bindPreferenceSummaryToValue(findPreference("device_3_mongo_uri")); +// bindPreferenceSummaryToValue(findPreference("device_4_mongo_uri")); + +// preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); +// +// // Trigger the listener immediately with the preference's +// // current value. +// sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, +// PreferenceManager +// .getDefaultSharedPreferences(preference.getContext()) +// .getString(preference.getKey(), "")); } /** {@inheritDoc} */ @@ -277,70 +282,16 @@ private static void bindPreferenceSummaryToValue(Preference preference) { .getString(preference.getKey(), "")); } - /** - * This fragment shows general preferences only. It is used when the - * activity is showing a two-pane settings UI. - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static class GeneralPreferenceFragment extends PreferenceFragment { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.pref_general); - - // Bind the summaries of EditText/List/Dialog/Ringtone preferences - // to their values. When their values change, their summaries are - // updated to reflect the new value, per the Android Design - // guidelines. -// bindPreferenceSummaryToValue(findPreference("example_text")); -// bindPreferenceSummaryToValue(findPreference("example_list")); - } - } - -// @TargetApi(Build.VERSION_CODES.HONEYCOMB) -// public static class MQTTPreferenceFragement extends PreferenceFragment { -// @Override -// public void onCreate(Bundle savedInstanceState){ -// super.onCreate(savedInstanceState); -// addPreferencesFromResource(R.xml.pref_headers); -// } -// } - -// /** -// * This fragment shows notification preferences only. It is used when the -// * activity is showing a two-pane settings UI. -// */ -// @TargetApi(Build.VERSION_CODES.HONEYCOMB) -// public static class NotificationPreferenceFragment extends PreferenceFragment { -// @Override -// public void onCreate(Bundle savedInstanceState) { -// super.onCreate(savedInstanceState); -// addPreferencesFromResource(R.xml.pref_notification); -// -// // Bind the summaries of EditText/List/Dialog/Ringtone preferences -// // to their values. When their values change, their summaries are -// // updated to reflect the new value, per the Android Design -// // guidelines. -// bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone")); -// } -// } -// // /** -// * This fragment shows data and sync preferences only. It is used when the +// * This fragment shows general preferences only. It is used when the // * activity is showing a two-pane settings UI. // */ // @TargetApi(Build.VERSION_CODES.HONEYCOMB) -// public static class DataSyncPreferenceFragment extends PreferenceFragment { +// public static class GeneralPreferenceFragment extends PreferenceFragment { // @Override // public void onCreate(Bundle savedInstanceState) { // super.onCreate(savedInstanceState); -// addPreferencesFromResource(R.xml.pref_data_sync); -// -// // Bind the summaries of EditText/List/Dialog/Ringtone preferences -// // to their values. When their values change, their summaries are -// // updated to reflect the new value, per the Android Design -// // guidelines. -// bindPreferenceSummaryToValue(findPreference("sync_frequency")); +// addPreferencesFromResource(R.xml.pref_general); // } // } } diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/model/DownloadSQLiteHelper.java b/mobile/src/main/java/com/ktind/cgm/bgscout/model/DownloadSQLiteHelper.java index 0f3d2f3..e9bb648 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/model/DownloadSQLiteHelper.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/model/DownloadSQLiteHelper.java @@ -32,6 +32,8 @@ import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; +import com.ktind.cgm.bgscout.Constants; + public class DownloadSQLiteHelper extends SQLiteOpenHelper { public static final String TABLE_DEVICE="devices"; public static final String COLUMN_ID="_id"; @@ -89,14 +91,18 @@ public class DownloadSQLiteHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { db.execSQL(DEVICES_TABLE_CREATE); ContentValues values = new ContentValues(); - values.put(COLUMN_NAME,"device_1"); - db.insert(TABLE_DEVICE,null,values); - values.put(COLUMN_NAME,"device_2"); - db.insert(TABLE_DEVICE,null,values); - values.put(COLUMN_NAME,"device_3"); - db.insert(TABLE_DEVICE,null,values); - values.put(COLUMN_NAME,"device_4"); - db.insert(TABLE_DEVICE,null,values); + for (String device: Constants.DEVICES){ + values.put(COLUMN_NAME,device); + db.insert(TABLE_DEVICE,null,values); + } +// values.put(COLUMN_NAME,"device_1"); +// db.insert(TABLE_DEVICE,null,values); +// values.put(COLUMN_NAME,"device_2"); +// db.insert(TABLE_DEVICE,null,values); +// values.put(COLUMN_NAME,"device_3"); +// db.insert(TABLE_DEVICE,null,values); +// values.put(COLUMN_NAME,"device_4"); +// db.insert(TABLE_DEVICE,null,values); values = new ContentValues(); db.execSQL(ROLES_TABLE_CREATE); diff --git a/mobile/src/main/java/com/ktind/cgm/bgscout/mqtt/MQTTMgr.java b/mobile/src/main/java/com/ktind/cgm/bgscout/mqtt/MQTTMgr.java index fcf5068..2c1ac76 100644 --- a/mobile/src/main/java/com/ktind/cgm/bgscout/mqtt/MQTTMgr.java +++ b/mobile/src/main/java/com/ktind/cgm/bgscout/mqtt/MQTTMgr.java @@ -60,19 +60,13 @@ public class MQTTMgr implements MqttCallback,MQTTMgrObservable { public static final int MQTT_QOS_0 = 0; // QOS Level 0 ( Delivery Once no confirmation ) public static final int MQTT_QOS_1 = 1; // QOS Level 1 ( Delevery at least Once with confirmation ) public static final int MQTT_QOS_2 = 2; // QOS Level 2 ( Delivery only once with confirmation with handshake ) - private static final int MQTT_KEEP_ALIVE = 24000; // KeepAlive Interval in MS private static final String MQTT_KEEP_ALIVE_TOPIC_FORMAT = "/users/%s/keepalive"; // Topic format for KeepAlives private static final byte[] MQTT_KEEP_ALIVE_MESSAGE = { 0 }; // Keep Alive message to send private static final int MQTT_KEEP_ALIVE_QOS = MQTT_QOS_0; // Default Keepalive QOS private static final boolean MQTT_CLEAN_SESSION = false; - private static final String MQTT_URL_FORMAT = "tcp://%s:%d"; - private static final String DEVICE_ID_FORMAT = "%s_%s"; + private static final String DEVICE_ID_FORMAT = "%s_1_%s"; private static final long RECONNECT_DELAY=60000L; private static final int KEEPALIVE_INTERVAL=150000; -// private static final int KEEPALIVE_INTERVAL=30000; - - private DownloadObject lastDownload; - private AlarmReceiver alarmReceiver; private AlarmReceiver keepAliveReceiver; private ReconnectReceiver reconnectReceiver; private NetConnReceiver netConnReceiver; @@ -80,7 +74,6 @@ public class MQTTMgr implements MqttCallback,MQTTMgrObservable { // private MqttDefaultFilePersistence mDataStore; private MemoryPersistence mDataStore; private MqttConnectOptions mOpts; -// private MqttTopic mKeepAliveTopic; private MqttClient mClient; private Context context; @@ -127,13 +120,9 @@ public void initConnect(String url){ } public void initConnect(String url, String lwt) { -// alarmReceiver=new AlarmReceiver(); -// setupNetworkNotifications(); -// setupReconnect(); -// setupKeepAlives(); - + setupReconnect(); + setupKeepAlives(); connect(url, lwt); - } public void connect(String url, String lwt) { @@ -143,8 +132,6 @@ public void connect(String url, String lwt) { return; } // setupNetworkNotifications(); - setupReconnect(); - setupKeepAlives(); // mClient=null; stats.addConnect(); setupOpts(lwt); @@ -481,22 +468,6 @@ public void onReceive(Context context, Intent intent) { }else{ Log.d(TAG,deviceIDStr+": Ignored a request for "+intent.getExtras().get("device")+" to perform an MQTT keepalive operation"); } - } else if (intent.getAction().equals(RECONNECT_INTENT_FILTER)) { - Log.d(TAG,"Received broadcast to reconnect"); - // Prevent a reconnect if we haven't subscribed to anything. - // I suspect this was a race condition where a reconnect somehow triggered before the initial connection completed - reconnect(); - } else if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)){ - Log.d(TAG,"Received network broadcast "+intent.getAction()); - if (mqttUrl==null){ - Log.e(TAG,"Ignoring connection change because mqttUrl is null"); - return; - } - if (isOnline() && state==State.CONNECTED) { - stats.addNetworkNotification(); - Log.i(TAG, "Network is online. Attempting to reconnect"); - reconnectDelayed(5000); - } } } } @@ -505,30 +476,11 @@ protected class ReconnectReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG,"Received a broadcast: "+intent.getAction()); - if (intent.getAction().equals(KEEPALIVE_INTENT_FILTER)){ - if (intent.getExtras().get("device").equals(deviceIDStr)) { - Log.d(TAG, "Received a request to perform an MQTT keepalive operation on " + intent.getExtras().get("device")); - sendKeepalive(); -// alarmMgr.set(AlarmManager.RTC_WAKEUP,System.currentTimeMillis()+KEEPALIVE_INTERVAL-3000L,keepAlivePendingIntent); - }else{ - Log.d(TAG,deviceIDStr+": Ignored a request for "+intent.getExtras().get("device")+" to perform an MQTT keepalive operation"); - } - } else if (intent.getAction().equals(RECONNECT_INTENT_FILTER)) { + if (intent.getAction().equals(RECONNECT_INTENT_FILTER)) { Log.d(TAG,"Received broadcast to reconnect"); // Prevent a reconnect if we haven't subscribed to anything. // I suspect this was a race condition where a reconnect somehow triggered before the initial connection completed reconnect(); - } else if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)){ - Log.d(TAG,"Received network broadcast "+intent.getAction()); - if (mqttUrl==null){ - Log.e(TAG,"Ignoring connection change because mqttUrl is null"); - return; - } - if (isOnline() && state==State.CONNECTED) { - stats.addNetworkNotification(); - Log.i(TAG, "Network is online. Attempting to reconnect"); - reconnectDelayed(5000); - } } } } @@ -537,20 +489,7 @@ protected class NetConnReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG,"Received a broadcast: "+intent.getAction()); - if (intent.getAction().equals(KEEPALIVE_INTENT_FILTER)){ - if (intent.getExtras().get("device").equals(deviceIDStr)) { - Log.d(TAG, "Received a request to perform an MQTT keepalive operation on " + intent.getExtras().get("device")); - sendKeepalive(); -// alarmMgr.set(AlarmManager.RTC_WAKEUP,System.currentTimeMillis()+KEEPALIVE_INTERVAL-3000L,keepAlivePendingIntent); - }else{ - Log.d(TAG,deviceIDStr+": Ignored a request for "+intent.getExtras().get("device")+" to perform an MQTT keepalive operation"); - } - } else if (intent.getAction().equals(RECONNECT_INTENT_FILTER)) { - Log.d(TAG,"Received broadcast to reconnect"); - // Prevent a reconnect if we haven't subscribed to anything. - // I suspect this was a race condition where a reconnect somehow triggered before the initial connection completed - reconnect(); - } else if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)){ + if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)){ Log.d(TAG,"Received network broadcast "+intent.getAction()); if (mqttUrl==null){ Log.e(TAG,"Ignoring connection change because mqttUrl is null"); diff --git a/mobile/src/main/res/drawable-hdpi/ic_snooze.png b/mobile/src/main/res/drawable-hdpi/ic_snooze.png new file mode 100644 index 0000000000000000000000000000000000000000..4037453be0549fd2b3202916da05f3d30ba09963 GIT binary patch literal 1880 zcmZ`)dpy$%8~-(#+;f`At=vPVSyPO9o6BrR8-^rdVsmRz4H+``$a%5oAah-;h%wUK zTAbt3sf5azd#M&CaYP~B_5Sz%@qV7q^L#(g<@-E;J)du;E5S)oPD2g=07V?ufha-v zPu~NUL@&_y3<>NGM&r={(3m5?6(B9icU>hq*#Y%0wB{sO7o8xqUEm_*C3dS7Hk9VesVS_coWines}al7u(g4TGKQ zW19lQHV2U-u2tLMgOuSU+R;k3Jdb4ArV?VeuiRQMra+)cA%0}r-bY%3!a+So+<=&< zg%%COmKHx;_0FPxnW0&@5-PWnD^hu{dn;IcRdleyPyT+)qAU!3-_{b5*ZNf18?0 zL02)X1-9jb`GA$umh6^-SSnpl{fEGmBrE(jrfjPquKfHc@~zxk){(h#`Q^(vmq&YL zw#N-0h6jD z;5NQtp4B^q#7$e3jxeEW>MA*_-15^cNWO5a^_-QbXk$bGD z)AXcTGesFjQT-sCt8`6vfAiXqAVXkpv;&Vk6e*|?^F5x*evo|357x%-w^F>gE1 zXA-$4p(W}k$EJMdOQ3OeOKushgF(AX!wnW=roG-*GymBN!aZ(N%CY9oF{7RGinL8w zkIws%5Y?bKi@|740MhsY&s8rMOWvX0urBFB>tjUkN-R#!!+VAUB}3s zjZj~@m2~8guhg1Bk3#dnm``+KU&8oI$y(Xg1nD#t*UDcb=VffXbH$jXve&Z{i;n9A zpWdBq)%};2%j02__!_pTvu6c}V6Jj_*NdC@vDo2LkfLi`@Ryvr!JAL+7`n#@G+d8y zlub2X!En41LP2u>*SIez}T=`oF}l6SMLWPW~%YLF$?i^Q1Vs8K`gKE&Ld zbdeet#}bWOxS<=nir_Qr1{w{Kq;mW)tic9at;Iizug+I3@Wof@CdT}opvUE6UV#*E5$=xTM9aQ!wsp9rW}fe~ z0|b5{h|38JW#BgYtRH!E@+&#Yl>o6_g5dnyu^kJQxn5Zf$GaZRG1zplLxuQ0^gjGT zHhZ0=5`k&DA67vuij~{_*C)QvLHfY<2 zt~%!@B1UV>Pxyz^Y|10eh4%Dg8)mH9`G6+g&~$%Tg&zqj6n-kG&tbH%gR6oEY&JXg zdcd`n3Pl(?s?$-}JYkV;E~om;!1%<1|Dn)^`#Z{F%PE`6Rjs@g(Pr7-J7d=HKD!)v8O(byst;n?=!@h33E82Y7nD2^27w4@eY?pL{! zv@PvHdcC{byKht?9)>i>v$UW98W|OMg)9L80k=TFEZ{J>rH2I)WnqCrAYkz0C^)=Y zn7sTy1ZqT3C?)p)6Gl@}5`hEMe_n8ppwMChu8;wAP{akY4vrcSOeT^8f?_WZkbf#1 NfO8}`)Z3Ag{{xwtVEF(5 literal 0 HcmV?d00001 diff --git a/mobile/src/main/res/layout/activity_device.xml b/mobile/src/main/res/layout/activity_device.xml index 7fe7ba1..5d88f57 100644 --- a/mobile/src/main/res/layout/activity_device.xml +++ b/mobile/src/main/res/layout/activity_device.xml @@ -2,7 +2,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/drawer_layout2" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:background="#ff3c3f41"> + + + + + + + + + + + diff --git a/mobile/src/main/res/layout/fragment_device.xml b/mobile/src/main/res/layout/fragment_device.xml index 1d0e330..ceb36be 100644 --- a/mobile/src/main/res/layout/fragment_device.xml +++ b/mobile/src/main/res/layout/fragment_device.xml @@ -6,11 +6,19 @@ android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" - tools:context="com.ktind.cgm.bgscout.DeviceActivity$PlaceholderFragment"> + tools:context="com.ktind.cgm.bgscout.DeviceActivity$PlaceholderFragment" + android:background="#ff3c3f41"> + android:layout_height="fill_parent" + android:background="#FFFFFFFF" + android:layout_alignParentEnd="false" + android:layout_alignParentStart="false" + android:layout_alignParentTop="false" + android:layout_alignParentLeft="false" + android:layout_alignParentBottom="false" + android:layout_alignParentRight="false"> diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 15b1e60..7d28aea 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -59,7 +59,7 @@ Open closed Hello blank fragment - DeviceActivity + Sugarcaster-multi Section 1 Section 2 Section 3 @@ -80,5 +80,6 @@ Critical high ringtone Critical Low Critical low ringtone + Call PWD diff --git a/mobile/src/main/res/xml/pref_general.xml b/mobile/src/main/res/xml/pref_general.xml index e64f37a..a0b2541 100644 --- a/mobile/src/main/res/xml/pref_general.xml +++ b/mobile/src/main/res/xml/pref_general.xml @@ -122,6 +122,11 @@ android:dependency="device_1_enable" android:title="Enable Android notification" android:checked="true"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pEBBLE_KIT/src/main/AndroidManifest.xml b/pEBBLE_KIT/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e0a0174 --- /dev/null +++ b/pEBBLE_KIT/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/Constants.java b/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/Constants.java new file mode 100644 index 0000000..1b9a37d --- /dev/null +++ b/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/Constants.java @@ -0,0 +1,366 @@ +package com.getpebble.android.kit; + +import java.util.UUID; + +/** + * Constant values used by PebbleKit-enabled android applications. + * + */ +public final class Constants { + + /** + * Intent broadcast by pebble.apk when a new connection to a Pebble is established. + */ + public static final String INTENT_PEBBLE_CONNECTED = "com.getpebble.action.PEBBLE_CONNECTED"; + /** + * Intent broadcast by pebble.apk when the connection to a Pebble is closed or lost. + */ + public static final String INTENT_PEBBLE_DISCONNECTED = "com.getpebble.action.PEBBLE_DISCONNECTED"; + + /** + * Intent broadcast to pebble.apk to indicate that a message was received from the watch. To avoid protocol timeouts + * on the watch, applications must ACK or NACK all received messages. + */ + public static final String INTENT_APP_ACK = "com.getpebble.action.app.ACK"; + + /** + * Intent broadcast to pebble.apk to indicate that a message was unsuccessfully received from the watch. + */ + public static final String INTENT_APP_NACK = "com.getpebble.action.app.NACK"; + + /** + * Intent broadcast from pebble.apk containing one-or-more key-value pairs sent from the watch to the phone. + */ + public static final String INTENT_APP_RECEIVE = "com.getpebble.action.app.RECEIVE"; + + + /** + * Intent broadcast from pebble.apk indicating that a sent message was successfully received by a watch app. + */ + public static final String INTENT_APP_RECEIVE_ACK = "com.getpebble.action.app.RECEIVE_ACK"; + + /** + * Intent broadcast from pebble.apk indicating that a sent message was not received by a watch app. + */ + public static final String INTENT_APP_RECEIVE_NACK = "com.getpebble.action.app.RECEIVE_NACK"; + + /** + * Intent broadcast to pebble.apk containing one-or-more key-value pairs to be sent to the watch from the phone. + */ + public static final String INTENT_APP_SEND = "com.getpebble.action.app.SEND"; + + /** + * Intent broadcast to pebble.apk responsible for launching a watch-app on the connected watch. This intent is + * idempotent. + */ + public static final String INTENT_APP_START = "com.getpebble.action.app.START"; + + /** + * Intent broadcast to pebble.apk responsible for closing a running watch-app on the connected watch. This intent is + * idempotent. + */ + public static final String INTENT_APP_STOP = "com.getpebble.action.app.STOP"; + + /** + * Intent broadcast to pebble.apk responsible for customizing the name and icon of the 'stock' Sports and Golf + * applications included in the watch's firmware. + */ + public static final String INTENT_APP_CUSTOMIZE = "com.getpebble.action.app.CONFIGURE"; + + /** + * Intent broadcast from pebble.apk containing a unit of data from a data log. + */ + public static final String INTENT_DL_RECEIVE_DATA = "com.getpebble.action.dl.RECEIVE_DATA"; + + /** + * Intent broadcast to pebble.apk implicitly when a unit of data from a data log is received. + */ + public static final String INTENT_DL_ACK_DATA = "com.getpebble.action.dl.ACK_DATA"; + + /** + * Intent broadcast to pebble.apk to request data logs for a particular app. + */ + public static final String INTENT_DL_REQUEST_DATA = "com.getpebble.action.dl.REQUEST_DATA"; + + /** + * Intent broadcast from pebble.apk indicating the session has finished. + */ + public static final String INTENT_DL_FINISH_SESSION = "com.getpebble.action.dl.FINISH_SESSION"; + + /** + * The UUID corresponding to Pebble's built-in "Sports" application. + */ + public static final UUID SPORTS_UUID = UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"); + + /** + * The UUID corresponding to Pebble's built-in "Golf" application. + */ + public static final UUID GOLF_UUID = UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"); + + /** + * The bundle-key used to store a message's transaction id. + */ + public static final String TRANSACTION_ID = "transaction_id"; + + /** + * The bundle-key used to store a message's UUID. + */ + public static final String APP_UUID = "uuid"; + + /** + * The bundle-key used to store a message's JSON payload send-to or received-from the watch. + */ + public static final String MSG_DATA = "msg_data"; + + /** + * The bundle-key used to store the type of application being customized in a CUSTOMIZE intent. + */ + public static final String CUST_APP_TYPE = "app_type"; + + /** + * The bundle-key used to store the custom name provided in a CUSTOMIZE intent. + */ + public static final String CUST_NAME = "name"; + + /** + * The bundle-key used to store the custom icon provided in a CUSTOMIZE intent. + */ + public static final String CUST_ICON = "icon"; + + /** + * The bundle-key used to store the timestamp of when a data log was first created. + */ + public static final String DATA_LOG_TIMESTAMP = "data_log_timestamp"; + + /** + * A bundle-key used to store the UUID that uniquely identifies a data log. + */ + public static final String DATA_LOG_UUID = "data_log_uuid"; + + /** + * A bundle-key used to store the tag for the corresponding data log. + */ + public static final String DATA_LOG_TAG = "data_log_tag"; + + /** + * A bundle-key used to store the ID of a unit of data in a data log. + */ + public static final String PBL_DATA_ID = "pbl_data_id"; + + /** + * A bundle-key used to store the data type of the data unit. + */ + public static final String PBL_DATA_TYPE = "pbl_data_type"; + + /** + * A bundle-key used to store the value of the data unit. + */ + public static final String PBL_DATA_OBJECT = "pbl_data_object"; + + /** + * The PebbleDictionary key corresponding to the 'time' field sent to the Sports watch-app. + */ + public static final int SPORTS_TIME_KEY = 0x00; + /** + * The PebbleDictionary key corresponding to the 'distance' field sent to the Sports watch-app. + */ + public static final int SPORTS_DISTANCE_KEY = 0x01; + /** + * The PebbleDictionary key corresponding to the 'data' field sent to the Sports watch-app. The data field is paired + * with a variable label and can be used to display any data. + */ + public static final int SPORTS_DATA_KEY = 0x02; + /** + * The PebbleDictionary key corresponding to the 'units' field sent to the Sports watch-app. + */ + public static final int SPORTS_UNITS_KEY = 0x03; + /** + * The PebbleDictionary key corresponding to the 'state' field sent to the Sports watch-app. Both the watch and + * phone-app may modify this field. The phone-application is responsible for performing any required state + * transitions to stay in sync with the watch-app's state. + */ + public static final int SPORTS_STATE_KEY = 0x04; + /** + * The PebbleDictionary key corresponding to the 'label' field sent to the Sports watch-app. The label field + * controls the label above the 'data' field. + */ + public static final int SPORTS_LABEL_KEY = 0x05; + + /** + * PebbleDictionary value corresponding to 'imperial' units. + */ + public static final int SPORTS_UNITS_IMPERIAL = 0x00; + /** + * PebbleDictionary value corresponding to 'metric' units. + */ + public static final int SPORTS_UNITS_METRIC = 0x01; + /** + * PebbleDictionary value corresponding to 'speed' data. + */ + public static final int SPORTS_DATA_SPEED = 0x00; + /** + * PebbleDictionary value corresponding to 'pace' data. + */ + public static final int SPORTS_DATA_PACE = 0x01; + + /** + * The Constant SPORTS_STATE_INIT. + */ + public static final int SPORTS_STATE_INIT = 0x00; + + /** + * The Constant SPORTS_STATE_RUNNING. + */ + public static final int SPORTS_STATE_RUNNING = 0x01; + + /** + * The Constant SPORTS_STATE_PAUSED. + */ + public static final int SPORTS_STATE_PAUSED = 0x02; + + /** + * The Constant SPORTS_STATE_END. + */ + public static final int SPORTS_STATE_END = 0x03; + + /** + * The Constant GOLF_FRONT_KEY. + */ + public static final int GOLF_FRONT_KEY = 0x00; + + /** + * The Constant GOLF_MID_KEY. + */ + public static final int GOLF_MID_KEY = 0x01; + + /** + * The Constant GOLF_BACK_KEY. + */ + public static final int GOLF_BACK_KEY = 0x02; + + /** + * The Constant GOLF_HOLE_KEY. + */ + public static final int GOLF_HOLE_KEY = 0x03; + + /** + * The Constant GOLF_PAR_KEY. + */ + public static final int GOLF_PAR_KEY = 0x04; + + /** + * The Constant GOLF_CMD_KEY. + */ + public static final int GOLF_CMD_KEY = 0x05; + + /** + * Command sent by the golf-application to display the next hole. + */ + public static final int GOLF_CMD_PREV = 0x01; + + /** + * Command sent by the golf-application to display the previous hole. + */ + public static final int GOLF_CMD_NEXT = 0x02; + + + public static final int KIT_STATE_COLUMN_CONNECTED = 0; + public static final int KIT_STATE_COLUMN_APPMSG_SUPPORT = 1; + public static final int KIT_STATE_COLUMN_DATALOGGING_SUPPORT = 2; + public static final int KIT_STATE_COLUMN_VERSION_MAJOR = 3; + public static final int KIT_STATE_COLUMN_VERSION_MINOR = 4; + public static final int KIT_STATE_COLUMN_VERSION_POINT = 5; + public static final int KIT_STATE_COLUMN_VERSION_TAG = 6; + + /** + * Instantiates a new constants. + */ + private Constants() { + + } + + /** + * The Enum PebbleAppType. + */ + public static enum PebbleAppType { + + /** + * The sports. + */ + SPORTS(0x00), + + /** + * The golf. + */ + GOLF(0x01), + + /** + * The other. + */ + OTHER(0xff); + + /** + * The ord. + */ + public final int ord; + + /** + * Instantiates a new pebble app type. + * + * @param ord + * the ord + */ + private PebbleAppType(final int ord) { + this.ord = ord; + } + } + + /** + * The Enum PebbleDataType. + */ + public static enum PebbleDataType { + /** + * The byte[]. + */ + BYTES(0x00), + + /** + * The UnsignedInteger. + */ + UINT(0x02), + + /** + * The Integer. + */ + INT(0x03), + + /** + * The Invalid. + */ + INVALID(0xff); + + /** + * The ord. + */ + public final byte ord; + + /** + * Instantiates a new pebble data type. + */ + private PebbleDataType(int ord) { + this.ord = (byte) ord; + } + + /** + * Instantiates a new pebble data type from a byte. + */ + public static PebbleDataType fromByte(byte b) { + for (PebbleDataType type : values()) { + if (type.ord == b) { + return type; + } + } + return null; + } + } +} diff --git a/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/PebbleKit.java b/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/PebbleKit.java new file mode 100644 index 0000000..2debb32 --- /dev/null +++ b/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/PebbleKit.java @@ -0,0 +1,914 @@ +package com.getpebble.android.kit; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Base64; +import android.util.Log; +import com.getpebble.android.kit.Constants.*; +import com.getpebble.android.kit.util.PebbleDictionary; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; + +import java.util.UUID; + +import static com.getpebble.android.kit.Constants.*; + +/** + * A helper class providing methods for interacting with third-party Pebble Smartwatch applications. Pebble-enabled + * Android applications may use this class to assist in sending/receiving data between the watch and the phone. + */ +public final class PebbleKit { + + /** + * The Constant NAME_MAX_LENGTH. + */ + private static final int NAME_MAX_LENGTH = 32; + + /** + * The Constant ICON_MAX_DIMENSIONS. + */ + private static final int ICON_MAX_DIMENSIONS = 32; + + /** + * Instantiates a new pebble kit. + */ + private PebbleKit() { + + } + + /** + * Send a message to the connected Pebble to "customize" a built-in PebbleKit watch-app. This is intended to allow + * third-party Android applications to apply custom branding (both name & icon) on the watch without needing to + * distribute a complete watch-app. + * + * @param context + * The context used to send the broadcast. (Protip: pass in the ApplicationContext here.) + * @param appType + * The watch-app to be configured. Options are either + * @param name + * The custom name to be applied to the watch-app. Names must be less than 32 characters in length. + * @param icon + * The custom icon to be applied to the watch-app. Icons must be black-and-white bitmaps no larger than 32px + * in either dimension. + * + * @throws IllegalArgumentException + * Thrown if the specified name or icon are invalid. {@link PebbleAppType#SPORTS} or {@link + * PebbleAppType#GOLF}. + */ + public static void customizeWatchApp(final Context context, final PebbleAppType appType, + final String name, final Bitmap icon) throws IllegalArgumentException { + + if (appType == null) { + throw new IllegalArgumentException("app type cannot be null"); + } + + if (name.length() > NAME_MAX_LENGTH) { + throw new IllegalArgumentException(String.format( + "app name exceeds maximum length (%d)", NAME_MAX_LENGTH)); + } + + if (icon.getHeight() > ICON_MAX_DIMENSIONS || icon.getWidth() > ICON_MAX_DIMENSIONS) { + throw new IllegalArgumentException(String.format( + "app icon exceeds maximum dimensions (32px x 32px); got (%dpx x %dpx)", + icon.getWidth(), icon.getHeight())); + } + + final Intent customizeAppIntent = new Intent(INTENT_APP_CUSTOMIZE); + customizeAppIntent.putExtra(CUST_APP_TYPE, appType.ord); + customizeAppIntent.putExtra(CUST_NAME, name); + customizeAppIntent.putExtra(CUST_ICON, icon); + context.sendBroadcast(customizeAppIntent); + } + + /** + * Synchronously query the Pebble application to see if an active Bluetooth connection to a watch currently exists. + * + * @param context + * The Android context used to perform the query. + *

+ * Protip: You probably want to use your ApplicationContext here. + * + * @return true if an active connection to the watch currently exists, otherwise false. This method will also return + * false if the Pebble application is not installed on the user's handset. + */ + public static boolean isWatchConnected(final Context context) { + Cursor c = + context.getContentResolver().query( + Uri.parse("content://com.getpebble.android.provider/state"), null, null, + null, null); + if (c == null || !c.moveToNext()) { + return false; + } + return c.getInt(KIT_STATE_COLUMN_CONNECTED) == 1; + } + + /** + * Synchronously query the Pebble application to see if the connected watch is running a firmware version that + * supports PebbleKit messages. + * + * @param context + * The Android context used to perform the query. + *

+ * Protip: You probably want to use your ApplicationContext here. + * + * @return true if the watch supports PebbleKit messages, otherwise false. This method will always return false if + * no Pebble is currently connected to the handset. + */ + public static boolean areAppMessagesSupported(final Context context) { + Cursor c = + context.getContentResolver().query( + Uri.parse("content://com.getpebble.android.provider/state"), null, null, + null, null); + if (c == null || !c.moveToNext()) { + return false; + } + return c.getInt(KIT_STATE_COLUMN_APPMSG_SUPPORT) == 1; + } + + + /** + * Get the version information of the firmware running on a connected watch. + * + * @param context + * The Android context used to perform the query. + *

+ * Protip: You probably want to use your ApplicationContext here. + * + * @return null if the watch is disconnected or we can't get the version. Otherwise, + * a FirmwareVersionObject containing info on the watch FW version + */ + public static FirmwareVersionInfo getWatchFWVersion(final Context context) { + Cursor c = + context.getContentResolver().query( + Uri.parse("content://com.getpebble.android.provider/state"), null, null, + null, null); + if (c == null || !c.moveToNext()) { + return null; + } + + int majorVersion = c.getInt(KIT_STATE_COLUMN_VERSION_MAJOR); + int minorVersion = c.getInt(KIT_STATE_COLUMN_VERSION_MINOR); + int pointVersion = c.getInt(KIT_STATE_COLUMN_VERSION_POINT); + String versionTag = c.getString(KIT_STATE_COLUMN_VERSION_TAG); + + return new FirmwareVersionInfo(majorVersion, minorVersion, pointVersion, versionTag); + } + + /** + * Synchronously query the Pebble application to see if the connected watch is running a firmware version that + * supports PebbleKit data logging. + * + * @param context + * The Android context used to perform the query. + *

+ * Protip: You probably want to use your ApplicationContext here. + * + * @return true if the watch supports PebbleKit messages, otherwise false. This method will always return false if + * no Pebble is currently connected to the handset. + */ + public static boolean isDataLoggingSupported(final Context context) { + Cursor c = + context.getContentResolver().query( + Uri.parse("content://com.getpebble.android.provider/state"), + null, null, null, null); + if (c == null || !c.moveToNext()) { + return false; + } + return c.getInt(KIT_STATE_COLUMN_DATALOGGING_SUPPORT) == 1; + } + + /** + * Send a message to the connected Pebble to launch an application identified by a UUID. If another application is + * currently running it will be terminated and the new application will be brought to the foreground. + * + * @param context + * The context used to send the broadcast. + * @param watchappUuid + * A UUID uniquely identifying the target application. UUIDs for the stock PebbleKit applications are + * available in {@link Constants}. + * + * @throws IllegalArgumentException + * Thrown if the specified UUID is invalid. + */ + public static void startAppOnPebble(final Context context, final UUID watchappUuid) + throws IllegalArgumentException { + + if (watchappUuid == null) { + throw new IllegalArgumentException("uuid cannot be null"); + } + + final Intent startAppIntent = new Intent(INTENT_APP_START); + startAppIntent.putExtra(APP_UUID, watchappUuid); + context.sendBroadcast(startAppIntent); + } + + /** + * Send a message to the connected Pebble to close an application identified by a UUID. If this application is not + * currently running, the message is ignored. + * + * @param context + * The context used to send the broadcast. + * @param watchappUuid + * A UUID uniquely identifying the target application. UUIDs for the stock kit applications are available in + * {@link Constants}. + * + * @throws IllegalArgumentException + * Thrown if the specified UUID is invalid. + */ + public static void closeAppOnPebble(final Context context, final UUID watchappUuid) + throws IllegalArgumentException { + + if (watchappUuid == null) { + throw new IllegalArgumentException("uuid cannot be null"); + } + + final Intent stopAppIntent = new Intent(INTENT_APP_STOP); + stopAppIntent.putExtra(APP_UUID, watchappUuid); + context.sendBroadcast(stopAppIntent); + } + + /** + * Send one-or-more key-value pairs to the watch-app identified by the provided UUID. This is the primary method for + * sending data from the phone to a connected Pebble. + *

+ * The watch-app and phone-app must agree of the set and type of key-value pairs being exchanged. Type mismatches or + * missing keys will cause errors on the receiver's end. + * + * @param context + * The context used to send the broadcast. + * @param watchappUuid + * A UUID uniquely identifying the target application. UUIDs for the stock kit applications are available in + * {@link Constants}. + * @param data + * A dictionary containing one-or-more key-value pairs. For more information about the types of data that + * can be stored, see {@link PebbleDictionary}. + * + * @throws IllegalArgumentException + * Thrown in the specified PebbleDictionary or UUID is invalid. + */ + public static void sendDataToPebble(final Context context, final UUID watchappUuid, + final PebbleDictionary data) throws IllegalArgumentException { + + sendDataToPebbleWithTransactionId(context, watchappUuid, data, -1); + } + + /** + * Send one-or-more key-value pairs to the watch-app identified by the provided UUID. + *

+ * The watch-app and phone-app must agree of the set and type of key-value pairs being exchanged. Type mismatches or + * missing keys will cause errors on the receiver's end. + * + * @param context + * The context used to send the broadcast. + * @param watchappUuid + * A UUID uniquely identifying the target application. UUIDs for the stock kit applications are available in + * {@link Constants}. + * @param data + * A dictionary containing one-or-more key-value pairs. For more information about the types of data that + * can be stored, see {@link PebbleDictionary}. + * @param transactionId + * An integer uniquely identifying the transaction. This can be used to correlate messages sent to the + * Pebble and ACK/NACKs received from the Pebble. + * + * @throws IllegalArgumentException + * Thrown in the specified PebbleDictionary or UUID is invalid. + */ + public static void sendDataToPebbleWithTransactionId(final Context context, + final UUID watchappUuid, final PebbleDictionary data, final int transactionId) + throws IllegalArgumentException { + + if (watchappUuid == null) { + throw new IllegalArgumentException("uuid cannot be null"); + } + + if (data == null) { + throw new IllegalArgumentException("data cannot be null"); + } + + if (data.size() == 0) { + return; + } + + final Intent sendDataIntent = new Intent(INTENT_APP_SEND); + sendDataIntent.putExtra(APP_UUID, watchappUuid); + sendDataIntent.putExtra(TRANSACTION_ID, transactionId); + sendDataIntent.putExtra(MSG_DATA, data.toJsonString()); + context.sendBroadcast(sendDataIntent); + } + + /** + * Send a message to the connected watch acknowledging the receipt of a PebbleDictionary. To avoid protocol timeouts + * on the watch, applications must ACK or NACK all received messages. + * + * @param context + * The context used to send the broadcast. + * @param transactionId + * The transaction id of the message in which the data was received. Valid transaction IDs are between (0, + * 255). + * + * @throws IllegalArgumentException + * Thrown if an invalid transaction id is specified. + */ + public static void sendAckToPebble(final Context context, final int transactionId) + throws IllegalArgumentException { + + if ((transactionId & ~0xff) != 0) { + throw new IllegalArgumentException(String.format( + "transaction id must be between (0, 255); got '%d'", transactionId)); + } + + final Intent ackIntent = new Intent(INTENT_APP_ACK); + ackIntent.putExtra(TRANSACTION_ID, transactionId); + context.sendBroadcast(ackIntent); + } + + /** + * Send a message to the connected watch that the previously sent PebbleDictionary was not received successfully. To + * avoid protocol timeouts on the watch, applications must ACK or NACK all received messages. + * + * @param context + * The context used to send the broadcast. + * @param transactionId + * The transaction id of the message in which the data was received. Valid transaction IDs are between (0, + * 255). + * + * @throws IllegalArgumentException + * Thrown if an invalid transaction id is specified. + */ + public static void sendNackToPebble(final Context context, final int transactionId) + throws IllegalArgumentException { + + if ((transactionId & ~0xff) != 0) { + throw new IllegalArgumentException(String.format( + "transaction id must be between (0, 255); got '%d'", transactionId)); + } + + final Intent nackIntent = new Intent(INTENT_APP_NACK); + nackIntent.putExtra(TRANSACTION_ID, transactionId); + context.sendBroadcast(nackIntent); + } + + /** + * A convenience function to assist in programatically registering a broadcast receiver for the 'CONNECTED' intent. + *

+ * To avoid leaking memory, activities registering BroadcastReceivers must unregister them in the + * Activity's {@link android.app.Activity#onPause()} method. + * + * @param context + * The context in which to register the BroadcastReceiver. + * @param receiver + * The receiver to be registered. + * + * @return The registered receiver. + * + * @see Constants#INTENT_PEBBLE_CONNECTED + */ + public static BroadcastReceiver registerPebbleConnectedReceiver(final Context context, + final BroadcastReceiver receiver) { + return registerBroadcastReceiverInternal(context, INTENT_PEBBLE_CONNECTED, receiver); + } + + /** + * A convenience function to assist in programatically registering a broadcast receiver for the 'DISCONNECTED' + * intent. + *

+ * Go avoid leaking memory, activities registering BroadcastReceivers must unregister them in the + * Activity's {@link android.app.Activity#onPause()} method. + * + * @param context + * The context in which to register the BroadcastReceiver. + * @param receiver + * The receiver to be registered. + * + * @return The registered receiver. + * + * @see Constants#INTENT_PEBBLE_DISCONNECTED + */ + public static BroadcastReceiver registerPebbleDisconnectedReceiver(final Context context, + final BroadcastReceiver receiver) { + return registerBroadcastReceiverInternal(context, INTENT_PEBBLE_DISCONNECTED, receiver); + } + + /** + * A convenience function to assist in programatically registering a broadcast receiver for the 'RECEIVE' intent. + *

+ * To avoid leaking memory, activities registering BroadcastReceivers must unregister them in the + * Activity's {@link android.app.Activity#onPause()} method. + * + * @param context + * The context in which to register the BroadcastReceiver. + * @param receiver + * The receiver to be registered. + * + * @return The registered receiver. + * + * @see Constants#INTENT_APP_RECEIVE + */ + public static BroadcastReceiver registerReceivedDataHandler(final Context context, + final PebbleDataReceiver receiver) { + return registerBroadcastReceiverInternal(context, INTENT_APP_RECEIVE, receiver); + } + + + /** + * A convenience function to assist in programatically registering a broadcast receiver for the 'RECEIVE_ACK' + * intent. + *

+ * To avoid leaking memory, activities registering BroadcastReceivers must unregister them in the + * Activity's {@link android.app.Activity#onPause()} method. + * + * @param context + * The context in which to register the BroadcastReceiver. + * @param receiver + * The receiver to be registered. + * + * @return The registered receiver. + * + * @see Constants#INTENT_APP_RECEIVE_ACK + */ + public static BroadcastReceiver registerReceivedAckHandler(final Context context, + final PebbleAckReceiver receiver) { + return registerBroadcastReceiverInternal(context, INTENT_APP_RECEIVE_ACK, receiver); + } + + /** + * A convenience function to assist in programatically registering a broadcast receiver for the 'RECEIVE_NACK' + * intent. + *

+ * To avoid leaking memory, activities registering BroadcastReceivers must unregister them in the + * Activity's {@link android.app.Activity#onPause()} method. + * + * @param context + * The context in which to register the BroadcastReceiver. + * @param receiver + * The receiver to be registered. + * + * @return The registered receiver. + * + * @see Constants#INTENT_APP_RECEIVE_NACK + */ + public static BroadcastReceiver registerReceivedNackHandler(final Context context, + final PebbleNackReceiver receiver) { + return registerBroadcastReceiverInternal(context, INTENT_APP_RECEIVE_NACK, receiver); + } + + /** + * Register broadcast receiver internal. + * + * @param context + * the context + * @param action + * the action + * @param receiver + * the receiver + * + * @return the broadcast receiver + */ + private static BroadcastReceiver registerBroadcastReceiverInternal(final Context context, + final String action, final BroadcastReceiver receiver) { + if (receiver == null) { + return null; + } + + IntentFilter filter = new IntentFilter(action); + context.registerReceiver(receiver, filter); + return receiver; + } + + /** + * A special-purpose BroadcastReceiver that makes it easy to handle 'RECEIVE' intents broadcast from pebble.apk. + */ + public static abstract class PebbleDataReceiver extends BroadcastReceiver { + + /** + * The subscribed uuid. + */ + private final UUID subscribedUuid; + + /** + * Instantiates a new pebble data receiver. + * + * @param subscribedUuid + * the subscribed uuid + */ + protected PebbleDataReceiver(final UUID subscribedUuid) { + this.subscribedUuid = subscribedUuid; + } + + /** + * Perform some work on the data received from the connected watch. + * + * @param context + * The BroadcastReceiver's context. + * @param transactionId + * The transaction ID of the message in which the data was received. This is required when ACK/NACKing + * the received message. + * @param data + * A dictionary of one-or-more key-value pairs received from the connected watch. + */ + public abstract void receiveData(final Context context, final int transactionId, + final PebbleDictionary data); + + /** + * {@inheritDoc} + */ + @Override + public void onReceive(final Context context, final Intent intent) { + final UUID receivedUuid = (UUID) intent.getSerializableExtra(APP_UUID); + + // Pebble-enabled apps are expected to be good citizens and only inspect broadcasts + // containing their UUID + if (!subscribedUuid.equals(receivedUuid)) { + return; + } + + final int transactionId = intent.getIntExtra(TRANSACTION_ID, -1); + final String jsonData = intent.getStringExtra(MSG_DATA); + if (jsonData == null || jsonData.isEmpty()) { + return; + } + + try { + final PebbleDictionary data = PebbleDictionary.fromJson(jsonData); + receiveData(context, transactionId, data); + } catch (JSONException e) { + e.printStackTrace(); + return; + } + } + } + + /** + * A special-purpose BroadcastReceiver that makes it easy to handle 'RECEIVE_ACK' intents broadcast from pebble + * .apk. + */ + public static abstract class PebbleAckReceiver extends BroadcastReceiver { + + /** + * The subscribed uuid. + */ + private final UUID subscribedUuid; + + /** + * Instantiates a new pebble ack receiver. + * + * @param subscribedUuid + * the subscribed uuid + */ + protected PebbleAckReceiver(final UUID subscribedUuid) { + this.subscribedUuid = subscribedUuid; + } + + /** + * Handle the ACK received from the connected watch. + * + * @param context + * The BroadcastReceiver's context. + * @param transactionId + * The transaction ID of the message for which the ACK was received. This indicates which message was + * successfully received. + */ + public abstract void receiveAck(final Context context, final int transactionId); + + /** + * {@inheritDoc} + */ + @Override + public void onReceive(final Context context, final Intent intent) { + final int transactionId = intent.getIntExtra(TRANSACTION_ID, -1); + receiveAck(context, transactionId); + + } + } + + /** + * A special-purpose BroadcastReceiver that makes it easy to handle 'RECEIVE_NACK' intents broadcast from pebble + * .apk. + */ + public static abstract class PebbleNackReceiver extends BroadcastReceiver { + + /** + * The subscribed uuid. + */ + private final UUID subscribedUuid; + + /** + * Instantiates a new pebble nack receiver. + * + * @param subscribedUuid + * the subscribed uuid + */ + protected PebbleNackReceiver(final UUID subscribedUuid) { + this.subscribedUuid = subscribedUuid; + } + + /** + * Handle the NACK received from the connected watch. + * + * @param context + * The BroadcastReceiver's context. + * @param transactionId + * The transaction ID of the message for which the NACK was received. This indicates which message was + * not received. + */ + public abstract void receiveNack(final Context context, final int transactionId); + + /** + * {@inheritDoc} + */ + @Override + public void onReceive(final Context context, final Intent intent) { + final int transactionId = intent.getIntExtra(TRANSACTION_ID, -1); + receiveNack(context, transactionId); + + } + } + + /** + * A special-purpose BroadcastReceiver that makes it easy to handle 'DATA_AVAILABLE' data logging intents broadcast from pebble.apk. + */ + public static abstract class PebbleDataLogReceiver extends BroadcastReceiver { + + /** + * The subscribed uuid. + */ + private final UUID subscribedUuid; + + /** + * The last data ID we've seen. Ignore subsequent intents for this same ID. + */ + private int lastDataId; + + /** + * Instantiates a new pebble nack receiver. + * + * @param subscribedUuid + * the subscribed uuid + */ + protected PebbleDataLogReceiver(final UUID subscribedUuid) { + this.subscribedUuid = subscribedUuid; + } + + /** + * Handle an UnsignedInteger data unit that was logged the watch and broadcast by pebble.apk. + * + * @param context + * The BroadcastReceiver's context. + * @param logUuid + * The UUID that uniquely identifies a data log. + * @param timestamp + * The timestamp when a data log was first created. + * @param tag + * The user-defined tag for the corresponding data log. + * @param data + * The unit of data that was logged on the watch. + * @throws UnsupportedOperationException + * Thrown if data is received and this handler is not implemented. + */ + public void receiveData(final Context context, UUID logUuid, + final UnsignedInteger timestamp, final UnsignedInteger tag, + final UnsignedInteger data) { + throw new UnsupportedOperationException("UnsignedInteger handler not implemented"); + + } + + /** + * Handle a byte array data unit that was logged the watch and broadcast by pebble.apk. + * + * @param context + * The BroadcastReceiver's context. + * @param logUuid + * The UUID that uniquely identifies a data log. + * @param timestamp + * The timestamp when a data log was first created. + * @param tag + * The user-defined tag for the corresponding data log. + * @param data + * The unit of data that was logged on the watch. + * @throws UnsupportedOperationException + * Thrown if data is received and this handler is not implemented. + */ + public void receiveData(final Context context, UUID logUuid, + final UnsignedInteger timestamp, final UnsignedInteger tag, + final byte[] data) { + throw new UnsupportedOperationException("Byte array handler not implemented"); + } + + /** + * Handle an int data unit that was logged the watch and broadcast by pebble.apk. + * + * @param context + * The BroadcastReceiver's context. + * @param logUuid + * The UUID that uniquely identifies a data log. + * @param timestamp + * The timestamp when a data log was first created. + * @param tag + * The user-defined tag for the corresponding data log. + * @param data + * The unit of data that was logged on the watch. + * @throws UnsupportedOperationException + * Thrown if data is received and this handler is not implemented. + */ + public void receiveData(final Context context, UUID logUuid, + final UnsignedInteger timestamp, final UnsignedInteger tag, final int data) { + throw new UnsupportedOperationException("int handler not implemented"); + + } + + /** + * Called when a session has been finished on the watch and all data has been transmitted by pebble.apk + * + * @param context + * The BroadcastReceiver's context. + * @param logUuid + * The UUID that uniquely identifies a data log. + * @param timestamp + * The timestamp when a data log was first created. + * @param tag + * The user-defined tag for the corresponding data log. + */ + public void onFinishSession(final Context context, UUID logUuid, final UnsignedInteger timestamp, + final UnsignedInteger tag) { + // Do nothing by default + } + + private void handleReceiveDataIntent(final Context context, final Intent intent, final UUID logUuid, + final UnsignedInteger timestamp, final UnsignedInteger tag) { + final int dataId = intent.getIntExtra(PBL_DATA_ID, -1); + if (dataId < 0) throw new IllegalArgumentException(); + + Log.i("pebble", "DataID: " + dataId + " LastDataID: " + lastDataId); + + if (dataId == lastDataId) { + // If we see the same dataId multiple times, just ignore it. + return; + } + + final PebbleDataType type = PebbleDataType.fromByte(intent.getByteExtra(PBL_DATA_TYPE, PebbleDataType.INVALID.ord)); + if (type == null) throw new IllegalArgumentException(); + + switch (type) { + case BYTES: + byte[] bytes = Base64.decode(intent.getStringExtra(PBL_DATA_OBJECT), Base64.NO_WRAP); + if (bytes == null) { + throw new IllegalArgumentException(); + } + + receiveData(context, logUuid, timestamp, tag, bytes); + break; + case UINT: + UnsignedInteger uint = (UnsignedInteger) intent.getSerializableExtra(PBL_DATA_OBJECT); + if (uint == null) { + throw new IllegalArgumentException(); + } + + receiveData(context, logUuid, timestamp, tag, uint); + break; + case INT: + Integer i = (Integer) intent.getSerializableExtra(PBL_DATA_OBJECT); + if (i == null) { + throw new IllegalArgumentException(); + } + + receiveData(context, logUuid, timestamp, tag, i.intValue()); + break; + default: + throw new IllegalArgumentException("Invalid type:" + type.toString()); + } + + lastDataId = dataId; + + final Intent ackIntent = new Intent(INTENT_DL_ACK_DATA); + ackIntent.putExtra(DATA_LOG_UUID, logUuid); + ackIntent.putExtra(PBL_DATA_ID, dataId); + context.sendBroadcast(ackIntent); + } + + private void handleFinishSessionIntent(final Context context, final Intent intent, final UUID logUuid, + final UnsignedInteger timestamp, final UnsignedInteger tag) { + onFinishSession(context, logUuid, timestamp, tag); + } + + /** + * {@inheritDoc} + */ + @Override + public void onReceive(final Context context, final Intent intent) { + final UUID receivedUuid = (UUID) intent.getSerializableExtra(APP_UUID); + // Pebble-enabled apps are expected to be good citizens and only inspect broadcasts + // containing their UUID + if (!subscribedUuid.equals(receivedUuid)) { + return; + } + + try { + final UUID logUuid; + final UnsignedInteger timestamp; + final UnsignedInteger tag; + + logUuid = (UUID) intent.getSerializableExtra(DATA_LOG_UUID); + if (logUuid == null) throw new IllegalArgumentException(); + + timestamp = (UnsignedInteger) intent.getSerializableExtra(DATA_LOG_TIMESTAMP); + if (timestamp == null) throw new IllegalArgumentException(); + + tag = (UnsignedInteger) intent.getSerializableExtra(DATA_LOG_TAG); + if (tag == null) throw new IllegalArgumentException(); + + if (intent.getAction() == INTENT_DL_RECEIVE_DATA) { + handleReceiveDataIntent(context, intent, logUuid, timestamp, tag); + } else if (intent.getAction() == INTENT_DL_FINISH_SESSION) { + handleFinishSessionIntent(context, intent, logUuid, timestamp, tag); + } + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + } + } + + /** + * A convenience function to assist in programatically registering a broadcast receiver for the 'DATA_AVAILABLE' + * intent. + *

+ * To avoid leaking memory, activities registering BroadcastReceivers must unregister them in the + * Activity's {@link android.app.Activity#onPause()} method. + * + * @param context + * The context in which to register the BroadcastReceiver. + * @param receiver + * The receiver to be registered. + * + * @return The registered receiver. + * + * @see Constants#INTENT_DL_RECEIVE_DATA + */ + public static BroadcastReceiver registerDataLogReceiver(final Context context, + final PebbleDataLogReceiver receiver) { + IntentFilter filter = new IntentFilter(); + filter.addAction(INTENT_DL_RECEIVE_DATA); + filter.addAction(INTENT_DL_FINISH_SESSION); + context.registerReceiver(receiver, filter); + + return receiver; + } + + /** + * A convenience function to emit an intent to pebble.apk to request the data logs for a particular app. If data + * is available, pebble.apk will advertise the data via 'INTENT_DL_RECEIVE_DATA' intents. + *

+ * To avoid leaking memory, activities registering BroadcastReceivers must unregister them in the + * Activity's {@link android.app.Activity#onPause()} method. + * + * @param context + * The context in which to register the BroadcastReceiver. + * @param appUuid + * The app for which to request data logs. + * + * @return The registered receiver. + * + * @see Constants#INTENT_DL_RECEIVE_DATA + * @see Constants#INTENT_DL_REQUEST_DATA + */ + public static void requestDataLogsForApp(final Context context, final UUID appUuid) { + final Intent requestIntent = new Intent(INTENT_DL_REQUEST_DATA); + requestIntent.putExtra(APP_UUID, appUuid); + context.sendBroadcast(requestIntent); + } + + public static class FirmwareVersionInfo { + private final int major; + private final int minor; + private final int point; + private final String tag; + + FirmwareVersionInfo(int major, int minor, int point, String tag) { + this.major = major; + this.minor = minor; + this.point = point; + this.tag = tag; + } + + public final int getMajor() { + return major; + } + + public final int getMinor() { + return minor; + } + + public final int getPoint() { + return point; + } + + public final String getTag() { + return tag; + } + } +} diff --git a/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/util/PebbleDictionary.java b/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/util/PebbleDictionary.java new file mode 100644 index 0000000..be5fa1b --- /dev/null +++ b/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/util/PebbleDictionary.java @@ -0,0 +1,370 @@ +package com.getpebble.android.kit.util; + +import android.util.Base64; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * A collection of key-value pairs of heterogeneous types. PebbleDictionaries are the primary structure used to exchange + * data between the phone and watch. + *

+ * To accommodate the mixed-types contained within a PebbleDictionary, an internal JSON representation is used when + * exchanging the dictionary between Android processes. + * + * @author zulak@getpebble.com + */ +public class PebbleDictionary implements Iterable { + + private static final String KEY = "key"; + private static final String TYPE = "type"; + private static final String LENGTH = "length"; + private static final String VALUE = "value"; + + protected final Map tuples = new HashMap(); + + /** + * {@inheritDoc} + */ + @Override + public Iterator iterator() { + return tuples.values().iterator(); + } + + /** + * Returns the number of key-value pairs in this dictionary. + * + * @return the number of key-value pairs in this dictionary + */ + public int size() { + return tuples.size(); + } + + /** + * Returns true if this dictionary contains a mapping for the specified key. + * + * @param key + * key whose presence in this dictionary is to be tested + * + * @return true if this dictionary contains a mapping for the specified key + */ + public boolean contains(final int key) { + return tuples.containsKey(key); + } + + /** + * Removes the mapping for a key from this map if it is present. + * + * @param key + * key to be removed from the dictionary + */ + public void remove(final int key) { + tuples.remove(key); + } + + /** + * Associate the specified byte array with the provided key in the dictionary. If another key-value pair with the + * same key is already present in the dictionary, it will be replaced. + * + * @param key + * key with which the specified value is associated + * @param bytes + * value to be associated with the specified key + */ + public void addBytes(int key, byte[] bytes) { + PebbleTuple t = PebbleTuple.create(key, PebbleTuple.TupleType.BYTES, PebbleTuple.Width.NONE, bytes); + addTuple(t); + } + + /** + * Associate the specified String with the provided key in the dictionary. If another key-value pair with the same + * key is already present in the dictionary, it will be replaced. + * + * @param key + * key with which the specified value is associated + * @param value + * value to be associated with the specified key + */ + public void addString(int key, String value) { + PebbleTuple t = + PebbleTuple.create(key, PebbleTuple.TupleType.STRING, PebbleTuple.Width.NONE, value); + addTuple(t); + } + + /** + * Associate the specified signed byte with the provided key in the dictionary. If another key-value pair with the + * same key is already present in the dictionary, it will be replaced. + * + * @param key + * key with which the specified value is associated + * @param b + * value to be associated with the specified key + */ + public void addInt8(final int key, final byte b) { + PebbleTuple t = PebbleTuple.create(key, PebbleTuple.TupleType.INT, PebbleTuple.Width.BYTE, b); + addTuple(t); + } + + /** + * Associate the specified unsigned byte with the provided key in the dictionary. If another key-value pair with the + * same key is already present in the dictionary, it will be replaced. + * + * @param key + * key with which the specified value is associated + * @param b + * value to be associated with the specified key + */ + public void addUint8(final int key, final byte b) { + PebbleTuple t = PebbleTuple.create(key, PebbleTuple.TupleType.UINT, PebbleTuple.Width.BYTE, b); + addTuple(t); + } + + /** + * Associate the specified signed short with the provided key in the dictionary. If another key-value pair with the + * same key is already present in the dictionary, it will be replaced. + * + * @param key + * key with which the specified value is associated + * @param s + * value to be associated with the specified key + */ + public void addInt16(final int key, final short s) { + PebbleTuple t = PebbleTuple.create(key, PebbleTuple.TupleType.INT, PebbleTuple.Width.SHORT, s); + addTuple(t); + } + + /** + * Associate the specified unsigned short with the provided key in the dictionary. If another key-value pair with + * the same key is already present in the dictionary, it will be replaced. + * + * @param key + * key with which the specified value is associated + * @param s + * value to be associated with the specified key + */ + public void addUint16(final int key, final short s) { + PebbleTuple t = PebbleTuple.create(key, PebbleTuple.TupleType.UINT, PebbleTuple.Width.SHORT, s); + addTuple(t); + } + + /** + * Associate the specified signed int with the provided key in the dictionary. If another key-value pair with the + * same key is already present in the dictionary, it will be replaced. + * + * @param key + * key with which the specified value is associated + * @param i + * value to be associated with the specified key + */ + public void addInt32(final int key, final int i) { + PebbleTuple t = PebbleTuple.create(key, PebbleTuple.TupleType.INT, PebbleTuple.Width.WORD, i); + addTuple(t); + } + + /** + * Associate the specified unsigned int with the provided key in the dictionary. If another key-value pair with the + * same key is already present in the dictionary, it will be replaced. + * + * @param key + * key with which the specified value is associated + * @param i + * value to be associated with the specified key + */ + public void addUint32(final int key, final int i) { + PebbleTuple t = PebbleTuple.create(key, PebbleTuple.TupleType.UINT, PebbleTuple.Width.WORD, i); + addTuple(t); + } + + private PebbleTuple getTuple(int key, PebbleTuple.TupleType type) { + if (!tuples.containsKey(key) || tuples.get(key) == null) { + return null; + } + + PebbleTuple t = tuples.get(key); + if (t.type != type) { + throw new PebbleDictTypeException(key, type, t.type); + } + return t; + } + + /** + * Returns the signed integer to which the specified key is mapped, or null if the key does not exist in this + * dictionary. + * + * @param key + * key whose associated value is to be returned + * + * @return value to which the specified key is mapped + */ + public Long getInteger(int key) { + PebbleTuple tuple = getTuple(key, PebbleTuple.TupleType.INT); + if (tuple == null) { + return null; + } + return (Long) tuple.value; + } + + /** + * Returns the unsigned integer to which the specified key is mapped, or null if the key does not exist in this + * dictionary. + * + * @param key + * key whose associated value is to be returned + * + * @return value to which the specified key is mapped + */ + public Long getUnsignedInteger(int key) { + PebbleTuple tuple = getTuple(key, PebbleTuple.TupleType.UINT); + if (tuple == null) { + return null; + } + return (Long) tuple.value; + } + + /** + * Returns the byte array to which the specified key is mapped, or null if the key does not exist in this + * dictionary. + * + * @param key + * key whose associated value is to be returned + * + * @return value to which the specified key is mapped + */ + public byte[] getBytes(int key) { + PebbleTuple tuple = getTuple(key, PebbleTuple.TupleType.BYTES); + if (tuple == null) { + return null; + } + return (byte[]) tuple.value; + } + + /** + * Returns the string to which the specified key is mapped, or null if the key does not exist in this dictionary. + * + * @param key + * key whose associated value is to be returned + * + * @return value to which the specified key is mapped + */ + public String getString(int key) { + PebbleTuple tuple = getTuple(key, PebbleTuple.TupleType.STRING); + if (tuple == null) { + return null; + } + return (String) tuple.value; + } + + protected void addTuple(PebbleTuple tuple) { + if (tuples.size() > 0xff) { + throw new TupleOverflowException(); + } + + tuples.put(tuple.key, tuple); + } + + public static class PebbleDictTypeException extends RuntimeException { + public PebbleDictTypeException(long key, PebbleTuple.TupleType expected, PebbleTuple.TupleType actual) { + super(String.format( + "Expected type '%s', but got '%s' for key 0x%08x", expected.name(), actual.name(), key)); + } + } + + public static class TupleOverflowException extends RuntimeException { + public TupleOverflowException() { + super("Too many tuples in dict"); + } + } + + /** + * Returns a JSON representation of this dictionary. + * + * @return a JSON representation of this dictionary + */ + public String toJsonString() { + try { + JSONArray array = new JSONArray(); + for (PebbleTuple t : tuples.values()) { + array.put(serializeTuple(t)); + } + return array.toString(); + } catch (JSONException je) { + je.printStackTrace(); + } + return null; + } + + /** + * Deserializes a JSON representation of a PebbleDictionary. + * + * @param jsonString + * the JSON representation to be deserialized + * + * @throws JSONException + * thrown if the specified JSON representation cannot be parsed + */ + public static PebbleDictionary fromJson(String jsonString) throws JSONException { + PebbleDictionary d = new PebbleDictionary(); + + JSONArray elements = new JSONArray(jsonString); + for (int idx = 0; idx < elements.length(); ++idx) { + JSONObject o = elements.getJSONObject(idx); + final int key = o.getInt(KEY); + final PebbleTuple.TupleType type = PebbleTuple.TYPE_NAMES.get(o.getString(TYPE)); + final PebbleTuple.Width width = PebbleTuple.WIDTH_MAP.get(o.getInt(LENGTH)); + + switch (type) { + case BYTES: + byte[] bytes = Base64.decode(o.getString(VALUE), Base64.NO_WRAP); + d.addBytes(key, bytes); + break; + case STRING: + d.addString(key, o.getString(VALUE)); + break; + case INT: + if (width == PebbleTuple.Width.BYTE) { + d.addInt8(key, (byte) o.getInt(VALUE)); + } else if (width == PebbleTuple.Width.SHORT) { + d.addInt16(key, (short) o.getInt(VALUE)); + } else if (width == PebbleTuple.Width.WORD) { + d.addInt32(key, o.getInt(VALUE)); + } + break; + case UINT: + if (width == PebbleTuple.Width.BYTE) { + d.addUint8(key, (byte) o.getInt(VALUE)); + } else if (width == PebbleTuple.Width.SHORT) { + d.addUint16(key, (short) o.getInt(VALUE)); + } else if (width == PebbleTuple.Width.WORD) { + d.addUint32(key, o.getInt(VALUE)); + } + break; + } + } + + return d; + } + + private static JSONObject serializeTuple(PebbleTuple t) throws JSONException { + JSONObject j = new JSONObject(); + j.put(KEY, t.key); + j.put(TYPE, t.type.getName()); + j.put(LENGTH, t.width.value); + + switch (t.type) { + case BYTES: + j.put(VALUE, Base64.encodeToString((byte[]) t.value, Base64.NO_WRAP)); + break; + case STRING: + case INT: + case UINT: + j.put(VALUE, t.value); + break; + } + + return j; + } +} diff --git a/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/util/PebbleTuple.java b/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/util/PebbleTuple.java new file mode 100644 index 0000000..9588afa --- /dev/null +++ b/pEBBLE_KIT/src/main/java/com/getpebble/android/kit/util/PebbleTuple.java @@ -0,0 +1,121 @@ +package com.getpebble.android.kit.util; + +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +/** + * A key-value pair stored in a {@link PebbleDictionary}. + * + * @author zulak@getpebble.com + */ +final class PebbleTuple { + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + static final Map TYPE_NAMES = new HashMap(); + + static { + for (TupleType t : TupleType.values()) { + TYPE_NAMES.put(t.getName(), t); + } + } + + static final Map WIDTH_MAP = new HashMap(); + + static { + for (Width w : Width.values()) { + WIDTH_MAP.put(w.value, w); + } + } + + /** + * The integer key identifying the tuple. + */ + public final int key; + /** + * The type of value contained in the tuple. + */ + public final TupleType type; + /** + * The 'width' of the tuple's value; This value will always be 'NONE' for non-integer types. + */ + public final Width width; + /** + * The length of the tuple's value in bytes. + */ + public final int length; + /** + * The value being associated with the tuple's key. + */ + public final Object value; + + private PebbleTuple(final int key, final TupleType type, final Width width, final int length, final Object value) { + this.key = key; + this.type = type; + this.width = width; + this.length = length; + this.value = value; + } + + static PebbleTuple create( + final int key, final TupleType type, final Width width, final int value) { + return new PebbleTuple(key, type, width, width.value, Long.valueOf(value)); + } + + static PebbleTuple create( + final int key, final TupleType type, final Width width, final Object value) { + + int length = Integer.MAX_VALUE; + if (width != Width.NONE) { + length = width.value; + } else if (type == TupleType.BYTES) { + length = ((byte[]) value).length; + } else if (type == TupleType.STRING) { + length = ((String) value).getBytes(UTF8).length; + } + + if (length > 0xffff) { + throw new ValueOverflowException(); + } + + return new PebbleTuple(key, type, width, length, value); + } + + public static class ValueOverflowException extends RuntimeException { + public ValueOverflowException() { + super("Value exceeds tuple capacity"); + } + } + + static enum Width { + NONE(0), + BYTE(1), + SHORT(2), + WORD(4); + + public final int value; + + private Width(final int width) { + value = width; + } + } + + static enum TupleType { + BYTES(0), + STRING(1), + UINT(2), + INT(3); + + public final byte ord; + + private TupleType(int ord) { + this.ord = (byte) ord; + } + + public String getName() { + return name().toLowerCase(); + } + } +} + diff --git a/pEBBLE_KIT/src/main/res/layout/main.xml b/pEBBLE_KIT/src/main/res/layout/main.xml new file mode 100644 index 0000000..553d772 --- /dev/null +++ b/pEBBLE_KIT/src/main/res/layout/main.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/pEBBLE_KIT/src/main/res/values/strings.xml b/pEBBLE_KIT/src/main/res/values/strings.xml new file mode 100644 index 0000000..9e56477 --- /dev/null +++ b/pEBBLE_KIT/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + PEBBLE_SPORTS_SDK + diff --git a/settings.gradle b/settings.gradle index 6a4e79f..758475e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':mobile', ':wear' +include ':pEBBLE_KIT'