diff --git a/.travis.yml b/.travis.yml index c65a9c3..6d11e4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,11 @@ language: android jdk: oraclejdk8 before_install: - - wget http://services.gradle.org/distributions/gradle-2.14.1-bin.zip - - unzip gradle-2.14.1-bin.zip - - export GRADLE_HOME=$PWD/gradle-2.14.1 + - wget http://services.gradle.org/distributions/gradle-3.3-bin.zip + - unzip gradle-3.3-bin.zip + - export GRADLE_HOME=$PWD/gradle-3.3 - export PATH=$GRADLE_HOME/bin:$PATH - - ( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk -a --no-ui --filter tool,platform-tool,build-tools-25.0.0 + - ( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk -a --no-ui --filter tool,platform-tool,build-tools-25.0.2 script: - gradle assembleDebug diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 3b7277a..45147a5 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,13 +1,18 @@ - + - + + + + diff --git a/assets/html/passive_data_kit/generator_location_disclosure.html b/assets/html/passive_data_kit/generator_location_disclosure.html new file mode 100755 index 0000000..583b4d7 --- /dev/null +++ b/assets/html/passive_data_kit/generator_location_disclosure.html @@ -0,0 +1,26 @@ + + + + + + + +

+ TODO: Write disclosure for use of location data... +

+ +

+ Place completed file in assets/html/passive_data_kit/generator_location_disclosure.html in your project to override this placeholder. +

+ + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 22ab1c0..7a80bca 100755 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.2.2' + classpath 'com.android.tools.build:gradle:2.3.0' } } @@ -23,7 +23,7 @@ repositories { android { compileSdkVersion 25 - buildToolsVersion "25" + buildToolsVersion "25.0.2" useLibrary 'org.apache.http.legacy' @@ -51,25 +51,22 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) // testCompile 'junit:junit:4.12' - compile 'com.android.support:appcompat-v7:25.0.1' - compile 'com.android.support:recyclerview-v7:25.0.1' - compile 'com.android.support:cardview-v7:25.0.1' - compile 'com.google.android.gms:play-services-location:9.8.0' - compile 'com.google.android.gms:play-services-maps:9.8.0' - compile 'com.google.android.gms:play-services-nearby:9.8.0' - compile 'com.google.android.gms:play-services-places:9.8.0' - compile 'com.google.android.gms:play-services-awareness:9.8.0' + compile 'com.android.support:appcompat-v7:25.3.0' + compile 'com.android.support:recyclerview-v7:25.3.0' + compile 'com.android.support:cardview-v7:25.3.0' + compile 'com.google.android.gms:play-services-location:10.2.0' + compile 'com.google.android.gms:play-services-maps:10.2.0' + compile 'com.google.android.gms:play-services-nearby:10.2.0' + compile 'com.google.android.gms:play-services-places:10.2.0' + compile 'com.google.android.gms:play-services-awareness:10.2.0' + compile 'com.android.support:customtabs:23.3.0' + compile 'com.google.maps.android:android-maps-utils:0.4' compile 'com.squareup.okhttp3:okhttp:3.2.0' compile 'commons-io:commons-io:2.4' compile 'commons-codec:commons-codec:1.10' compile 'org.apache.commons:commons-lang3:3.4' compile 'com.fasterxml.jackson.core:jackson-core:2.7.3' - compile 'com.github.philjay:mpandroidchart:v3.0.0' - compile 'com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7' - - compile project(':libraries-base') - compile project(':libraries-common') - compile project(':libraries-controller') + compile 'com.github.philjay:mpandroidchart:v3.0.1' } buildTypes { @@ -78,4 +75,4 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } -} \ No newline at end of file +} diff --git a/gvr-android-sdk b/gvr-android-sdk deleted file mode 160000 index 3360cbd..0000000 --- a/gvr-android-sdk +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3360cbd21d8ba07c305fa379b905ff8c7632f6cc diff --git a/res/drawable-hdpi/ic_button_disclosure_setting.png b/res/drawable-hdpi/ic_button_disclosure_setting.png new file mode 100755 index 0000000..9628f97 Binary files /dev/null and b/res/drawable-hdpi/ic_button_disclosure_setting.png differ diff --git a/res/drawable-hdpi/ic_pdk_diagnostic.png b/res/drawable-hdpi/ic_pdk_diagnostic.png new file mode 100755 index 0000000..e80247a Binary files /dev/null and b/res/drawable-hdpi/ic_pdk_diagnostic.png differ diff --git a/res/drawable-hdpi/ic_text_messages.png b/res/drawable-hdpi/ic_text_messages.png new file mode 100755 index 0000000..ef5d391 Binary files /dev/null and b/res/drawable-hdpi/ic_text_messages.png differ diff --git a/res/drawable-hdpi/ic_withings_device.png b/res/drawable-hdpi/ic_withings_device.png new file mode 100755 index 0000000..168df46 Binary files /dev/null and b/res/drawable-hdpi/ic_withings_device.png differ diff --git a/res/drawable-mdpi/ic_button_disclosure_setting.png b/res/drawable-mdpi/ic_button_disclosure_setting.png new file mode 100755 index 0000000..a252c98 Binary files /dev/null and b/res/drawable-mdpi/ic_button_disclosure_setting.png differ diff --git a/res/drawable-mdpi/ic_pdk_diagnostic.png b/res/drawable-mdpi/ic_pdk_diagnostic.png new file mode 100755 index 0000000..539cf8a Binary files /dev/null and b/res/drawable-mdpi/ic_pdk_diagnostic.png differ diff --git a/res/drawable-mdpi/ic_text_messages.png b/res/drawable-mdpi/ic_text_messages.png new file mode 100755 index 0000000..cdabac0 Binary files /dev/null and b/res/drawable-mdpi/ic_text_messages.png differ diff --git a/res/drawable-mdpi/ic_withings_device.png b/res/drawable-mdpi/ic_withings_device.png new file mode 100755 index 0000000..5b184d8 Binary files /dev/null and b/res/drawable-mdpi/ic_withings_device.png differ diff --git a/res/drawable-xhdpi/ic_button_disclosure_setting.png b/res/drawable-xhdpi/ic_button_disclosure_setting.png new file mode 100755 index 0000000..c006523 Binary files /dev/null and b/res/drawable-xhdpi/ic_button_disclosure_setting.png differ diff --git a/res/drawable-xhdpi/ic_pdk_diagnostic.png b/res/drawable-xhdpi/ic_pdk_diagnostic.png new file mode 100755 index 0000000..c9ebaa0 Binary files /dev/null and b/res/drawable-xhdpi/ic_pdk_diagnostic.png differ diff --git a/res/drawable-xhdpi/ic_text_messages.png b/res/drawable-xhdpi/ic_text_messages.png new file mode 100755 index 0000000..a0a8ceb Binary files /dev/null and b/res/drawable-xhdpi/ic_text_messages.png differ diff --git a/res/drawable-xhdpi/ic_withings_device.png b/res/drawable-xhdpi/ic_withings_device.png new file mode 100755 index 0000000..73aece2 Binary files /dev/null and b/res/drawable-xhdpi/ic_withings_device.png differ diff --git a/res/drawable-xxhdpi/ic_button_disclosure_setting.png b/res/drawable-xxhdpi/ic_button_disclosure_setting.png new file mode 100755 index 0000000..8310d1e Binary files /dev/null and b/res/drawable-xxhdpi/ic_button_disclosure_setting.png differ diff --git a/res/drawable-xxhdpi/ic_pdk_diagnostic.png b/res/drawable-xxhdpi/ic_pdk_diagnostic.png new file mode 100755 index 0000000..62b411d Binary files /dev/null and b/res/drawable-xxhdpi/ic_pdk_diagnostic.png differ diff --git a/res/drawable-xxhdpi/ic_text_messages.png b/res/drawable-xxhdpi/ic_text_messages.png new file mode 100755 index 0000000..31ceb29 Binary files /dev/null and b/res/drawable-xxhdpi/ic_text_messages.png differ diff --git a/res/drawable-xxhdpi/ic_withings_device.png b/res/drawable-xxhdpi/ic_withings_device.png new file mode 100755 index 0000000..5dd8a9d Binary files /dev/null and b/res/drawable-xxhdpi/ic_withings_device.png differ diff --git a/res/drawable-xxxhdpi/ic_button_disclosure_setting.png b/res/drawable-xxxhdpi/ic_button_disclosure_setting.png new file mode 100755 index 0000000..b25919e Binary files /dev/null and b/res/drawable-xxxhdpi/ic_button_disclosure_setting.png differ diff --git a/res/drawable-xxxhdpi/ic_pdk_diagnostic.png b/res/drawable-xxxhdpi/ic_pdk_diagnostic.png new file mode 100755 index 0000000..ed16022 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_pdk_diagnostic.png differ diff --git a/res/drawable-xxxhdpi/ic_text_messages.png b/res/drawable-xxxhdpi/ic_text_messages.png new file mode 100755 index 0000000..352d5d5 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_text_messages.png differ diff --git a/res/drawable-xxxhdpi/ic_withings_device.png b/res/drawable-xxxhdpi/ic_withings_device.png new file mode 100755 index 0000000..2803e06 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_withings_device.png differ diff --git a/res/drawable/ic_location_heatmap_marker.xml b/res/drawable/ic_location_heatmap_marker.xml new file mode 100755 index 0000000..1df13e4 --- /dev/null +++ b/res/drawable/ic_location_heatmap_marker.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/res/layout/card_diagnostic_action.xml b/res/layout/card_diagnostic_action.xml index a3274e1..acfd651 100755 --- a/res/layout/card_diagnostic_action.xml +++ b/res/layout/card_diagnostic_action.xml @@ -1,20 +1,41 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - + + android:layout_height="wrap_content"> + + + + \ No newline at end of file diff --git a/res/layout/card_generator_location_google.xml b/res/layout/card_generator_location_google.xml index 716536d..979c932 100755 --- a/res/layout/card_generator_location_google.xml +++ b/res/layout/card_generator_location_google.xml @@ -2,6 +2,7 @@ @@ -42,24 +43,29 @@ android:textColor="@android:color/white" android:layout_marginRight="8dp"/> - + android:layout_height="200dp"> - + - + android:layout_gravity="bottom|right" + android:layout_marginRight="6dp" + android:layout_marginBottom="104dp" /> + \ No newline at end of file diff --git a/res/layout/card_generator_phone_calls.xml b/res/layout/card_generator_phone_calls.xml index f66de04..2a2725b 100755 --- a/res/layout/card_generator_phone_calls.xml +++ b/res/layout/card_generator_phone_calls.xml @@ -41,7 +41,8 @@ android:textColor="@android:color/white" android:layout_marginRight="8dp"/> - @@ -150,6 +151,12 @@ android:layout_marginBottom="8dp"/> + \ No newline at end of file diff --git a/res/layout/card_generator_screen_state.xml b/res/layout/card_generator_screen_state.xml index f09f805..7af3c2d 100755 --- a/res/layout/card_generator_screen_state.xml +++ b/res/layout/card_generator_screen_state.xml @@ -41,7 +41,8 @@ android:textColor="@android:color/white" android:layout_marginRight="8dp"/> - @@ -138,6 +139,12 @@ + \ No newline at end of file diff --git a/res/layout/card_generator_text_messages.xml b/res/layout/card_generator_text_messages.xml new file mode 100755 index 0000000..03c022d --- /dev/null +++ b/res/layout/card_generator_text_messages.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/card_generator_withings_device.xml b/res/layout/card_generator_withings_device.xml new file mode 100755 index 0000000..5eb8c89 --- /dev/null +++ b/res/layout/card_generator_withings_device.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/dialog_location_randomized.xml b/res/layout/dialog_location_randomized.xml new file mode 100755 index 0000000..58965d7 --- /dev/null +++ b/res/layout/dialog_location_randomized.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/res/layout/dialog_location_user.xml b/res/layout/dialog_location_user.xml new file mode 100755 index 0000000..17bbf31 --- /dev/null +++ b/res/layout/dialog_location_user.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/res/layout/layout_data_disclosure_detail_pdk.xml b/res/layout/layout_data_disclosure_detail_pdk.xml new file mode 100755 index 0000000..9cd2e4e --- /dev/null +++ b/res/layout/layout_data_disclosure_detail_pdk.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/res/layout/layout_data_disclosure_pdk.xml b/res/layout/layout_data_disclosure_pdk.xml new file mode 100755 index 0000000..cc763d0 --- /dev/null +++ b/res/layout/layout_data_disclosure_pdk.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/res/layout/layout_diagnostics_pdk.xml b/res/layout/layout_diagnostics_pdk.xml index e71a783..379c418 100755 --- a/res/layout/layout_diagnostics_pdk.xml +++ b/res/layout/layout_diagnostics_pdk.xml @@ -1,7 +1,16 @@ - + android:layout_height="match_parent"> + + + diff --git a/res/layout/row_disclosure_action_pdk.xml b/res/layout/row_disclosure_action_pdk.xml new file mode 100755 index 0000000..a5250e3 --- /dev/null +++ b/res/layout/row_disclosure_action_pdk.xml @@ -0,0 +1,19 @@ + + + + diff --git a/res/layout/row_disclosure_location_accuracy_pdk.xml b/res/layout/row_disclosure_location_accuracy_pdk.xml new file mode 100755 index 0000000..78cd4cc --- /dev/null +++ b/res/layout/row_disclosure_location_accuracy_pdk.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/res/layout/row_generator_disclosure_generic.xml b/res/layout/row_generator_disclosure_generic.xml new file mode 100755 index 0000000..200f44e --- /dev/null +++ b/res/layout/row_generator_disclosure_generic.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/res/menu/diagnostic_menu.xml b/res/menu/diagnostic_menu.xml new file mode 100755 index 0000000..65f394a --- /dev/null +++ b/res/menu/diagnostic_menu.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/res/values/databases.xml b/res/values/databases.xml new file mode 100755 index 0000000..02d98e8 --- /dev/null +++ b/res/values/databases.xml @@ -0,0 +1,26 @@ + + CREATE TABLE metadata(key TEXT, value TEXT, last_updated INTEGER); + + + CREATE TABLE history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, latitude REAL, longitude REAL, altitude REAL, bearing REAL, speed REAL, provider TEXT, location_timestamp INTEGER, accuracy REAL); + + + CREATE TABLE history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, state TEXT); + + + CREATE TABLE history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, duration INTEGER, call_type TEXT, number TEXT, post_dial_digits TEXT, via_number TEXT, is_new INTEGER, pulled_externally INTEGER, country_iso TEXT, data_usage INTEGER, geocoded_location TEXT, is_video INTEGER, presentation TEXT, is_read INTEGER); + + + CREATE TABLE history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, direction TEXT, length INTEGER, body TEXT, number_name TEXT, number TEXT); + + + CREATE TABLE history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, event_name TEXT, event_details TEXT); + + + CREATE TABLE activity_measure_history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, date_start INTEGER, timezone TEXT, steps REAL, distance REAL, active_calories REAL, total_calories REAL, elevation REAL, soft_activity_duration REAL, moderate_activity_duration REAL, intense_activity_duration REAL); + CREATE TABLE body_measure_history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, measure_date INTEGER, measure_status TEXT, measure_category TEXT, measure_type TEXT, measure_value REAL); + CREATE TABLE intraday_activity_history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, activity_start REAL, activity_duration REAL, calories REAL, distance REAL, elevation_climbed REAL, steps REAL, swim_strokes REAL, pool_laps REAL); + CREATE TABLE sleep_measure_history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, start_date REAL, end_date REAL, state TEXT, measurement_device TEXT); + CREATE TABLE sleep_summary_history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, start_date REAL, end_date REAL, timezone TEXT, measurement_device TEXT, wake_duration REAL, light_sleep_duration REAL, deep_sleep_duration REAL, rem_sleep_duration REAL, wake_count INTEGER, to_sleep_duration REAL, to_wake_duration REAL); + CREATE TABLE workout_history(_id INTEGER PRIMARY KEY AUTOINCREMENT, fetched INTEGER, transmitted INTEGER, observed INTEGER, start_date REAL, end_date REAL, measurement_device TEXT, workout_category TEXT, caolories REAL, effective_duration REAL, raw_data TEXT); + diff --git a/res/values/diagnostics.xml b/res/values/diagnostics.xml index c0adb6a..026fce6 100755 --- a/res/values/diagnostics.xml +++ b/res/values/diagnostics.xml @@ -1,7 +1,23 @@ - Unable to connect to Microsoft Band. + Microsoft Band App Not Installed + Unable to connect to Microsoft Band. Please install the app and try again. + + Microsoft Band Permission Required Please grant the app sensor access on the Microsoft Band. - Please grant the app permission to access location services on this device. + + Activity Recognition Permission Required Please grant the app permission to recognize your activity using data from this device. + + Call Log Permission Required Please grant the app permission to access the phone call logs on this device. + + Text Messaging Permission Required + Please all the app permission to access your text messages to gather and report your messaging activity statistics. + + Location Permission Required + Please grant the app permission to access location services on this device. + + Access to Withing Account Required + Permission is required to fetch data from the Withings server. Please grant this application the requested permissions. + diff --git a/res/values/generators.xml b/res/values/generators.xml index 166f4b6..934f041 100755 --- a/res/values/generators.xml +++ b/res/values/generators.xml @@ -3,9 +3,11 @@ com.audacious_software.passive_data_kit.generators.device.Location com.audacious_software.passive_data_kit.generators.device.ScreenState com.audacious_software.passive_data_kit.generators.communication.PhoneCalls - com.audacious_software.passive_data_kit.generators.wearables.MicrosoftBand - com.audacious_software.passive_data_kit.generators.services.GoogleAwareness - com.audacious_software.passive_data_kit.generators.vr.DaydreamViewController + com.audacious_software.passive_data_kit.generators.communication.TextMessages + + + + com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice @@ -43,9 +45,59 @@ Device Location Coordinates: %1$.4f, %2$.4f + Location Accuracy + Tap here to update the accuracy of your data. + + Best Accuracy + Best Available Data From Location Hardware + + Locally Randomized + Location Combined With Random Noise + + User Provided + Static Location Provided By User + + Disabled + App Does Not Use Location Data + + Best Accuracy + This app will use the location hardware on this device to obtain the most accurate location readings. + + Location Disabled + This app will not use your location, but use an placeholder instead. + + Locally Randomized + Please enter the random distance (kilometers) to use to obfuscate your exact location: + + User Provided + Please enter a postal code or city and province to use as your location: + Unable to find your location. Please enter your location another way and try again. Screen State + On + Off + Doze + Unknown + Legend: + No screen state changes have been observed yet. + + + SMS Text Messages + Incoming + Outgoing + Other + + #3F51B5 + #4CAF50 + #9E9E9E + + Latest Text Message + Length + Direction + %1$d chars. + + No text messages have been sent or received on this device. Phone Calls @@ -64,4 +116,8 @@ Direction %1$.2f min. + No phone calls have been made or received on this device. + + + Withings Device \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 61e918e..73b220f 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4,6 +4,7 @@ 0.0.1 Unknown Version Data Stream + Continue %d generator @@ -14,10 +15,17 @@ Today - On - Off - Doze - Unknown - Legend: + Never + Diagnostics + Diagnostics (%d) + Diagnostics + + The app is set up correctly.\n\nNo further actions are needed. + + Passive Data Disclosure + Select a disclosure item for more information… + + Data Collection Description + Tap here to learn how this app uses your data. diff --git a/src/com/audacious_software/passive_data_kit/Logger.java b/src/com/audacious_software/passive_data_kit/Logger.java index b315574..9ea620e 100755 --- a/src/com/audacious_software/passive_data_kit/Logger.java +++ b/src/com/audacious_software/passive_data_kit/Logger.java @@ -2,7 +2,10 @@ import android.content.Context; +import com.audacious_software.passive_data_kit.generators.diagnostics.AppEvent; + import java.util.HashMap; +import java.util.Map; /** * Created by cjkarr on 4/3/2016. @@ -10,8 +13,12 @@ public class Logger { private Context mContext = null; - public void log(String event, HashMap details) { - // TODO + public void log(String event, Map details) { + if (details == null) { + details = new HashMap<>(); + } + + AppEvent.getInstance(this.mContext).logEvent(event, details); } private static class LoggerHolder { diff --git a/src/com/audacious_software/passive_data_kit/PassiveDataKit.java b/src/com/audacious_software/passive_data_kit/PassiveDataKit.java index 8a8efc1..285f97e 100755 --- a/src/com/audacious_software/passive_data_kit/PassiveDataKit.java +++ b/src/com/audacious_software/passive_data_kit/PassiveDataKit.java @@ -5,9 +5,13 @@ import com.audacious_software.passive_data_kit.diagnostics.DiagnosticAction; import com.audacious_software.passive_data_kit.generators.Generators; +import java.io.File; import java.util.ArrayList; public class PassiveDataKit { + private static final String STORAGE_PATH = "passive-data-kit"; + private static final String GENERATORS_PATH = "generators"; + private Context mContext = null; private boolean mStarted = false; @@ -30,6 +34,17 @@ public static ArrayList diagnostics(Context context) return actions; } + public static File getGeneratorsStorage(Context context) { + File path = new File(context.getFilesDir(), PassiveDataKit.STORAGE_PATH); + path = new File(path, PassiveDataKit.GENERATORS_PATH); + + if (path.exists() == false) { + path.mkdirs(); + } + + return path; + } + private static class PassiveDataKitHolder { public static PassiveDataKit instance = new PassiveDataKit(); } diff --git a/src/com/audacious_software/passive_data_kit/PhoneUtililties.java b/src/com/audacious_software/passive_data_kit/PhoneUtililties.java new file mode 100755 index 0000000..0213eb1 --- /dev/null +++ b/src/com/audacious_software/passive_data_kit/PhoneUtililties.java @@ -0,0 +1,30 @@ +package com.audacious_software.passive_data_kit; + +import android.content.Context; + +/** + * Created by cjkarr on 12/13/2016. + */ + +public class PhoneUtililties { + public static String normalizedPhoneNumber(String phoneNumber) + { + if (phoneNumber == null) { + return null; + } + + phoneNumber = phoneNumber.replaceAll("[^\\d.]", ""); + + while (phoneNumber.length() > 10) { + phoneNumber = phoneNumber.substring(1); + } + + while (phoneNumber.length() < 10) { + phoneNumber += "0"; + } + + + return phoneNumber; + } +} + diff --git a/src/com/audacious_software/passive_data_kit/activities/DataDisclosureActivity.java b/src/com/audacious_software/passive_data_kit/activities/DataDisclosureActivity.java new file mode 100755 index 0000000..70d41f3 --- /dev/null +++ b/src/com/audacious_software/passive_data_kit/activities/DataDisclosureActivity.java @@ -0,0 +1,44 @@ +package com.audacious_software.passive_data_kit.activities; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.MenuItem; +import android.widget.FrameLayout; + +import com.audacious_software.passive_data_kit.activities.generators.GeneratorsAdapter; +import com.audacious_software.pdk.passivedatakit.R; + +public class DataDisclosureActivity extends AppCompatActivity { + private GeneratorsAdapter mAdapter = null; + + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.layout_data_disclosure_pdk); + this.getSupportActionBar().setTitle(R.string.title_data_disclosure); + this.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + FrameLayout dataView = (FrameLayout) this.findViewById(R.id.data_view); + + this.mAdapter = new GeneratorsAdapter(); + this.mAdapter.setContext(this.getApplicationContext()); + this.mAdapter.setDataView(dataView); + + RecyclerView listView = (RecyclerView) this.findViewById(R.id.list_view); + + listView.setLayoutManager(new LinearLayoutManager(this)); + + listView.setAdapter(this.mAdapter); + } + + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == android.R.id.home) + { + this.finish(); + } + + return true; + } +} diff --git a/src/com/audacious_software/passive_data_kit/activities/DataDisclosureDetailActivity.java b/src/com/audacious_software/passive_data_kit/activities/DataDisclosureDetailActivity.java new file mode 100755 index 0000000..aaf79bb --- /dev/null +++ b/src/com/audacious_software/passive_data_kit/activities/DataDisclosureDetailActivity.java @@ -0,0 +1,123 @@ +package com.audacious_software.passive_data_kit.activities; + +import android.content.Context; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.FrameLayout; +import android.widget.ListView; +import android.widget.TextView; + +import com.audacious_software.passive_data_kit.Logger; +import com.audacious_software.passive_data_kit.generators.Generator; +import com.audacious_software.pdk.passivedatakit.R; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +public class DataDisclosureDetailActivity extends AppCompatActivity { + public static class Action { + public String title; + public String subtitle; + + public View view; + } + + public static final String GENERATOR_CLASS_NAME = "com.audacious_software.passive_data_kit.activities.DataDisclosureDetailActivity.GENERATOR_CLASS_NAME"; + + private Class mGeneratorClass = null; + + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final DataDisclosureDetailActivity me = this; + + this.setContentView(R.layout.layout_data_disclosure_detail_pdk); + this.getSupportActionBar().setSubtitle(R.string.title_data_disclosure); + this.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + try { + this.mGeneratorClass = (Class) Class.forName(this.getIntent().getStringExtra(DataDisclosureDetailActivity.GENERATOR_CLASS_NAME)); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + + if (this.mGeneratorClass != null) { + try { + Method getGeneratorTitle = this.mGeneratorClass.getDeclaredMethod("getGeneratorTitle", Context.class); + + String title = (String) getGeneratorTitle.invoke(null, this); + this.getSupportActionBar().setTitle(title); + + Method getDisclosureActions = this.mGeneratorClass.getDeclaredMethod("getDisclosureActions", Context.class); + + final List actions = (List) getDisclosureActions.invoke(null, this); + + ListView actionsList = (ListView) this.findViewById(R.id.disclosure_actions); + ArrayAdapter adapter = new ArrayAdapter(this, R.layout.row_disclosure_action_pdk, actions) { + public View getView (int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(me).inflate(R.layout.row_disclosure_action_pdk, null); + } + + Action action = actions.get(position); + + TextView title = (TextView) convertView.findViewById(R.id.action_title); + title.setText(action.title); + + TextView description = (TextView) convertView.findViewById(R.id.action_description); + description.setText(action.subtitle); + + return convertView; + } + }; + + actionsList.setAdapter(adapter); + + actionsList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long l) { + Log.e("PDK", "TAPPED: " + position); + + Action action = actions.get(position); + + FrameLayout dataView = (FrameLayout) me.findViewById(R.id.data_view); + dataView.removeAllViews(); + + if (action.view != null) { + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + + action.view.setLayoutParams(params); + + dataView.addView(action.view); + } + } + }); + + actionsList.performItemClick(null, 0, 0); + } catch (NoSuchMethodException e1) { + Logger.getInstance(this).logThrowable(e1); + } catch (InvocationTargetException e1) { + Logger.getInstance(this).logThrowable(e1); + } catch (IllegalAccessException e1) { + Logger.getInstance(this).logThrowable(e1); + } + } + } + + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + this.finish(); + } + + return true; + } + +} diff --git a/src/com/audacious_software/passive_data_kit/activities/DataStreamActivity.java b/src/com/audacious_software/passive_data_kit/activities/DataStreamActivity.java index 184173a..3e80bb0 100755 --- a/src/com/audacious_software/passive_data_kit/activities/DataStreamActivity.java +++ b/src/com/audacious_software/passive_data_kit/activities/DataStreamActivity.java @@ -6,21 +6,14 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import com.audacious_software.passive_data_kit.PassiveDataKit; import com.audacious_software.passive_data_kit.activities.generators.DataPointsAdapter; -import com.audacious_software.passive_data_kit.diagnostics.DiagnosticAction; import com.audacious_software.passive_data_kit.generators.Generators; import com.audacious_software.pdk.passivedatakit.R; import java.util.ArrayList; -public class DataStreamActivity extends AppCompatActivity implements Generators.NewDataPointListener { +public class DataStreamActivity extends AppCompatActivity implements Generators.GeneratorUpdatedListener { private DataPointsAdapter mAdapter = null; protected void onCreate(Bundle savedInstanceState) { @@ -30,6 +23,7 @@ protected void onCreate(Bundle savedInstanceState) { this.getSupportActionBar().setSubtitle(this.getResources().getQuantityString(R.plurals.activity_data_stream_subtitle, 0, 0)); this.mAdapter = new DataPointsAdapter(); + this.mAdapter.setContext(this.getApplicationContext()); RecyclerView listView = (RecyclerView) this.findViewById(R.id.list_view); @@ -41,30 +35,37 @@ protected void onCreate(Bundle savedInstanceState) { protected void onResume() { super.onResume(); - Generators.getInstance(this).addNewDataPointListener(this); + Generators.getInstance(this).addNewGeneratorUpdatedListener(this); - Generators.getInstance(this).broadcastLatestDataPoints(); + final int count = this.mAdapter.getItemCount(); + + Handler mainHandler = new Handler(Looper.getMainLooper()); + + final DataStreamActivity me = this; + + mainHandler.post(new Runnable() { + @Override + public void run() { + me.getSupportActionBar().setSubtitle(me.getResources().getQuantityString(R.plurals.activity_data_stream_subtitle, count, count)); + } + }); } protected void onPause() { super.onPause(); - Generators.getInstance(this).removeNewDataPointListener(this); + Generators.getInstance(this).removeGeneratorUpdatedListener(this); } @Override - public void onNewDataPoint(String identifier, Bundle data) { - this.mAdapter.updateDataPoint(identifier, data); - - final int count = this.mAdapter.getItemCount(); - - Handler mainHandler = new Handler(Looper.getMainLooper()); - + public void onGeneratorUpdated(String identifier, long timestamp, Bundle data) { final DataStreamActivity me = this; - mainHandler.post(new Runnable() { + this.runOnUiThread(new Runnable() { @Override public void run() { + me.mAdapter.notifyDataSetChanged(); + int count = me.mAdapter.getItemCount(); me.getSupportActionBar().setSubtitle(me.getResources().getQuantityString(R.plurals.activity_data_stream_subtitle, count, count)); } }); diff --git a/src/com/audacious_software/passive_data_kit/activities/DiagnosticsActivity.java b/src/com/audacious_software/passive_data_kit/activities/DiagnosticsActivity.java index ce8a3d5..851103d 100755 --- a/src/com/audacious_software/passive_data_kit/activities/DiagnosticsActivity.java +++ b/src/com/audacious_software/passive_data_kit/activities/DiagnosticsActivity.java @@ -1,11 +1,14 @@ package com.audacious_software.passive_data_kit.activities; +import android.app.Activity; +import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; @@ -17,6 +20,38 @@ import java.util.ArrayList; public class DiagnosticsActivity extends AppCompatActivity { + public static void setUpDiagnositicsItem(Activity activity, Menu menu, boolean showAction) { + final ArrayList actions = PassiveDataKit.diagnostics(activity); + + MenuItem item = menu.add(Menu.NONE, R.id.action_diagnostics, 0, activity.getString(R.string.action_diagnostics)); + + if (actions.size() > 0 && showAction) { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + + item.setIcon(R.drawable.ic_pdk_diagnostic); + item.setTitle("" + actions.size()); + } else { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + + if (actions.size() > 0) { + item.setTitle(activity.getString(R.string.action_diagnostics_incomplete, actions.size())); + } + } + } + + public static boolean diagnosticItemSelected(Activity activity, MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.action_diagnostics) { + Intent diagnosticsIntent = new Intent(activity, DiagnosticsActivity.class); + activity.startActivity(diagnosticsIntent); + + return true; + } + + return false; + } + private class DiagnosticViewHolder extends RecyclerView.ViewHolder { private View mView = null; @@ -44,7 +79,10 @@ public void bindDiagnosticAction(DiagnosticAction action) { this.mAction = action; - TextView message = (TextView) this.mView.findViewById(R.id.message_action); + TextView title = (TextView) this.mView.findViewById(R.id.action_title); + title.setText(action.getTitle()); + + TextView message = (TextView) this.mView.findViewById(R.id.action_message); message.setText(action.getMessage()); } } @@ -52,34 +90,46 @@ public void bindDiagnosticAction(DiagnosticAction action) protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.layout_diagnostics_pdk); + this.getSupportActionBar().setTitle(R.string.title_pdk_diagnostics); + } - final ArrayList actions = PassiveDataKit.diagnostics(this); + protected void onResume() { + super.onResume(); - Log.e("PDK", "ACTIONS COUNT: " + actions.size()); + final ArrayList actions = PassiveDataKit.diagnostics(this); RecyclerView listView = (RecyclerView) this.findViewById(R.id.list_view); + TextView emptyMessage = (TextView) this.findViewById(R.id.message_no_diagnostics); - listView.setLayoutManager(new LinearLayoutManager(this)); + if (actions.size() > 0) { + listView.setVisibility(View.VISIBLE); + emptyMessage.setVisibility(View.GONE); - listView.setAdapter(new RecyclerView.Adapter() { - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_diagnostic_action, parent, false); + listView.setLayoutManager(new LinearLayoutManager(this)); - return new DiagnosticViewHolder(v); - } + listView.setAdapter(new RecyclerView.Adapter() { + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_diagnostic_action, parent, false); - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - DiagnosticViewHolder diagHolder = (DiagnosticViewHolder) holder; + return new DiagnosticViewHolder(v); + } - diagHolder.bindDiagnosticAction(actions.get(position)); - } + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + DiagnosticViewHolder diagHolder = (DiagnosticViewHolder) holder; - @Override - public int getItemCount() { - return actions.size(); - } - }); + diagHolder.bindDiagnosticAction(actions.get(position)); + } + + @Override + public int getItemCount() { + return actions.size(); + } + }); + } else { + listView.setVisibility(View.GONE); + emptyMessage.setVisibility(View.VISIBLE); + } } } diff --git a/src/com/audacious_software/passive_data_kit/activities/OAuthResponseActivity.java b/src/com/audacious_software/passive_data_kit/activities/OAuthResponseActivity.java new file mode 100755 index 0000000..3edcfa3 --- /dev/null +++ b/src/com/audacious_software/passive_data_kit/activities/OAuthResponseActivity.java @@ -0,0 +1,32 @@ +package com.audacious_software.passive_data_kit.activities; + +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; + +import com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice; +import com.audacious_software.pdk.passivedatakit.R; + +/** + * Created by cjkarr on 3/18/2017. + */ + +public class OAuthResponseActivity extends AppCompatActivity { + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + protected void onResume() { + super.onResume(); + + Uri u = this.getIntent().getData(); + + Log.e("PDK", "CALLBACK FROM OAUTH: " + u); + Log.e("PDK", "PATH: " + u.getPath()); + + if (u.getPath().startsWith(WithingsDevice.API_OAUTH_CALLBACK_PATH)) { + WithingsDevice.getInstance(this).finishAuthentication(u); + } + } +} diff --git a/src/com/audacious_software/passive_data_kit/activities/PdkActivity.java b/src/com/audacious_software/passive_data_kit/activities/PdkActivity.java index d8ef541..b0a4a1e 100755 --- a/src/com/audacious_software/passive_data_kit/activities/PdkActivity.java +++ b/src/com/audacious_software/passive_data_kit/activities/PdkActivity.java @@ -1,6 +1,5 @@ package com.audacious_software.passive_data_kit.activities; -import android.os.Bundle; import android.support.v7.app.AppCompatActivity; public abstract class PdkActivity extends AppCompatActivity diff --git a/src/com/audacious_software/passive_data_kit/activities/generators/DataPointsAdapter.java b/src/com/audacious_software/passive_data_kit/activities/generators/DataPointsAdapter.java index b711460..fd1679d 100755 --- a/src/com/audacious_software/passive_data_kit/activities/generators/DataPointsAdapter.java +++ b/src/com/audacious_software/passive_data_kit/activities/generators/DataPointsAdapter.java @@ -1,9 +1,6 @@ package com.audacious_software.passive_data_kit.activities.generators; import android.content.Context; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; @@ -14,15 +11,17 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; public class DataPointsAdapter extends RecyclerView.Adapter { - private ArrayList mDataPoints = new ArrayList<>(); + private Context mContext = null; @Override public DataPointViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - Class generatorClass = Generators.getInstance(null).fetchCustomViewClass(viewType); + Class generatorClass = Generators.getInstance(this.mContext).fetchCustomViewClass(viewType); try { Method fetchView = generatorClass.getDeclaredMethod("fetchView", ViewGroup.class); @@ -51,20 +50,24 @@ public DataPointViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { } @Override - public void onBindViewHolder(DataPointViewHolder holder, int position) { - Bundle dataPoint = this.mDataPoints.get(position); - Class generatorClass = Generators.getInstance(null).fetchCustomViewClass(dataPoint.getBundle(Generator.PDK_METADATA).getString(Generator.IDENTIFIER)); + public void onBindViewHolder(final DataPointViewHolder holder, int position) { + List> activeGenerators = Generators.getInstance(holder.itemView.getContext()).activeGenerators(); + + this.sortGenerators(this.mContext, activeGenerators); + + Class generatorClass = activeGenerators.get(position); try { - Method bindViewHolder = generatorClass.getDeclaredMethod("bindViewHolder", DataPointViewHolder.class, Bundle.class); - bindViewHolder.invoke(null, holder, dataPoint); + Method bindViewHolder = generatorClass.getDeclaredMethod("bindViewHolder", DataPointViewHolder.class); + bindViewHolder.invoke(null, holder); } catch (Exception e) { e.printStackTrace(); try { generatorClass = Generator.class; - Method bindViewHolder = generatorClass.getDeclaredMethod("bindViewHolder", DataPointViewHolder.class, Bundle.class); - bindViewHolder.invoke(null, holder, dataPoint); + Method bindViewHolder = generatorClass.getDeclaredMethod("bindViewHolder", DataPointViewHolder.class); + + bindViewHolder.invoke(null, holder); } catch (NoSuchMethodException e1) { Logger.getInstance(holder.itemView.getContext()).logThrowable(e1); } catch (InvocationTargetException e1) { @@ -77,51 +80,63 @@ public void onBindViewHolder(DataPointViewHolder holder, int position) { @Override public int getItemCount() { - return this.mDataPoints.size(); + return Generators.getInstance(null).activeGenerators().size(); } - public int getItemViewType (int position) { - Bundle dataPoint = this.mDataPoints.get(position); - Class generatorClass = Generators.getInstance(null).fetchCustomViewClass(dataPoint.getBundle(Generator.PDK_METADATA).getString(Generator.IDENTIFIER)); - - return generatorClass.hashCode(); - } - - public void updateDataPoint(String identifier, Bundle data) { - ArrayList toDelete = new ArrayList<>(); - - Handler mainHandler = new Handler(Looper.getMainLooper()); - final DataPointsAdapter me = this; - - for (Bundle bundle : this.mDataPoints) { - if (bundle.getBundle(Generator.PDK_METADATA).getString(Generator.IDENTIFIER).equals(identifier)) { - toDelete.add(bundle); + private void sortGenerators(final Context context, List> generators) { + Collections.sort(generators, new Comparator>() { + @Override + public int compare(Class one, Class two) { + long oneUpdated = 0; + + try { + Method oneGenerated = one.getDeclaredMethod("latestPointGenerated", Context.class); + + oneUpdated = (long) oneGenerated.invoke(null, context); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + long twoUpdated = 0; + + try { + Method twoGenerated = two.getDeclaredMethod("latestPointGenerated", Context.class); + + twoUpdated = (long) twoGenerated.invoke(null, context); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + if (oneUpdated < twoUpdated) { + return 1; + } else if (oneUpdated > twoUpdated) { + return -1; + } + + return 0; } - } - - Collections.reverse(toDelete); + }); + } - for (Bundle delete : toDelete) { - final int position = this.mDataPoints.indexOf(delete); + public int getItemViewType (int position) { + List> activeGenerators = Generators.getInstance(this.mContext).activeGenerators(); - this.mDataPoints.remove(position); + this.sortGenerators(this.mContext, activeGenerators); -// mainHandler.post(new Runnable() { -// @Override -// public void run() { -// me.notifyItemRemoved(position); -// } -// }); - } + Class generatorClass = activeGenerators.get(position); - this.mDataPoints.add(0, data); + return generatorClass.hashCode(); + } - mainHandler.post(new Runnable() { - @Override - public void run() { -// me.notifyItemInserted(0); - me.notifyDataSetChanged(); - } - }); + public void setContext(Context context) { + this.mContext = context; } } diff --git a/src/com/audacious_software/passive_data_kit/activities/generators/GeneratorViewHolder.java b/src/com/audacious_software/passive_data_kit/activities/generators/GeneratorViewHolder.java new file mode 100755 index 0000000..5726eb2 --- /dev/null +++ b/src/com/audacious_software/passive_data_kit/activities/generators/GeneratorViewHolder.java @@ -0,0 +1,10 @@ +package com.audacious_software.passive_data_kit.activities.generators; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class GeneratorViewHolder extends RecyclerView.ViewHolder { + public GeneratorViewHolder(View itemView) { + super(itemView); + } +} diff --git a/src/com/audacious_software/passive_data_kit/activities/generators/GeneratorsAdapter.java b/src/com/audacious_software/passive_data_kit/activities/generators/GeneratorsAdapter.java new file mode 100755 index 0000000..adf3618 --- /dev/null +++ b/src/com/audacious_software/passive_data_kit/activities/generators/GeneratorsAdapter.java @@ -0,0 +1,129 @@ +package com.audacious_software.passive_data_kit.activities.generators; + +import android.content.Context; +import android.content.Intent; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.Toast; + +import com.audacious_software.passive_data_kit.Logger; +import com.audacious_software.passive_data_kit.activities.DataDisclosureDetailActivity; +import com.audacious_software.passive_data_kit.generators.Generator; +import com.audacious_software.passive_data_kit.generators.Generators; +import com.audacious_software.pdk.passivedatakit.R; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class GeneratorsAdapter extends RecyclerView.Adapter { + private Context mContext = null; + private FrameLayout mDataView = null; + + @Override + public GeneratorViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = Generator.fetchDisclosureView(parent); + + return new GeneratorViewHolder(view); + } + + @Override + public void onBindViewHolder(final GeneratorViewHolder holder, int position) { + final GeneratorsAdapter me = this; + + List> activeGenerators = Generators.getInstance(holder.itemView.getContext()).activeGenerators(); + + this.sortGenerators(this.mContext, activeGenerators); + + Class generatorClass = activeGenerators.get(position); + + Log.e("PDK", "GENERATOR CLASS: " + generatorClass); + + try { + Method bindViewHolder = generatorClass.getDeclaredMethod("bindDisclosureViewHolder", GeneratorViewHolder.class); + bindViewHolder.invoke(null, holder); + } catch (Exception e) { +// e.printStackTrace(); + try { + generatorClass = Generator.class; + + Method bindViewHolder = generatorClass.getDeclaredMethod("bindDisclosureViewHolder", GeneratorViewHolder.class); + + bindViewHolder.invoke(null, holder); + } catch (NoSuchMethodException e1) { + Logger.getInstance(holder.itemView.getContext()).logThrowable(e1); + } catch (InvocationTargetException e1) { + Logger.getInstance(holder.itemView.getContext()).logThrowable(e1); + } catch (IllegalAccessException e1) { + Logger.getInstance(holder.itemView.getContext()).logThrowable(e1); + } + } + + final Class finalClass = generatorClass; + + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + me.mDataView.removeAllViews(); + + try { + Method bindViewHolder = finalClass.getDeclaredMethod("getDisclosureDataView", GeneratorViewHolder.class); + + View dataView = (View) bindViewHolder.invoke(null, holder); + me.mDataView.addView(dataView); + } catch (NoSuchMethodException e1) { + Logger.getInstance(holder.itemView.getContext()).logThrowable(e1); + } catch (InvocationTargetException e1) { + Logger.getInstance(holder.itemView.getContext()).logThrowable(e1); + } catch (IllegalAccessException e1) { + Logger.getInstance(holder.itemView.getContext()).logThrowable(e1); + } + } + }); + + ImageView settingsButton = (ImageView) holder.itemView.findViewById(R.id.button_disclosure_item); + + settingsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(holder.itemView.getContext(), DataDisclosureDetailActivity.class); + intent.putExtra(DataDisclosureDetailActivity.GENERATOR_CLASS_NAME, finalClass.getCanonicalName()); + + holder.itemView.getContext().startActivity(intent); + } + }); + + } + + @Override + public int getItemCount() { + return Generators.getInstance(null).activeGenerators().size(); + } + + private void sortGenerators(final Context context, List> generators) { + Collections.sort(generators, new Comparator>() { + @Override + public int compare(Class one, Class two) { + return one.getName().compareTo(two.getName()); + } + }); + } + + public int getItemViewType (int position) { + return 0; + } + + public void setContext(Context context) { + this.mContext = context; + } + + public void setDataView(FrameLayout dataView) { + this.mDataView = dataView; + } +} diff --git a/src/com/audacious_software/passive_data_kit/diagnostics/DiagnosticAction.java b/src/com/audacious_software/passive_data_kit/diagnostics/DiagnosticAction.java index c3869ff..d466198 100755 --- a/src/com/audacious_software/passive_data_kit/diagnostics/DiagnosticAction.java +++ b/src/com/audacious_software/passive_data_kit/diagnostics/DiagnosticAction.java @@ -8,8 +8,10 @@ public class DiagnosticAction { private String mMessage = null; private Runnable mAction = null; + private String mTitle = null; - public DiagnosticAction(String message, Runnable action) { + public DiagnosticAction(String title, String message, Runnable action) { + this.mTitle = title; this.mMessage = message; this.mAction = action; } @@ -25,4 +27,8 @@ public void run() { public String getMessage() { return this.mMessage; } + + public String getTitle() { + return this.mTitle; + } } diff --git a/src/com/audacious_software/passive_data_kit/generators/Generator.java b/src/com/audacious_software/passive_data_kit/generators/Generator.java index dedb648..66563c0 100755 --- a/src/com/audacious_software/passive_data_kit/generators/Generator.java +++ b/src/com/audacious_software/passive_data_kit/generators/Generator.java @@ -1,6 +1,10 @@ package com.audacious_software.passive_data_kit.generators; +import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -8,11 +12,13 @@ import android.widget.TextView; import com.audacious_software.passive_data_kit.activities.generators.DataPointViewHolder; +import com.audacious_software.passive_data_kit.activities.generators.GeneratorViewHolder; import com.audacious_software.pdk.passivedatakit.R; import java.text.DateFormat; import java.util.Calendar; import java.util.Date; +import java.util.List; @SuppressWarnings("unused") public abstract class Generator @@ -25,6 +31,15 @@ public abstract class Generator public static final String MEDIA_ATTACHMENT_KEY = "attachment"; public static final String MEDIA_CONTENT_TYPE_KEY = "attachment-type"; public static final String MEDIA_ATTACHMENT_GUID_KEY = "attachment-guid"; + public static final String LATITUDE = "latitude"; + public static final String LONGITUDE = "longitude"; + + private static final String TABLE_SQLITE_MASTER = "sqlite_master"; + + private static final String TABLE_METADATA = "metadata"; + private static String TABLE_METADATA_LAST_UPDATED = "last_updated"; + private static String TABLE_METADATA_KEY = "key"; + private static String TABLE_METADATA_VALUE = "value"; protected Context mContext = null; @@ -41,10 +56,6 @@ public static void stop(Context context) { // Do nothing - override in subclasses. } - public static void broadcastLatestDataPoint(Context context) { - // Do nothing - override in subclasses. - } - public static boolean isEnabled(Context context) { return false; @@ -55,12 +66,32 @@ public static boolean isRunning(Context context) return false; } + public static long latestPointGenerated(Context context) { + return 0; + } + public static View fetchView(ViewGroup parent) { return LayoutInflater.from(parent.getContext()).inflate(R.layout.card_generator_generic, parent, false); } - public static void bindViewHolder(DataPointViewHolder holder, Bundle dataPoint) { - String identifier = dataPoint.getBundle(Generator.PDK_METADATA).getString(Generator.IDENTIFIER); + public static void bindViewHolder(DataPointViewHolder holder) { + Class currentClass = new Object() { }.getClass().getEnclosingClass(); + + String identifier = currentClass.getCanonicalName(); + + TextView generatorLabel = (TextView) holder.itemView.findViewById(R.id.label_generator); + + generatorLabel.setText(identifier); + } + + public static View fetchDisclosureView(ViewGroup parent) { + return LayoutInflater.from(parent.getContext()).inflate(R.layout.row_generator_disclosure_generic, parent, false); + } + + public static void bindDisclosureViewHolder(GeneratorViewHolder holder) { + Class currentClass = new Object() { }.getClass().getEnclosingClass(); + + String identifier = currentClass.getCanonicalName(); TextView generatorLabel = (TextView) holder.itemView.findViewById(R.id.label_generator); @@ -87,4 +118,68 @@ public static String formatTimestamp(Context context, double timestamp) { return context.getString(R.string.format_full_timestamp_pdk, date, time); } + + public abstract List fetchPayloads(); + + public Cursor queryHistory(String[] cols, String where, String[] args, String orderBy) { + Cursor c = new MatrixCursor(cols); + + return c; + } + + protected int getDatabaseVersion(SQLiteDatabase db) { + String where = "type = ? AND name = ?"; + String[] args = { "table", Generator.TABLE_METADATA }; + + Cursor c = db.query(Generator.TABLE_SQLITE_MASTER, null, where, args, null, null, null); + + if (c.getCount() > 0) { + // Do nothing - table exists... + } else { + db.execSQL(this.mContext.getString(R.string.pdk_generator_create_version_table)); + } + + c.close(); + + String versionWhere = Generator.TABLE_METADATA_KEY + " = ?"; + String[] versionArgs = { "version" }; + + c = db.query(Generator.TABLE_METADATA, null, versionWhere, versionArgs, null, null, Generator.TABLE_METADATA_LAST_UPDATED + " DESC"); + + int version = 0; + + if (c.moveToNext()) { + version = Integer.parseInt(c.getString(c.getColumnIndex(Generator.TABLE_METADATA_VALUE))); + } + + c.close(); + + return version; + } + + protected void setDatabaseVersion(SQLiteDatabase db, int newVersion) { + boolean keyExists = false; + + String versionWhere = Generator.TABLE_METADATA_KEY + " = ?"; + String[] versionArgs = { "version" }; + + Cursor c = db.query(Generator.TABLE_METADATA, null, versionWhere, versionArgs, null, null, Generator.TABLE_METADATA_LAST_UPDATED + " DESC"); + + if (c.getCount() > 0) { + keyExists = true; + } + + c.close(); + + ContentValues values = new ContentValues(); + values.put(Generator.TABLE_METADATA_KEY, "version"); + values.put(Generator.TABLE_METADATA_VALUE, "" + newVersion); + values.put(Generator.TABLE_METADATA_LAST_UPDATED, System.currentTimeMillis()); + + if (keyExists) { + db.update(Generator.TABLE_METADATA, values, versionWhere, versionArgs); + } else { + db.insert(Generator.TABLE_METADATA, null, values); + } + } } diff --git a/src/com/audacious_software/passive_data_kit/generators/Generators.java b/src/com/audacious_software/passive_data_kit/generators/Generators.java index fb9e412..14a4d26 100755 --- a/src/com/audacious_software/passive_data_kit/generators/Generators.java +++ b/src/com/audacious_software/passive_data_kit/generators/Generators.java @@ -11,6 +11,7 @@ import com.audacious_software.passive_data_kit.Logger; import com.audacious_software.passive_data_kit.diagnostics.DiagnosticAction; +import com.audacious_software.passive_data_kit.generators.diagnostics.AppEvent; import com.audacious_software.pdk.passivedatakit.R; import java.lang.reflect.InvocationTargetException; @@ -19,22 +20,26 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; public class Generators { private Context mContext = null; private boolean mStarted = false; + private ArrayList mGenerators = new ArrayList<>(); private HashSet mActiveGenerators = new HashSet<>(); private SharedPreferences mSharedPreferences = null; - private HashSet mNewDataPointListeners = new HashSet<>(); private HashMap> mGeneratorMap = new HashMap<>(); private SparseArray> mViewTypeMap = new SparseArray<>(); + private HashSet mGeneratorUpdatedListeners = new HashSet<>(); public void start() { if (!this.mStarted) { this.mGenerators.clear(); + this.mGenerators.add(AppEvent.class.getCanonicalName()); + for (String className : this.mContext.getResources().getStringArray(R.array.pdk_available_generators)) { this.mGenerators.add(className); @@ -148,27 +153,11 @@ public ArrayList diagnostics() { return actions; } - public void transmitData(String identifier, Bundle data) { - double now = (double) System.currentTimeMillis(); - now = now / 1000; // Convert to seconds... - - Bundle metadata = new Bundle(); - metadata.putString(Generator.IDENTIFIER, identifier); - metadata.putDouble(Generator.TIMESTAMP, now); - metadata.putString(Generator.GENERATOR, this.getGeneratorFullName(identifier)); - metadata.putString(Generator.SOURCE, this.getSource()); - data.putBundle(Generator.PDK_METADATA, metadata); - - for (Generators.NewDataPointListener listener : this.mNewDataPointListeners) { - listener.onNewDataPoint(identifier, data); - } - } - - private String getSource() { + public String getSource() { return "unknown-user-please-set-me"; } - private String getGeneratorFullName(String identifier) { + public String getGeneratorFullName(String identifier) { String pdkName = this.mContext.getString(R.string.pdk_name); String pdkVersion = this.mContext.getString(R.string.pdk_version); String appName = this.mContext.getString(this.mContext.getApplicationInfo().labelRes); @@ -186,10 +175,6 @@ private String getGeneratorFullName(String identifier) { return identifier + ": " + appName + "/" + version + " " + pdkName + "/" + pdkVersion; } - public void removeNewDataPointListener(Generators.NewDataPointListener listener) { - this.mNewDataPointListeners.remove(listener); - } - public void registerCustomViewClass(String identifier, Class generatorClass) { this.mGeneratorMap.put(identifier, generatorClass); this.mViewTypeMap.put(generatorClass.hashCode(), generatorClass); @@ -215,29 +200,59 @@ public Class fetchCustomViewClass(int viewType) { return generatorClass; } - public void broadcastLatestDataPoints() { - for (String className : this.mGenerators) - { - try { - Class generatorClass = (Class) Class.forName(className); - - Log.e("PDK", "CLASS " + generatorClass); + public Generator getGenerator(String className) { + Log.e("BB", "GENERATOR FIND START"); + for (String name : this.mActiveGenerators) { + Log.e("BB", "GENERATOR NAME: " + name); + } + Log.e("BB", "GENERATOR FIND END"); - if (generatorClass != null) { - Method broadcast = generatorClass.getDeclaredMethod("broadcastLatestDataPoint", Context.class); + if (this.mActiveGenerators.contains(className)) { + try { + Class probeClass = (Class) Class.forName(className); - broadcast.invoke(null, this.mContext); - } + Method getInstance = probeClass.getDeclaredMethod("getInstance", Context.class); + return (Generator) getInstance.invoke(null, this.mContext); + } catch (ClassNotFoundException e) { + e.printStackTrace(); } catch (NoSuchMethodException e) { - - } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + + return null; + } + + public List> activeGenerators() { + ArrayList> active = new ArrayList<>(); + + for (String className : this.mActiveGenerators) { + try { + active.add((Class) Class.forName(className)); } catch (ClassNotFoundException e) { e.printStackTrace(); } } + + return active; + } + + public void notifyGeneratorUpdated(String identifier, long timestamp, Bundle bundle) { + for (GeneratorUpdatedListener listener : this.mGeneratorUpdatedListeners) { + listener.onGeneratorUpdated(identifier, timestamp, bundle); + } + } + + public void notifyGeneratorUpdated(String identifier, Bundle bundle) { + long timestamp = System.currentTimeMillis(); + + for (GeneratorUpdatedListener listener : this.mGeneratorUpdatedListeners) { + listener.onGeneratorUpdated(identifier, timestamp, bundle); + } } private static class GeneratorsHolder { @@ -257,11 +272,15 @@ private void setContext(Context context) { this.mContext = context.getApplicationContext(); } - public void addNewDataPointListener(Generators.NewDataPointListener listener) { - this.mNewDataPointListeners.add(listener); + public void addNewGeneratorUpdatedListener(Generators.GeneratorUpdatedListener listener) { + this.mGeneratorUpdatedListeners.add(listener); + } + + public void removeGeneratorUpdatedListener(Generators.GeneratorUpdatedListener listener) { + this.mGeneratorUpdatedListeners.remove(listener); } - public interface NewDataPointListener { - void onNewDataPoint(String identifier, Bundle data); + public interface GeneratorUpdatedListener { + void onGeneratorUpdated(String identifier, long timestamp, Bundle data); } } diff --git a/src/com/audacious_software/passive_data_kit/generators/communication/PhoneCalls.java b/src/com/audacious_software/passive_data_kit/generators/communication/PhoneCalls.java index 4294ab8..cea19b6 100755 --- a/src/com/audacious_software/passive_data_kit/generators/communication/PhoneCalls.java +++ b/src/com/audacious_software/passive_data_kit/generators/communication/PhoneCalls.java @@ -1,11 +1,13 @@ package com.audacious_software.passive_data_kit.generators.communication; import android.Manifest; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.graphics.Typeface; import android.os.Build; import android.os.Bundle; @@ -20,6 +22,7 @@ import android.view.ViewGroup; import android.widget.TextView; +import com.audacious_software.passive_data_kit.PassiveDataKit; import com.audacious_software.passive_data_kit.activities.generators.DataPointViewHolder; import com.audacious_software.passive_data_kit.activities.generators.RequestPermissionActivity; import com.audacious_software.passive_data_kit.diagnostics.DiagnosticAction; @@ -27,19 +30,17 @@ import com.audacious_software.passive_data_kit.generators.Generators; import com.audacious_software.pdk.passivedatakit.R; import com.github.mikephil.charting.charts.PieChart; -import com.github.mikephil.charting.components.Description; -import com.github.mikephil.charting.components.Legend; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.PieData; import com.github.mikephil.charting.data.PieDataSet; import com.github.mikephil.charting.data.PieEntry; import com.github.mikephil.charting.formatter.IValueFormatter; -import com.github.mikephil.charting.utils.ColorTemplate; import com.github.mikephil.charting.utils.ViewPortHandler; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; +import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -50,21 +51,6 @@ public class PhoneCalls extends Generator { private static final String ENABLED = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.ENABLED"; private static final boolean ENABLED_DEFAULT = true; - private static final String SAMPLE_INTERVAL = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.SAMPLE_INTERVAL"; - private static final long SAMPLE_INTERVAL_DEFAULT = 30000; // 300000; - - private static final String LAST_INCOMING_COUNT = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.LAST_INCOMING_COUNT"; - private static final String LAST_OUTGOING_COUNT = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.LAST_OUTGOING_COUNT"; - private static final String LAST_MISSED_COUNT = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.LAST_MISSED_COUNT"; - private static final String LAST_TOTAL_COUNT = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.LAST_TOTAL_COUNT"; - - private static final String LAST_CALL_TIMESTAMP = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.LAST_CALL_TIMESTAMP"; - private static final String LAST_CALL_DURATION = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.LAST_CALL_DURATION"; - private static final String LAST_CALL_TYPE = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.LAST_CALL_TYPE"; - - private static final String LAST_SAMPLE = "com.audacious_software.passive_data_kit.generators.communication.PhoneCalls.LAST_SAMPLE"; - private static final long LAST_SAMPLE_DEFAULT = 0; - private static final String CALL_DATE_KEY = "call_timestamp"; private static final String CALL_DURATION_KEY = "duration"; private static final String CALL_IS_NEW_KEY = "is_new"; @@ -94,9 +80,33 @@ public class PhoneCalls extends Generator { private static final String CALL_PRESENTATION_PAYPHONE = "payphone"; private static final String CALL_PRESENTATION_UNKNOWN = "unknown"; + private static int DATABASE_VERSION = 2; + + private static final String TABLE_HISTORY = "history"; + private static final String HISTORY_OBSERVED = "observed"; + private static final String HISTORY_DURATION = "duration"; + private static final String HISTORY_NUMBER = "number"; + private static final String HISTORY_IS_NEW = "is_new"; + private static final String HISTORY_PULLED_EXTERNALLY = "pulled_externally"; + private static final String HISTORY_POST_DIAL_DIGITS = "post_dial_digits"; + private static final String HISTORY_COUNTRY_ISO = "country_iso"; + private static final String HISTORY_DATA_USAGE = "data_usage"; + private static final String HISTORY_GEOCODED_LOCATION = "geocoded_location"; + private static final String HISTORY_VIDEO = "is_video"; + private static final String HISTORY_VIA_NUMBER = "via_number"; + private static final String HISTORY_PRESENTATION = "presentation"; + private static final String HISTORY_IS_READ = "is_read"; + private static final String HISTORY_CALL_TYPE = "call_type"; + private static PhoneCalls sInstance = null; private Handler mHandler = null; + private Context mContext = null; + + private static final String DATABASE_PATH = "pdk-phone-calls.sqlite"; + + private SQLiteDatabase mDatabase = null; + private long mSampleInterval = 60000; public static PhoneCalls getInstance(Context context) { if (PhoneCalls.sInstance == null) { @@ -108,6 +118,8 @@ public static PhoneCalls getInstance(Context context) { public PhoneCalls(Context context) { super(context); + + this.mContext = context.getApplicationContext(); } public static void start(final Context context) { @@ -115,12 +127,14 @@ public static void start(final Context context) { } private void startGenerator() { - Log.e("PDK", "START PHONE CALL GENERATOR"); - final PhoneCalls me = this; if (this.mHandler != null) { - this.mHandler.getLooper().quitSafely(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + this.mHandler.getLooper().quitSafely(); + } else { + this.mHandler.getLooper().quit(); + } this.mHandler = null; } @@ -128,9 +142,7 @@ private void startGenerator() { final Runnable checkLogs = new Runnable() { @Override public void run() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(me.mContext); - SharedPreferences.Editor e = prefs.edit(); - + Log.e("PDK", "CHECK PHONE LOGS"); boolean approved = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @@ -143,156 +155,172 @@ public void run() { Log.e("PDK", "TODO: Fetch Call Logs..."); - long now = System.currentTimeMillis(); - if (approved) { - long totalIncoming = prefs.getLong(PhoneCalls.LAST_INCOMING_COUNT, 0); - long totalOutgoing = prefs.getLong(PhoneCalls.LAST_OUTGOING_COUNT, 0); - long totalMissed = prefs.getLong(PhoneCalls.LAST_MISSED_COUNT, 0); - long total = prefs.getLong(PhoneCalls.LAST_TOTAL_COUNT, 0); + long lastObserved = 0; - long lastSample = prefs.getLong(PhoneCalls.LAST_SAMPLE, PhoneCalls.LAST_SAMPLE_DEFAULT); + Cursor lastCursor = me.mDatabase.query(PhoneCalls.TABLE_HISTORY, null, null, null, null, null, PhoneCalls.HISTORY_OBSERVED + " DESC"); - String where = CallLog.Calls.DATE + " > ?"; - String[] args = {"" + lastSample}; + if (lastCursor.moveToNext()) { + lastObserved = lastCursor.getLong(lastCursor.getColumnIndex(PhoneCalls.HISTORY_OBSERVED)); + } - long latestCallTimestamp = -1; - String latestCallType = null; - long latestCallDuration = -1; + lastCursor.close(); - Cursor c = me.mContext.getContentResolver().query(CallLog.Calls.CONTENT_URI, null, where, args, CallLog.Calls.DATE); + String where = CallLog.Calls.DATE + " > ?"; + String[] args = {"" + lastObserved}; - while (c.moveToNext()) { - Bundle bundle = new Bundle(); + Cursor c = me.mContext.getContentResolver().query(CallLog.Calls.CONTENT_URI, null, where, args, CallLog.Calls.DATE); - bundle.putLong(PhoneCalls.CALL_DATE_KEY, c.getLong(c.getColumnIndex(CallLog.Calls.DATE))); - bundle.putLong(PhoneCalls.CALL_DURATION_KEY, c.getLong(c.getColumnIndex(CallLog.Calls.DURATION))); - bundle.putString(PhoneCalls.CALL_NUMBER_KEY, c.getString(c.getColumnIndex(CallLog.Calls.NUMBER))); - bundle.putBoolean(PhoneCalls.CALL_IS_NEW_KEY, (c.getInt(c.getColumnIndex(CallLog.Calls.NEW)) != 0)); + if (c != null) { + while (c.moveToNext()) { + ContentValues values = new ContentValues(); + values.put(PhoneCalls.HISTORY_OBSERVED, c.getLong(c.getColumnIndex(CallLog.Calls.DATE))); + values.put(PhoneCalls.HISTORY_DURATION, c.getLong(c.getColumnIndex(CallLog.Calls.DURATION))); + values.put(PhoneCalls.HISTORY_NUMBER, c.getLong(c.getColumnIndex(CallLog.Calls.DURATION))); + values.put(PhoneCalls.HISTORY_IS_NEW, (c.getInt(c.getColumnIndex(CallLog.Calls.NEW)) != 0)); - int features = 0; + Bundle bundle = new Bundle(); + bundle.putLong(PhoneCalls.CALL_DATE_KEY, c.getLong(c.getColumnIndex(CallLog.Calls.DATE))); + bundle.putLong(PhoneCalls.CALL_DURATION_KEY, c.getLong(c.getColumnIndex(CallLog.Calls.DURATION))); + bundle.putString(PhoneCalls.CALL_NUMBER_KEY, c.getString(c.getColumnIndex(CallLog.Calls.NUMBER))); + bundle.putBoolean(PhoneCalls.CALL_IS_NEW_KEY, (c.getInt(c.getColumnIndex(CallLog.Calls.NEW)) != 0)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - features = c.getInt(c.getColumnIndex(CallLog.Calls.FEATURES)); - } + int features = 0; - int typeInt = c.getInt(c.getColumnIndex(CallLog.Calls.TYPE)); - String type = PhoneCalls.CALL_TYPE_UNKNOWN; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + features = c.getInt(c.getColumnIndex(CallLog.Calls.FEATURES)); + } - switch(Build.VERSION.SDK_INT) { - case 25: - if (typeInt == CallLog.Calls.ANSWERED_EXTERNALLY_TYPE) { - type = PhoneCalls.CALL_TYPE_ANSWERED_EXTERNALLY; - } + int typeInt = c.getInt(c.getColumnIndex(CallLog.Calls.TYPE)); + String type = PhoneCalls.CALL_TYPE_UNKNOWN; + + switch (Build.VERSION.SDK_INT) { + case 25: + if (typeInt == CallLog.Calls.ANSWERED_EXTERNALLY_TYPE) { + type = PhoneCalls.CALL_TYPE_ANSWERED_EXTERNALLY; + } + + bundle.putBoolean(PhoneCalls.CALL_PULLED_EXTERNALLY_KEY, ((features & CallLog.Calls.FEATURES_PULLED_EXTERNALLY) == CallLog.Calls.FEATURES_PULLED_EXTERNALLY)); + + values.put(PhoneCalls.HISTORY_PULLED_EXTERNALLY, ((features & CallLog.Calls.FEATURES_PULLED_EXTERNALLY) == CallLog.Calls.FEATURES_PULLED_EXTERNALLY)); + case 24: + if (typeInt == CallLog.Calls.REJECTED_TYPE) { + type = PhoneCalls.CALL_TYPE_REJECTED; + } else if (typeInt == CallLog.Calls.BLOCKED_TYPE) { + type = PhoneCalls.CALL_TYPE_BLOCKED; + } + + bundle.putString(PhoneCalls.CALL_POST_DIAL_DIGITS_KEY, c.getString(c.getColumnIndex(CallLog.Calls.POST_DIAL_DIGITS))); + bundle.putString(PhoneCalls.CALL_VIA_NUMBER_KEY, c.getString(c.getColumnIndex(CallLog.Calls.VIA_NUMBER))); + + values.put(PhoneCalls.HISTORY_POST_DIAL_DIGITS, c.getString(c.getColumnIndex(CallLog.Calls.POST_DIAL_DIGITS))); + values.put(PhoneCalls.HISTORY_VIA_NUMBER, c.getString(c.getColumnIndex(CallLog.Calls.VIA_NUMBER))); + case 21: + if (typeInt == CallLog.Calls.VOICEMAIL_TYPE) { + type = PhoneCalls.CALL_TYPE_VOICEMAIL; + } + + bundle.putString(PhoneCalls.CALL_COUNTRY_ISO_KEY, c.getString(c.getColumnIndex(CallLog.Calls.COUNTRY_ISO))); + bundle.putLong(PhoneCalls.CALL_DATA_USAGE_KEY, c.getLong(c.getColumnIndex(CallLog.Calls.DATA_USAGE))); + bundle.putString(PhoneCalls.CALL_GEOCODED_LOCATION_KEY, c.getString(c.getColumnIndex(CallLog.Calls.GEOCODED_LOCATION))); + bundle.putBoolean(PhoneCalls.CALL_VIDEO_KEY, ((features & CallLog.Calls.FEATURES_VIDEO) == CallLog.Calls.FEATURES_VIDEO)); + + values.put(PhoneCalls.HISTORY_COUNTRY_ISO, c.getString(c.getColumnIndex(CallLog.Calls.COUNTRY_ISO))); + values.put(PhoneCalls.HISTORY_DATA_USAGE, c.getLong(c.getColumnIndex(CallLog.Calls.DATA_USAGE))); + values.put(PhoneCalls.HISTORY_GEOCODED_LOCATION, c.getString(c.getColumnIndex(CallLog.Calls.GEOCODED_LOCATION))); + values.put(PhoneCalls.HISTORY_VIDEO, ((features & CallLog.Calls.FEATURES_VIDEO) == CallLog.Calls.FEATURES_VIDEO)); - bundle.putBoolean(PhoneCalls.CALL_PULLED_EXTERNALLY_KEY, ((features & CallLog.Calls.FEATURES_PULLED_EXTERNALLY) == CallLog.Calls.FEATURES_PULLED_EXTERNALLY)); - case 24: - if (typeInt == CallLog.Calls.REJECTED_TYPE) { - type = PhoneCalls.CALL_TYPE_REJECTED; - } else if (typeInt == CallLog.Calls.BLOCKED_TYPE) { - type = PhoneCalls.CALL_TYPE_BLOCKED; - } - - bundle.putString(PhoneCalls.CALL_POST_DIAL_DIGITS_KEY, c.getString(c.getColumnIndex(CallLog.Calls.POST_DIAL_DIGITS))); - bundle.putString(PhoneCalls.CALL_VIA_NUMBER_KEY, c.getString(c.getColumnIndex(CallLog.Calls.VIA_NUMBER))); - case 21: - if (typeInt == CallLog.Calls.VOICEMAIL_TYPE) { - type = PhoneCalls.CALL_TYPE_VOICEMAIL; - } +// bundle.putString(PhoneCalls.CALL_TRANSCRIPTION_KEY, c.getString(c.getColumnIndex(CallLog.Calls.TRANSCRIPTION))); + case 19: + switch (c.getInt(c.getColumnIndex(CallLog.Calls.NUMBER_PRESENTATION))) { + case CallLog.Calls.PRESENTATION_ALLOWED: + bundle.putString(PhoneCalls.CALL_PRESENTATION_KEY, PhoneCalls.CALL_PRESENTATION_ALLOWED); + values.put(PhoneCalls.HISTORY_PRESENTATION, PhoneCalls.CALL_PRESENTATION_ALLOWED); + break; + case CallLog.Calls.PRESENTATION_RESTRICTED: + bundle.putString(PhoneCalls.CALL_PRESENTATION_KEY, PhoneCalls.CALL_PRESENTATION_RESTRICTED); + values.put(PhoneCalls.HISTORY_PRESENTATION, PhoneCalls.CALL_PRESENTATION_RESTRICTED); + break; + case CallLog.Calls.PRESENTATION_PAYPHONE: + bundle.putString(PhoneCalls.CALL_PRESENTATION_KEY, PhoneCalls.CALL_PRESENTATION_PAYPHONE); + values.put(PhoneCalls.HISTORY_PRESENTATION, PhoneCalls.CALL_PRESENTATION_PAYPHONE); + break; + case CallLog.Calls.PRESENTATION_UNKNOWN: + bundle.putString(PhoneCalls.CALL_PRESENTATION_KEY, PhoneCalls.CALL_PRESENTATION_UNKNOWN); + values.put(PhoneCalls.HISTORY_PRESENTATION, PhoneCalls.CALL_PRESENTATION_UNKNOWN); + break; + } + case 14: + bundle.putBoolean(PhoneCalls.CALL_IS_READ_KEY, (c.getInt(c.getColumnIndex(CallLog.Calls.IS_READ)) != 0)); + values.put(PhoneCalls.HISTORY_IS_READ, (c.getInt(c.getColumnIndex(CallLog.Calls.IS_READ)) != 0)); + case 1: + if (typeInt == CallLog.Calls.INCOMING_TYPE) { + type = PhoneCalls.CALL_TYPE_INCOMING; + } else if (typeInt == CallLog.Calls.OUTGOING_TYPE) { + type = PhoneCalls.CALL_TYPE_OUTGOING; + } else if (typeInt == CallLog.Calls.MISSED_TYPE) { + type = PhoneCalls.CALL_TYPE_MISSED; + } + } - bundle.putString(PhoneCalls.CALL_COUNTRY_ISO_KEY, c.getString(c.getColumnIndex(CallLog.Calls.COUNTRY_ISO))); - bundle.putLong(PhoneCalls.CALL_DATA_USAGE_KEY, c.getLong(c.getColumnIndex(CallLog.Calls.DATA_USAGE))); - bundle.putString(PhoneCalls.CALL_GEOCODED_LOCATION_KEY, c.getString(c.getColumnIndex(CallLog.Calls.GEOCODED_LOCATION))); + bundle.putString(PhoneCalls.CALL_TYPE_KEY, type); + values.put(PhoneCalls.HISTORY_CALL_TYPE, type); - bundle.putBoolean(PhoneCalls.CALL_VIDEO_KEY, ((features & CallLog.Calls.FEATURES_VIDEO) == CallLog.Calls.FEATURES_VIDEO)); + String[] sensitiveFields = { + PhoneCalls.CALL_NUMBER_KEY, + PhoneCalls.CALL_POST_DIAL_DIGITS_KEY, + PhoneCalls.CALL_VIA_NUMBER_KEY, + }; -// bundle.putString(PhoneCalls.CALL_TRANSCRIPTION_KEY, c.getString(c.getColumnIndex(CallLog.Calls.TRANSCRIPTION))); - case 19: - switch (c.getInt(c.getColumnIndex(CallLog.Calls.NUMBER_PRESENTATION))) { - case CallLog.Calls.PRESENTATION_ALLOWED: - bundle.putString(PhoneCalls.CALL_PRESENTATION_KEY, PhoneCalls.CALL_PRESENTATION_ALLOWED); - break; - case CallLog.Calls.PRESENTATION_RESTRICTED: - bundle.putString(PhoneCalls.CALL_PRESENTATION_KEY, PhoneCalls.CALL_PRESENTATION_RESTRICTED); - break; - case CallLog.Calls.PRESENTATION_PAYPHONE: - bundle.putString(PhoneCalls.CALL_PRESENTATION_KEY, PhoneCalls.CALL_PRESENTATION_PAYPHONE); - break; - case CallLog.Calls.PRESENTATION_UNKNOWN: - bundle.putString(PhoneCalls.CALL_PRESENTATION_KEY, PhoneCalls.CALL_PRESENTATION_UNKNOWN); - break; + for (String field : sensitiveFields) { + if (bundle.containsKey(field)) { + bundle.putString(field, new String(Hex.encodeHex(DigestUtils.sha256(bundle.getString(field))))); } - case 14: - bundle.putBoolean(PhoneCalls.CALL_IS_READ_KEY, (c.getInt(c.getColumnIndex(CallLog.Calls.IS_READ)) != 0)); - case 1: - if (typeInt == CallLog.Calls.INCOMING_TYPE) { - type = PhoneCalls.CALL_TYPE_INCOMING; - totalIncoming += 1; - } else if (typeInt == CallLog.Calls.OUTGOING_TYPE) { - type = PhoneCalls.CALL_TYPE_OUTGOING; - totalOutgoing += 1; - } else if (typeInt == CallLog.Calls.MISSED_TYPE) { - type = PhoneCalls.CALL_TYPE_MISSED; - totalMissed += 1; - } - } - - bundle.putString(PhoneCalls.CALL_TYPE_KEY, type); + } - if (bundle.getLong(PhoneCalls.CALL_DATE_KEY, 0) > latestCallTimestamp) { - latestCallTimestamp = bundle.getLong(PhoneCalls.CALL_DATE_KEY, 0); - latestCallDuration = bundle.getLong(PhoneCalls.CALL_DURATION_KEY, 0); - latestCallType = type; - } + String[] valueSensitiveFields = { + PhoneCalls.HISTORY_NUMBER, + PhoneCalls.HISTORY_POST_DIAL_DIGITS, + PhoneCalls.HISTORY_VIA_NUMBER, + }; - Log.e("PDK", "------"); - for (int i = 0; i < c.getColumnCount(); i++) { - Log.e("PDK", "CALL LOG: " + c.getColumnName(i) + " --> " + c.getString(i)); - } + for (String field : valueSensitiveFields) { + if (values.containsKey(field)) { + values.put(field, new String(Hex.encodeHex(DigestUtils.sha256(values.getAsString(field))))); + } + } - String[] sensitiveFields = { - PhoneCalls.CALL_NUMBER_KEY, - PhoneCalls.CALL_POST_DIAL_DIGITS_KEY, - PhoneCalls.CALL_VIA_NUMBER_KEY - }; + me.mDatabase.insert(PhoneCalls.TABLE_HISTORY, null, values); - for (String field : sensitiveFields) { - if (bundle.containsKey(field)) { - bundle.putString(field, new String(Hex.encodeHex(DigestUtils.sha256(bundle.getString(field))))); - } + Generators.getInstance(me.mContext).notifyGeneratorUpdated(PhoneCalls.GENERATOR_IDENTIFIER, bundle); } - Generators.getInstance(me.mContext).transmitData(PhoneCalls.GENERATOR_IDENTIFIER, bundle); + c.close(); } + } - Log.e("PDK", "------"); + if (me.mHandler != null) { + me.mHandler.postDelayed(this, me.mSampleInterval); + } + } + }; - total += c.getCount(); + File path = PassiveDataKit.getGeneratorsStorage(this.mContext); - c.close(); + path = new File(path, PhoneCalls.DATABASE_PATH); - if (latestCallType != null) { - e.putLong(PhoneCalls.LAST_CALL_TIMESTAMP, latestCallTimestamp); - e.putLong(PhoneCalls.LAST_CALL_DURATION, latestCallDuration); - e.putString(PhoneCalls.LAST_CALL_TYPE, latestCallType); - } + this.mDatabase = SQLiteDatabase.openOrCreateDatabase(path, null); - e.putLong(PhoneCalls.LAST_INCOMING_COUNT, totalIncoming); - e.putLong(PhoneCalls.LAST_OUTGOING_COUNT, totalOutgoing); - e.putLong(PhoneCalls.LAST_MISSED_COUNT, totalMissed); - e.putLong(PhoneCalls.LAST_TOTAL_COUNT, total); + int version = this.getDatabaseVersion(this.mDatabase); - e.putLong(PhoneCalls.LAST_SAMPLE, now); - e.apply(); - } - - long sampleInterval = prefs.getLong(PhoneCalls.SAMPLE_INTERVAL, PhoneCalls.SAMPLE_INTERVAL_DEFAULT); + switch (version) { + case 0: + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_phone_calls_create_history_table)); + case 1: + this.mDatabase.delete(PhoneCalls.TABLE_HISTORY, null, null); + } - if (me.mHandler != null) { - me.mHandler.postDelayed(this, sampleInterval); - } - } - }; + this.setDatabaseVersion(this.mDatabase, PhoneCalls.DATABASE_VERSION); Runnable r = new Runnable() { @Override @@ -340,7 +368,7 @@ public static ArrayList diagnostics(final Context context) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALL_LOG) != PackageManager.PERMISSION_GRANTED){ final Handler handler = new Handler(Looper.getMainLooper()); - actions.add(new DiagnosticAction(context.getString(R.string.diagnostic_call_log_permission_required), new Runnable() { + actions.add(new DiagnosticAction(context.getString(R.string.diagnostic_call_log_permission_required_title), context.getString(R.string.diagnostic_call_log_permission_required), new Runnable() { @Override public void run() { @@ -363,92 +391,136 @@ public void run() { return actions; } - public static void bindViewHolder(DataPointViewHolder holder, final Bundle dataPoint) { + public static void bindViewHolder(DataPointViewHolder holder) { final Context context = holder.itemView.getContext(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - double timestamp = dataPoint.getBundle(Generator.PDK_METADATA).getDouble(Generator.TIMESTAMP); + long lastTimestamp = 0; + long lastDuration = 0; + String callType = null; - TextView dateLabel = (TextView) holder.itemView.findViewById(R.id.generator_data_point_date); + long totalIncoming = 0; + long totalOutgoing = 0; + long totalMissed = 0; + long total = 0; - dateLabel.setText(Generator.formatTimestamp(context, timestamp)); + PhoneCalls generator = PhoneCalls.getInstance(holder.itemView.getContext()); - PieChart pieChart = (PieChart) holder.itemView.findViewById(R.id.chart_phone_calls); - pieChart.getLegend().setEnabled(false); + Cursor c = generator.mDatabase.query(PhoneCalls.TABLE_HISTORY, null, null, null, null, null, PhoneCalls.HISTORY_OBSERVED + " DESC"); - pieChart.setEntryLabelColor(android.R.color.transparent); - pieChart.getDescription().setEnabled(false); - pieChart.setDrawHoleEnabled(false); + while (c.moveToNext()) { + if (lastTimestamp == 0) { + lastTimestamp = c.getLong(c.getColumnIndex(PhoneCalls.HISTORY_OBSERVED)); + lastDuration = c.getLong(c.getColumnIndex(PhoneCalls.HISTORY_DURATION)); + } - long totalIncoming = prefs.getLong(PhoneCalls.LAST_INCOMING_COUNT, 0); - long totalOutgoing = prefs.getLong(PhoneCalls.LAST_OUTGOING_COUNT, 0); - long totalMissed = prefs.getLong(PhoneCalls.LAST_MISSED_COUNT, 0); - long total = prefs.getLong(PhoneCalls.LAST_TOTAL_COUNT, 0); + total += 1; - List entries = new ArrayList<>(); + String type = c.getString(c.getColumnIndex(PhoneCalls.HISTORY_CALL_TYPE)); - if (totalIncoming > 0) { - entries.add(new PieEntry(totalIncoming, context.getString(R.string.generator_phone_calls_incoming_label))); - } + if (PhoneCalls.CALL_TYPE_INCOMING.equals(type)) { + totalIncoming += 1; + } else if (PhoneCalls.CALL_TYPE_OUTGOING.equals(type)) { + totalOutgoing += 1; + } else if (PhoneCalls.CALL_TYPE_MISSED.equals(type)) { + totalOutgoing += 1; + } - if (totalOutgoing > 0) { - entries.add(new PieEntry(totalOutgoing, context.getString(R.string.generator_phone_calls_outgoing_label))); + if (callType == null) { + callType = type; + } } - if (totalMissed > 0) { - entries.add(new PieEntry(totalMissed, context.getString(R.string.generator_phone_calls_missed_label))); - } + c.close(); - long other = total - (totalIncoming + totalOutgoing + totalMissed); + View cardContent = holder.itemView.findViewById(R.id.card_content); + View cardEmpty = holder.itemView.findViewById(R.id.card_empty); + TextView dateLabel = (TextView) holder.itemView.findViewById(R.id.generator_data_point_date); - if (other > 0) { - entries.add(new PieEntry(other, context.getString(R.string.generator_phone_calls_other_label))); - } + if (total > 0) { + cardContent.setVisibility(View.VISIBLE); + cardEmpty.setVisibility(View.GONE); - PieDataSet set = new PieDataSet(entries, " "); + dateLabel.setText(Generator.formatTimestamp(context, lastTimestamp)); - int[] colors = { - R.color.generator_phone_call_incoming, - R.color.generator_phone_call_outgoing, - R.color.generator_phone_call_missed, - R.color.generator_phone_call_other - }; + PieChart pieChart = (PieChart) holder.itemView.findViewById(R.id.chart_phone_calls); + pieChart.getLegend().setEnabled(false); - set.setColors(colors, context); + pieChart.setEntryLabelColor(android.R.color.transparent); + pieChart.getDescription().setEnabled(false); + pieChart.setDrawHoleEnabled(false); - PieData data = new PieData(set); - data.setValueTextSize(14); - data.setValueTypeface(Typeface.DEFAULT_BOLD); - data.setValueTextColor(0xffffffff); + List entries = new ArrayList<>(); - data.setValueFormatter(new IValueFormatter() { - @Override - public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { - return "" + ((Float) value).intValue(); + if (totalIncoming > 0) { + entries.add(new PieEntry(totalIncoming, context.getString(R.string.generator_phone_calls_incoming_label))); + } + + if (totalOutgoing > 0) { + entries.add(new PieEntry(totalOutgoing, context.getString(R.string.generator_phone_calls_outgoing_label))); + } + + if (totalMissed > 0) { + entries.add(new PieEntry(totalMissed, context.getString(R.string.generator_phone_calls_missed_label))); } - }); - pieChart.setData(data); - pieChart.invalidate(); + long other = total - (totalIncoming + totalOutgoing + totalMissed); + + if (other > 0) { + entries.add(new PieEntry(other, context.getString(R.string.generator_phone_calls_other_label))); + } + + PieDataSet set = new PieDataSet(entries, " "); + + int[] colors = { + R.color.generator_phone_call_incoming, + R.color.generator_phone_call_outgoing, + R.color.generator_phone_call_missed, + R.color.generator_phone_call_other + }; + + set.setColors(colors, context); + + PieData data = new PieData(set); + data.setValueTextSize(14); + data.setValueTypeface(Typeface.DEFAULT_BOLD); + data.setValueTextColor(0xffffffff); + + data.setValueFormatter(new IValueFormatter() { + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + return "" + ((Float) value).intValue(); + } + }); + + pieChart.setData(data); + pieChart.invalidate(); - long latest = prefs.getLong(PhoneCalls.LAST_CALL_TIMESTAMP, 0); - long duration = prefs.getLong(PhoneCalls.LAST_CALL_DURATION, 0); - String direction = prefs.getString(PhoneCalls.LAST_CALL_TYPE, ""); + TextView latestField = (TextView) holder.itemView.findViewById(R.id.field_latest_call); + TextView durationField = (TextView) holder.itemView.findViewById(R.id.field_duration); + TextView directionField = (TextView) holder.itemView.findViewById(R.id.field_direction); - TextView latestField = (TextView) holder.itemView.findViewById(R.id.field_latest_call); - TextView durationField = (TextView) holder.itemView.findViewById(R.id.field_duration); - TextView directionField = (TextView) holder.itemView.findViewById(R.id.field_direction); + Date lateDate = new Date(lastTimestamp); + String day = android.text.format.DateFormat.getMediumDateFormat(context).format(lateDate); + String time = android.text.format.DateFormat.getTimeFormat(context).format(lateDate); - Date lateDate = new Date(latest); - String day = android.text.format.DateFormat.getMediumDateFormat(context).format(lateDate); - String time = android.text.format.DateFormat.getTimeFormat(context).format(lateDate); + latestField.setText(context.getString(R.string.format_full_timestamp_pdk, day, time)); + durationField.setText(context.getString(R.string.generator_phone_calls_duration_format, ((float) lastDuration) / 60)); + directionField.setText(callType); - latestField.setText(context.getString(R.string.format_full_timestamp_pdk, day, time)); - durationField.setText(context.getString(R.string.generator_phone_calls_duration_format, ((float) duration) / 60)); - directionField.setText(direction); + dateLabel.setText(Generator.formatTimestamp(context, lastTimestamp / 1000)); + } else { + cardContent.setVisibility(View.GONE); + cardEmpty.setVisibility(View.VISIBLE); - dateLabel.setText(Generator.formatTimestamp(context, latest / 1000)); + dateLabel.setText(R.string.label_never_pdk); + } + } + + @Override + public List fetchPayloads() { + return new ArrayList<>(); } public static View fetchView(ViewGroup parent) @@ -456,8 +528,19 @@ public static View fetchView(ViewGroup parent) return LayoutInflater.from(parent.getContext()).inflate(R.layout.card_generator_phone_calls, parent, false); } - public static void broadcastLatestDataPoint(Context context) { - Generators.getInstance(context).transmitData(PhoneCalls.GENERATOR_IDENTIFIER, new Bundle()); - } + public static long latestPointGenerated(Context context) { + long timestamp = 0; + + PhoneCalls me = PhoneCalls.getInstance(context); + Cursor c = me.mDatabase.query(PhoneCalls.TABLE_HISTORY, null, null, null, null, null, PhoneCalls.HISTORY_OBSERVED + " DESC"); + + if (c.moveToNext()) { + timestamp = c.getLong(c.getColumnIndex(PhoneCalls.HISTORY_OBSERVED)); + } + + c.close(); + + return timestamp; + } } diff --git a/src/com/audacious_software/passive_data_kit/generators/communication/TextMessages.java b/src/com/audacious_software/passive_data_kit/generators/communication/TextMessages.java new file mode 100755 index 0000000..f82e641 --- /dev/null +++ b/src/com/audacious_software/passive_data_kit/generators/communication/TextMessages.java @@ -0,0 +1,473 @@ +package com.audacious_software.passive_data_kit.generators.communication; + +import android.Manifest; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.support.v4.content.ContextCompat; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.audacious_software.passive_data_kit.PassiveDataKit; +import com.audacious_software.passive_data_kit.PhoneUtililties; +import com.audacious_software.passive_data_kit.activities.generators.DataPointViewHolder; +import com.audacious_software.passive_data_kit.activities.generators.RequestPermissionActivity; +import com.audacious_software.passive_data_kit.diagnostics.DiagnosticAction; +import com.audacious_software.passive_data_kit.generators.Generator; +import com.audacious_software.passive_data_kit.generators.Generators; +import com.audacious_software.pdk.passivedatakit.R; +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.data.PieEntry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +public class TextMessages extends Generator { + private static final String GENERATOR_IDENTIFIER = "pdk-text-messages"; + + private static final String ENABLED = "com.audacious_software.passive_data_kit.generators.communication.TextMessages.ENABLED"; + private static final boolean ENABLED_DEFAULT = true; + + private static final Uri SMS_INBOX_URI = Uri.parse("content://sms/inbox"); + private static final Uri SMS_SENT_URI = Uri.parse("content://sms/sent"); + + private static final String SMS_DATE = "date"; + private static final String SMS_BODY = "body"; + private static final String SMS_NUMBER_NAME = "person"; + private static final String SMS_NUMBER = "address"; + private static final String SMS_LENGTH = "length"; + private static final String SMS_DIRECTION = "direction"; + + private static int DATABASE_VERSION = 1; + + private static final String TABLE_HISTORY = "history"; + private static final String HISTORY_OBSERVED = "observed"; + private static final String HISTORY_DIRECTION = "direction"; + private static final String HISTORY_LENGTH = "length"; + private static final String HISTORY_BODY = "body"; + private static final String HISTORY_NUMBER_NAME = "number_name"; + private static final String HISTORY_NUMBER = "number"; + private static final String HISTORY_DIRECTION_INCOMING = "incoming"; + private static final String HISTORY_DIRECTION_OUTGOING = "outgoing"; + + private static TextMessages sInstance = null; + + private Handler mHandler = null; + private Context mContext = null; + + private static final String DATABASE_PATH = "pdk-text-messages.sqlite"; + + private SQLiteDatabase mDatabase = null; + private long mSampleInterval = 60000; + + public static TextMessages getInstance(Context context) { + if (TextMessages.sInstance == null) { + TextMessages.sInstance = new TextMessages(context.getApplicationContext()); + } + + return TextMessages.sInstance; + } + + public TextMessages(Context context) { + super(context); + + this.mContext = context.getApplicationContext(); + } + + public static void start(final Context context) { + TextMessages.getInstance(context).startGenerator(); + } + + private void startGenerator() { + final TextMessages me = this; + + if (this.mHandler != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + this.mHandler.getLooper().quitSafely(); + } else { + this.mHandler.getLooper().quit(); + } + + this.mHandler = null; + } + + final Runnable checkLogs = new Runnable() { + @Override + public void run() { + + Log.e("PDK", "CHECK TEXT LOGS"); + + boolean approved = false; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(me.mContext, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_GRANTED){ + approved = true; + } + } else { + approved = true; + } + + if (approved) { + long lastObserved = 0; + + Cursor lastCursor = me.mDatabase.query(TextMessages.TABLE_HISTORY, null, null, null, null, null, TextMessages.HISTORY_OBSERVED + " DESC"); + + if (lastCursor.moveToNext()) { + lastObserved = lastCursor.getLong(lastCursor.getColumnIndex(TextMessages.HISTORY_OBSERVED)); + } + + lastCursor.close(); + + ArrayList toTransmit = new ArrayList<>(); + + String where = TextMessages.SMS_DATE + " > ?"; + String[] args = {"" + lastObserved}; + + Cursor c = me.mContext.getContentResolver().query(TextMessages.SMS_INBOX_URI, null, where, args, TextMessages.SMS_DATE); + + while (c.moveToNext()) { + ContentValues values = new ContentValues(); + values.put(TextMessages.HISTORY_OBSERVED, c.getLong(c.getColumnIndex(TextMessages.SMS_DATE))); + + String body = c.getString(c.getColumnIndex(TextMessages.SMS_BODY)); + + values.put(TextMessages.HISTORY_LENGTH, body.length()); + values.put(TextMessages.HISTORY_BODY, body); + + String name = c.getString(c.getColumnIndex(TextMessages.SMS_NUMBER_NAME)); + String number = c.getString(c.getColumnIndex(TextMessages.SMS_NUMBER)); + + if (name == null) { + name = number; + } + + values.put(TextMessages.HISTORY_NUMBER_NAME, name); + values.put(TextMessages.HISTORY_NUMBER, number); + + values.put(TextMessages.HISTORY_DIRECTION, TextMessages.HISTORY_DIRECTION_INCOMING); + + toTransmit.add(values); + } + + c.close(); + + c = me.mContext.getContentResolver().query(TextMessages.SMS_SENT_URI, null, where, args, TextMessages.SMS_DATE); + + while (c.moveToNext()) { + ContentValues values = new ContentValues(); + values.put(TextMessages.HISTORY_OBSERVED, c.getLong(c.getColumnIndex(TextMessages.SMS_DATE))); + + String body = c.getString(c.getColumnIndex(TextMessages.SMS_BODY)); + + values.put(TextMessages.HISTORY_LENGTH, body.length()); + values.put(TextMessages.HISTORY_BODY, body); + + String name = c.getString(c.getColumnIndex(TextMessages.SMS_NUMBER_NAME)); + String number = c.getString(c.getColumnIndex(TextMessages.SMS_NUMBER)); + + if (name == null) { + name = number; + } + + values.put(TextMessages.HISTORY_NUMBER_NAME, name); + values.put(TextMessages.HISTORY_NUMBER, number); + + values.put(TextMessages.HISTORY_DIRECTION, TextMessages.HISTORY_DIRECTION_OUTGOING); + + toTransmit.add(values); + } + + c.close(); + + Collections.sort(toTransmit, new Comparator() { + @Override + public int compare(ContentValues one, ContentValues two) { + Long oneTime = one.getAsLong(TextMessages.HISTORY_OBSERVED); + Long twoTime = two.getAsLong(TextMessages.HISTORY_OBSERVED); + + return oneTime.compareTo(twoTime); + } + }); + + for (ContentValues values : toTransmit) { + String[] sensitiveFields = { + TextMessages.HISTORY_NUMBER_NAME, + TextMessages.HISTORY_NUMBER, + TextMessages.HISTORY_BODY, + }; + + for (String field : sensitiveFields) { + if (values.containsKey(field)) { + String value = values.getAsString(field); + + if (TextMessages.HISTORY_NUMBER.equals(TextMessages.HISTORY_NUMBER)) { + value = PhoneUtililties.normalizedPhoneNumber(value); + } + + values.put(field, new String(Hex.encodeHex(DigestUtils.sha256(value)))); + } + } + + Bundle bundle = new Bundle(); + bundle.putLong(TextMessages.SMS_DATE, values.getAsLong(TextMessages.HISTORY_OBSERVED)); + bundle.putInt(TextMessages.SMS_LENGTH, values.getAsInteger(TextMessages.HISTORY_LENGTH)); + bundle.putString(TextMessages.SMS_NUMBER_NAME, values.getAsString(TextMessages.HISTORY_NUMBER_NAME)); + bundle.putString(TextMessages.SMS_NUMBER, values.getAsString(TextMessages.HISTORY_NUMBER)); + bundle.putString(TextMessages.SMS_DIRECTION, values.getAsString(TextMessages.HISTORY_DIRECTION)); + bundle.putString(TextMessages.SMS_BODY, values.getAsString(TextMessages.HISTORY_BODY)); + + me.mDatabase.insert(TextMessages.TABLE_HISTORY, null, values); + + Generators.getInstance(me.mContext).notifyGeneratorUpdated(TextMessages.GENERATOR_IDENTIFIER, bundle); + } + } + + if (me.mHandler != null) { + me.mHandler.postDelayed(this, me.mSampleInterval); + } + } + }; + + File path = PassiveDataKit.getGeneratorsStorage(this.mContext); + + path = new File(path, TextMessages.DATABASE_PATH); + + this.mDatabase = SQLiteDatabase.openOrCreateDatabase(path, null); + + int version = this.getDatabaseVersion(this.mDatabase); + + switch (version) { + case 0: + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_text_messages_create_history_table)); + } + + this.setDatabaseVersion(this.mDatabase, TextMessages.DATABASE_VERSION); + + Runnable r = new Runnable() { + @Override + public void run() { + Looper.prepare(); + + me.mHandler = new Handler(); + + Looper.loop(); + } + }; + + Thread t = new Thread(r); + t.start(); + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + me.mHandler.post(checkLogs); + + Generators.getInstance(this.mContext).registerCustomViewClass(TextMessages.GENERATOR_IDENTIFIER, TextMessages.class); + } + + public static boolean isEnabled(Context context) { + SharedPreferences prefs = Generators.getInstance(context).getSharedPreferences(context); + + return prefs.getBoolean(TextMessages.ENABLED, TextMessages.ENABLED_DEFAULT); + } + + public static boolean isRunning(Context context) { + if (TextMessages.sInstance == null) { + return false; + } + + return TextMessages.sInstance.mHandler != null; + } + + public static ArrayList diagnostics(final Context context) { + ArrayList actions = new ArrayList<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED){ + final Handler handler = new Handler(Looper.getMainLooper()); + + actions.add(new DiagnosticAction(context.getString(R.string.diagnostic_sms_log_permission_required_title), context.getString(R.string.diagnostic_sms_log_permission_required), new Runnable() { + + @Override + public void run() { + handler.post(new Runnable() { + + @Override + public void run() { + Intent intent = new Intent(context, RequestPermissionActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(RequestPermissionActivity.PERMISSION, Manifest.permission.READ_SMS); + + context.startActivity(intent); + } + }); + } + })); + } + } + + return actions; + } + + public static void bindViewHolder(DataPointViewHolder holder) { + final Context context = holder.itemView.getContext(); + + long lastTimestamp = 0; + int lastLength = 0; + + long totalIncoming = 0; + long totalOutgoing = 0; + long total = 0; + + TextMessages generator = TextMessages.getInstance(holder.itemView.getContext()); + String lastDirection = null; + + Cursor c = generator.mDatabase.query(TextMessages.TABLE_HISTORY, null, null, null, null, null, TextMessages.HISTORY_OBSERVED + " DESC"); + + while (c.moveToNext()) { + if (lastTimestamp == 0) { + lastTimestamp = c.getLong(c.getColumnIndex(TextMessages.HISTORY_OBSERVED)); + lastDirection = c.getString(c.getColumnIndex(TextMessages.HISTORY_DIRECTION)); + lastLength = c.getInt(c.getColumnIndex(TextMessages.HISTORY_LENGTH)); + } + + total += 1; + + String direction = c.getString(c.getColumnIndex(TextMessages.HISTORY_DIRECTION)); + + if (TextMessages.HISTORY_DIRECTION_INCOMING.equals(direction)) { + totalIncoming += 1; + } else if (TextMessages.HISTORY_DIRECTION_OUTGOING.equals(direction)) { + totalOutgoing += 1; + } + } + + c.close(); + + View cardContent = holder.itemView.findViewById(R.id.card_content); + View cardEmpty = holder.itemView.findViewById(R.id.card_empty); + TextView dateLabel = (TextView) holder.itemView.findViewById(R.id.generator_data_point_date); + + if (total > 0) { + cardContent.setVisibility(View.VISIBLE); + cardEmpty.setVisibility(View.GONE); + + dateLabel.setText(Generator.formatTimestamp(context, lastTimestamp)); + + PieChart pieChart = (PieChart) holder.itemView.findViewById(R.id.chart_text_messages); + pieChart.getLegend().setEnabled(false); + + pieChart.setEntryLabelColor(android.R.color.transparent); + pieChart.getDescription().setEnabled(false); + pieChart.setDrawHoleEnabled(false); + + List entries = new ArrayList<>(); + + if (totalIncoming > 0) { + entries.add(new PieEntry(totalIncoming, context.getString(R.string.generator_text_messages_incoming_label))); + } + + if (totalOutgoing > 0) { + entries.add(new PieEntry(totalOutgoing, context.getString(R.string.generator_text_messages_outgoing_label))); + } + + PieDataSet set = new PieDataSet(entries, " "); + + int[] colors = { + R.color.generator_text_messages_incoming, + R.color.generator_text_messages_outgoing + }; + + set.setColors(colors, context); + + PieData data = new PieData(set); + data.setValueTextSize(14); + data.setValueTypeface(Typeface.DEFAULT_BOLD); + data.setValueTextColor(0xffffffff); + + data.setValueFormatter(new IValueFormatter() { + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + return "" + ((Float) value).intValue(); + } + }); + + pieChart.setData(data); + pieChart.invalidate(); + + TextView latestField = (TextView) holder.itemView.findViewById(R.id.field_latest_text_message); + TextView lengthField = (TextView) holder.itemView.findViewById(R.id.field_length); + TextView directionField = (TextView) holder.itemView.findViewById(R.id.field_direction); + + Date lateDate = new Date(lastTimestamp); + String day = android.text.format.DateFormat.getMediumDateFormat(context).format(lateDate); + String time = android.text.format.DateFormat.getTimeFormat(context).format(lateDate); + + latestField.setText(context.getString(R.string.format_full_timestamp_pdk, day, time)); + lengthField.setText(context.getString(R.string.generator_text_messages_length_format, lastLength)); + directionField.setText(lastDirection); + + dateLabel.setText(Generator.formatTimestamp(context, lastTimestamp / 1000)); + } else { + cardContent.setVisibility(View.GONE); + cardEmpty.setVisibility(View.VISIBLE); + + dateLabel.setText(R.string.label_never_pdk); + } + } + + @Override + public List fetchPayloads() { + return new ArrayList<>(); + } + + public static View fetchView(ViewGroup parent) + { + return LayoutInflater.from(parent.getContext()).inflate(R.layout.card_generator_text_messages, parent, false); + } + + public static long latestPointGenerated(Context context) { + long timestamp = 0; + + TextMessages me = TextMessages.getInstance(context); + + Cursor c = me.mDatabase.query(TextMessages.TABLE_HISTORY, null, null, null, null, null, TextMessages.HISTORY_OBSERVED + " DESC"); + + if (c.moveToNext()) { + timestamp = c.getLong(c.getColumnIndex(TextMessages.HISTORY_OBSERVED)); + } + + c.close(); + + return timestamp; + } +} diff --git a/src/com/audacious_software/passive_data_kit/generators/device/Location.java b/src/com/audacious_software/passive_data_kit/generators/device/Location.java index 814c5ad..058f4ec 100755 --- a/src/com/audacious_software/passive_data_kit/generators/device/Location.java +++ b/src/com/audacious_software/passive_data_kit/generators/device/Location.java @@ -1,26 +1,51 @@ package com.audacious_software.passive_data_kit.generators.device; import android.Manifest; +import android.app.Activity; +import android.content.ContentValues; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.ColorStateList; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.location.Address; +import android.location.Geocoder; import android.location.LocationManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; -import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.SwitchCompat; import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.webkit.WebView; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import com.audacious_software.passive_data_kit.DeviceInformation; +import com.audacious_software.passive_data_kit.PassiveDataKit; +import com.audacious_software.passive_data_kit.activities.DataDisclosureDetailActivity; import com.audacious_software.passive_data_kit.activities.generators.DataPointViewHolder; +import com.audacious_software.passive_data_kit.activities.generators.GeneratorViewHolder; import com.audacious_software.passive_data_kit.activities.generators.RequestPermissionActivity; import com.audacious_software.passive_data_kit.diagnostics.DiagnosticAction; import com.audacious_software.passive_data_kit.generators.Generator; @@ -35,16 +60,23 @@ import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.MapView; import com.google.android.gms.maps.OnMapReadyCallback; -import com.google.android.gms.maps.UiSettings; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.MarkerOptions; +import com.google.maps.android.ui.IconGenerator; +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; import java.util.ArrayList; +import java.util.List; +import java.util.Random; @SuppressWarnings("unused") public class Location extends Generator implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { private static final String GENERATOR_IDENTIFIER = "pdk-location"; + private static final String DATABASE_PATH = "pdk-location.sqlite"; private static final String ENABLED = "com.audacious_software.passive_data_kit.generators.device.Location.ENABLED"; private static final boolean ENABLED_DEFAULT = true; @@ -61,18 +93,43 @@ public class Location extends Generator implements GoogleApiClient.ConnectionCal private static final String BEARING_KEY = "bearing"; private static final String SPEED_KEY = "speed"; private static final String EXTRAS_KEY = "extras"; + private static final String SETTING_DISPLAY_HYBRID_MAP = "com.audacious_software.passive_data_kit.generators.device.Location.SETTING_DISPLAY_HYBRID_MAP"; + private static final boolean SETTING_DISPLAY_HYBRID_MAP_DEFAULT = true; - private static final String LAST_KNOWN_LATITUDE = "com.audacious_software.passive_data_kit.generators.device.Location.LAST_KNOWN_LATITUDE"; - private static final float LAST_KNOWN_LATITUDE_DEFAULT = 0; + private static String ACCURACY_MODE = "com.audacious_software.passive_data_kit.generators.device.Location.ACCURACY_MODE"; - private static final String LAST_KNOWN_LONGITUDE = "com.audacious_software.passive_data_kit.generators.device.Location.LAST_KNOWN_LONGITUDE"; - private static final float LAST_KNOWN_LONGITUDE_DEFAULT = 0; + private static int ACCURACY_BEST = 0; + private static int ACCURACY_RANDOMIZED = 1; + private static int ACCURACY_USER = 2; + private static int ACCURACY_DISABLED = 3; - private static final String LAST_KNOWN_TIMESTAMP = "com.audacious_software.passive_data_kit.generators.device.Location.LAST_KNOWN_TIMESTAMP";; + private static final String ACCURACY_MODE_RANDOMIZED_RANGE = "com.audacious_software.passive_data_kit.generators.device.Location.ACCURACY_MODE_RANDOMIZED_RANGE"; + private static final long ACCURACY_MODE_RANDOMIZED_RANGE_DEFAULT = 100; + + private static final String ACCURACY_MODE_USER_LOCATION = "com.audacious_software.passive_data_kit.generators.device.Location.ACCURACY_MODE_USER_LOCATION"; + private static final String ACCURACY_MODE_USER_LOCATION_DEFAULT = "Chicago, Illinois"; + + private static final String ACCURACY_MODE_USER_LOCATION_LATITUDE = "com.audacious_software.passive_data_kit.generators.device.Location.ACCURACY_MODE_USER_LOCATION_LATITUDE"; + private static final String ACCURACY_MODE_USER_LOCATION_LONGITUDE = "com.audacious_software.passive_data_kit.generators.device.Location.ACCURACY_MODE_USER_LOCATION_LONGITUDE"; private static Location sInstance = null; private GoogleApiClient mGoogleApiClient = null; private android.location.Location mLastLocation = null; + private long mUpdateInterval = 60000; + + private SQLiteDatabase mDatabase = null; + private static int DATABASE_VERSION = 1; + + private static final String TABLE_HISTORY = "history"; + public static final String HISTORY_OBSERVED = "observed"; + public static final String HISTORY_LATITUDE = "latitude"; + public static final String HISTORY_LONGITUDE = "longitude"; + public static final String HISTORY_ALTITUDE = "altitude"; + public static final String HISTORY_BEARING = "bearing"; + public static final String HISTORY_SPEED = "speed"; + public static final String HISTORY_PROVIDER = "provider"; + public static final String HISTORY_LOCATION_TIMESTAMP = "location_timestamp"; + public static final String HISTORY_ACCURACY = "accuracy"; public static Location getInstance(Context context) { if (Location.sInstance == null) { @@ -114,7 +171,6 @@ else if (Location.useGoogleLocationServices(me.mContext)) me.mGoogleApiClient = builder.build(); me.mGoogleApiClient.connect(); } - } else { @@ -128,6 +184,31 @@ else if (Location.useGoogleLocationServices(me.mContext)) t.start(); Generators.getInstance(this.mContext).registerCustomViewClass(Location.GENERATOR_IDENTIFIER, Location.class); + + File path = PassiveDataKit.getGeneratorsStorage(this.mContext); + + path = new File(path, Location.DATABASE_PATH); + + this.mDatabase = SQLiteDatabase.openOrCreateDatabase(path, null); + + int version = this.getDatabaseVersion(this.mDatabase); + + switch (version) { + case 0: + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_location_create_history_table)); + } + + this.setDatabaseVersion(this.mDatabase, Location.DATABASE_VERSION); + } + + private void stopGenerator() { + if (this.mGoogleApiClient != null) { + this.mGoogleApiClient.disconnect(); + this.mGoogleApiClient = null; + } + + this.mDatabase.close(); + this.mDatabase = null; } public static boolean useGoogleLocationServices(Context context) { @@ -183,7 +264,7 @@ private ArrayList runDiagostics() { int permissionCheck = ContextCompat.checkSelfPermission(this.mContext, Manifest.permission.ACCESS_FINE_LOCATION); if (permissionCheck != PackageManager.PERMISSION_GRANTED) { - actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_location_permission), new Runnable() { + actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_location_permission_title), me.mContext.getString(R.string.diagnostic_missing_location_permission), new Runnable() { @Override public void run() { @@ -206,13 +287,13 @@ public void run() { return actions; } - @Override public void onConnected(Bundle bundle) { final LocationRequest request = new LocationRequest(); request.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY); - request.setInterval(60000); + request.setFastestInterval(this.mUpdateInterval); + request.setInterval(this.mUpdateInterval); if (this.mGoogleApiClient != null && this.mGoogleApiClient.isConnected()) { if (ContextCompat.checkSelfPermission(this.mContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { @@ -239,78 +320,139 @@ public void onLocationChanged(android.location.Location location) { long now = System.currentTimeMillis(); - Bundle bundle = new Bundle(); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.mContext); + int selected = prefs.getInt(Location.ACCURACY_MODE, Location.ACCURACY_BEST); - bundle.putDouble(Location.LATITUDE_KEY, location.getLatitude()); - bundle.putDouble(Location.LONGITUDE_KEY, location.getLongitude()); - bundle.putDouble(Location.FIX_TIMESTAMP_KEY, ((double) location.getTime()) / 1000); - bundle.putString(Location.PROVIDER_KEY, location.getProvider()); + if (selected == Location.ACCURACY_RANDOMIZED) { + // http://gis.stackexchange.com/a/68275/10230 - this.mLastLocation = location; + double latitude = location.getLatitude(); + double longitude = location.getLongitude(); - if (location.hasAccuracy()) { - bundle.putFloat(Location.ACCURACY_KEY, location.getAccuracy()); + double radius = prefs.getLong(Location.ACCURACY_MODE_RANDOMIZED_RANGE, Location.ACCURACY_MODE_RANDOMIZED_RANGE_DEFAULT); + + double radiusInDegrees = radius / 111000; + + Random r = new SecureRandom(); + + double u = r.nextDouble(); + double v = r.nextDouble(); + + double w = radiusInDegrees * Math.sqrt(u); + double t = 2 * Math.PI * v; + double x = w * Math.cos(t); + double y = w * Math.sin(t); + + // Adjust the x-coordinate for the shrinking of the east-west distances + longitude = longitude + (x / Math.cos(latitude)); + latitude = y + latitude; + + location.setLongitude(longitude); + location.setLatitude(latitude); } + ContentValues values = new ContentValues(); + values.put(Location.HISTORY_OBSERVED, System.currentTimeMillis()); + values.put(Location.HISTORY_LATITUDE, location.getLatitude()); + values.put(Location.HISTORY_LONGITUDE, location.getLongitude()); + values.put(Location.HISTORY_PROVIDER, location.getProvider()); + values.put(Location.HISTORY_LOCATION_TIMESTAMP, location.getTime()); + + Bundle updated = new Bundle(); + updated.putLong(Location.HISTORY_OBSERVED, System.currentTimeMillis()); + updated.putDouble(Location.HISTORY_LATITUDE, location.getLatitude()); + updated.putDouble(Location.HISTORY_LONGITUDE, location.getLongitude()); + updated.putString(Location.HISTORY_PROVIDER, location.getProvider()); + updated.putLong(Location.HISTORY_LOCATION_TIMESTAMP, location.getTime()); + + Bundle metadata = new Bundle(); + metadata.putDouble(Generator.LATITUDE, location.getLatitude()); + metadata.putDouble(Generator.LONGITUDE, location.getLongitude()); + + updated.putBundle(Generator.PDK_METADATA, metadata); + if (location.hasAltitude()) { - bundle.putDouble(Location.ALTITUDE_KEY, location.getAltitude()); + values.put(Location.HISTORY_ALTITUDE, location.getAltitude()); + updated.putDouble(Location.HISTORY_ALTITUDE, location.getAltitude()); } if (location.hasBearing()) { - bundle.putFloat(Location.BEARING_KEY, location.getBearing()); + values.put(Location.HISTORY_BEARING, location.getBearing()); + updated.putDouble(Location.HISTORY_BEARING, location.getBearing()); } if (location.hasSpeed()) { - bundle.putFloat(Location.SPEED_KEY, location.getSpeed()); + values.put(Location.HISTORY_SPEED, location.getBearing()); + updated.putDouble(Location.HISTORY_SPEED, location.getBearing()); } - Bundle extras = location.getExtras(); - - if (extras != null) { - bundle.putBundle(Location.EXTRAS_KEY, extras); + if (location.hasAccuracy()) { + values.put(Location.HISTORY_ACCURACY, location.getAccuracy()); + updated.putDouble(Location.HISTORY_ACCURACY, location.getAccuracy()); } - Generators.getInstance(this.mContext).transmitData(Location.GENERATOR_IDENTIFIER, bundle); + this.mDatabase.insert(Location.TABLE_HISTORY, null, values); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.mContext); - SharedPreferences.Editor e = prefs.edit(); + Generators.getInstance(this.mContext).notifyGeneratorUpdated(Location.GENERATOR_IDENTIFIER, updated); + } - e.putFloat(Location.LAST_KNOWN_LATITUDE, (float) location.getLatitude()); - e.putFloat(Location.LAST_KNOWN_LONGITUDE, (float) location.getLongitude()); - e.putLong(Location.LAST_KNOWN_TIMESTAMP, System.currentTimeMillis()); + public static long latestPointGenerated(Context context) { + long timestamp = 0; - e.apply(); - } + Location me = Location.getInstance(context); - public static void bindViewHolder(DataPointViewHolder holder, final Bundle dataPoint) { - Log.e("PDK", "DRAWING LOCATION: " + dataPoint); + Cursor c = me.mDatabase.query(Location.TABLE_HISTORY, null, null, null, null, null, Location.HISTORY_OBSERVED + " DESC"); + + if (c.moveToNext()) { + timestamp = c.getLong(c.getColumnIndex(Location.HISTORY_OBSERVED)); + } + + c.close(); + + return timestamp; + } + public static void bindViewHolder(DataPointViewHolder holder) { final Context context = holder.itemView.getContext(); - String identifier = dataPoint.getBundle(Generator.PDK_METADATA).getString(Generator.IDENTIFIER); + Location me = Location.getInstance(context); - double timestamp = dataPoint.getBundle(Generator.PDK_METADATA).getDouble(Generator.TIMESTAMP); + double lastLatitude = 0.0; + double lastLongitude = 0.0; + long timestamp = 0; - double latitude = Location.LAST_KNOWN_LATITUDE_DEFAULT; - double longitude = Location.LAST_KNOWN_LONGITUDE_DEFAULT; + final List locations = new ArrayList<>(); - if (dataPoint.containsKey(Location.LATITUDE_KEY) && dataPoint.containsKey(Location.LONGITUDE_KEY)) { - latitude = dataPoint.getDouble(Location.LATITUDE_KEY); - longitude = dataPoint.getDouble(Location.LONGITUDE_KEY); - } else { // Empty point - retrieve last known location... - Log.e("PDK", "FETCHING LAST KNOWN LOCATION..."); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String where = Location.HISTORY_OBSERVED + " > ?"; + String[] args = { "" + (System.currentTimeMillis() - (1000 * 60 * 60 * 24)) }; - latitude = prefs.getFloat(Location.LAST_KNOWN_LATITUDE, Location.LAST_KNOWN_LATITUDE_DEFAULT); - longitude = prefs.getFloat(Location.LAST_KNOWN_LONGITUDE, Location.LAST_KNOWN_LONGITUDE_DEFAULT); - timestamp = ((double) prefs.getLong(Location.LAST_KNOWN_TIMESTAMP, System.currentTimeMillis())) / 1000 ; + Cursor c = me.mDatabase.query(Location.TABLE_HISTORY, null, where, args, null, null, Location.HISTORY_OBSERVED); + + while (c.moveToNext()) { + lastLatitude = c.getDouble(c.getColumnIndex(Location.HISTORY_LATITUDE)); + lastLongitude = c.getDouble(c.getColumnIndex(Location.HISTORY_LONGITUDE)); + timestamp = c.getLong(c.getColumnIndex(Location.HISTORY_OBSERVED)); + + LatLng location = new LatLng(lastLatitude, lastLongitude); + + locations.add(location); } + c.close(); + TextView dateLabel = (TextView) holder.itemView.findViewById(R.id.generator_data_point_date); - dateLabel.setText(Generator.formatTimestamp(context, timestamp)); - final double finalLatitude = latitude; - final double finalLongitude = longitude; + if (timestamp > 0) { + dateLabel.setText(Generator.formatTimestamp(context, timestamp / 1000)); + } else { + dateLabel.setText(R.string.label_never_pdk); + } + + final double finalLatitude = lastLatitude; + final double finalLongitude = lastLongitude; + + final DisplayMetrics metrics = new DisplayMetrics(); + ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(metrics); if (Location.useKindleLocationServices()) { @@ -322,24 +464,95 @@ else if (Location.useGoogleLocationServices(holder.itemView.getContext())) final MapView mapView = (MapView) holder.itemView.findViewById(R.id.map_view); mapView.onCreate(null); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + final boolean useHybrid = prefs.getBoolean(Location.SETTING_DISPLAY_HYBRID_MAP, Location.SETTING_DISPLAY_HYBRID_MAP_DEFAULT); + + SwitchCompat hybridSwitch = (SwitchCompat) holder.itemView.findViewById(R.id.pdk_google_location_map_type_hybrid); + hybridSwitch.setChecked(useHybrid); + + hybridSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, final boolean checked) { + SharedPreferences.Editor e = prefs.edit(); + e.putBoolean(Location.SETTING_DISPLAY_HYBRID_MAP, checked); + e.apply(); + + mapView.getMapAsync(new OnMapReadyCallback() { + public void onMapReady(GoogleMap googleMap) { + if (checked) { + googleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + } else { + googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); + } + } + }); + } + }); + + ColorStateList buttonStates = new ColorStateList( + new int[][]{ + new int[]{android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled}, + new int[]{} + }, + new int[]{ + 0xfff1f1f1, + 0x1c000000, + 0xff33691E + } + ); + + DrawableCompat.setTintList(hybridSwitch.getThumbDrawable(), buttonStates); + + IconGenerator iconGen = new IconGenerator(context); + + Drawable shapeDrawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_location_heatmap_marker, null); + iconGen.setBackground(shapeDrawable); + + View view = new View(context); + view.setLayoutParams(new ViewGroup.LayoutParams(8, 8)); + iconGen.setContentView(view); + + final Bitmap bitmap = iconGen.makeIcon(); + mapView.getMapAsync(new OnMapReadyCallback() { public void onMapReady(GoogleMap googleMap) { - googleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); - googleMap.getUiSettings().setZoomControlsEnabled(false); + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + googleMap.setMyLocationEnabled(true); + } + + if (useHybrid) { + googleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + } else { + googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); + } + + googleMap.getUiSettings().setZoomControlsEnabled(true); googleMap.getUiSettings().setMyLocationButtonEnabled(false); + googleMap.getUiSettings().setMapToolbarEnabled(false); + googleMap.getUiSettings().setAllGesturesEnabled(false); + + LatLngBounds.Builder builder = new LatLngBounds.Builder(); - googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(finalLatitude, finalLongitude), 14)); + for (LatLng latlng : locations) { + builder.include(latlng); + } - googleMap.addMarker(new MarkerOptions() - .position(new LatLng(finalLatitude, finalLongitude))); -// .icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_marker_none))); + if (locations.size() > 0) { + googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), (int) (16 * metrics.density))); + } DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - googleMap.setPadding(0, 0, 0, (int) (32 * metrics.density)); + for (LatLng latLng : locations) { + googleMap.addMarker(new MarkerOptions() + .position(latLng) + .icon(BitmapDescriptorFactory.fromBitmap(bitmap))); + } - UiSettings settings = googleMap.getUiSettings(); - settings.setMapToolbarEnabled(false); + mapView.onResume(); } }); } @@ -348,9 +561,11 @@ public void onMapReady(GoogleMap googleMap) { // TODO throw new RuntimeException("Throw rocks at developer to implement generic location support."); } + } - TextView description = (TextView) holder.itemView.findViewById(R.id.generator_location_description); - description.setText(context.getResources().getString(R.string.generator_location_value, latitude, longitude)); + @Override + public List fetchPayloads() { + return new ArrayList(); } public static View fetchView(ViewGroup parent) @@ -371,7 +586,347 @@ else if (Location.useGoogleLocationServices(parent.getContext())) } } + public static String getGeneratorTitle(Context context) { + return context.getString(R.string.generator_location); + } + + public static List getDisclosureActions(final Context context) { + List actions = new ArrayList<>(); + + DataDisclosureDetailActivity.Action disclosure = new DataDisclosureDetailActivity.Action(); + + disclosure.title = context.getString(R.string.label_data_collection_description); + disclosure.subtitle = context.getString(R.string.label_data_collection_description_more); + + WebView disclosureView = new WebView(context); + disclosureView.loadUrl("file:///android_asset/html/passive_data_kit/generator_location_disclosure.html"); + + disclosure.view = disclosureView; + + actions.add(disclosure); + + DataDisclosureDetailActivity.Action accuracy = new DataDisclosureDetailActivity.Action(); + accuracy.title = context.getString(R.string.label_data_collection_location_accuracy); + accuracy.subtitle = context.getString(R.string.label_data_collection_location_accuracy_more); + + final Integer[] options = { Location.ACCURACY_BEST, Location.ACCURACY_RANDOMIZED, Location.ACCURACY_USER, Location.ACCURACY_DISABLED }; + + final ListView listView = new ListView(context); + + final ArrayAdapter accuracyAdapter = new ArrayAdapter(context, R.layout.row_disclosure_location_accuracy_pdk, options) { + public View getView (final int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(context).inflate(R.layout.row_disclosure_location_accuracy_pdk, null); + } + + final Integer option = options[position]; + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + int selected = prefs.getInt(Location.ACCURACY_MODE, Location.ACCURACY_BEST); + + CheckBox checked = (CheckBox) convertView.findViewById(R.id.action_checked); + + checked.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + + } + }); + + checked.setChecked(selected == option); + + TextView title = (TextView) convertView.findViewById(R.id.action_title); + TextView description = (TextView) convertView.findViewById(R.id.action_description); + + if (option == Location.ACCURACY_BEST) { + title.setText(R.string.label_data_collection_location_accuracy_best); + description.setText(R.string.label_data_collection_location_accuracy_best_more); + } else if (option == Location.ACCURACY_RANDOMIZED) { + title.setText(R.string.label_data_collection_location_accuracy_randomized); + description.setText(R.string.label_data_collection_location_accuracy_randomized_more); + } else if (option == Location.ACCURACY_USER) { + title.setText(R.string.label_data_collection_location_accuracy_user); + description.setText(R.string.label_data_collection_location_accuracy_user_more); + } else if (option == Location.ACCURACY_DISABLED) { + title.setText(R.string.label_data_collection_location_accuracy_disabled); + description.setText(R.string.label_data_collection_location_accuracy_disabled_more); + } + + final ArrayAdapter meAdapter = this; + + checked.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(final CompoundButton compoundButton, final boolean checked) { + Log.e("PDK", "CHECK CHANGE!"); + + final CompoundButton.OnCheckedChangeListener me = this; + + if (option == Location.ACCURACY_BEST) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.title_location_accuracy_best); + builder.setMessage(R.string.message_location_accuracy_best); + + builder.setPositiveButton(R.string.action_continue, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + SharedPreferences.Editor e = prefs.edit(); + e.putInt(Location.ACCURACY_MODE, option); + e.apply(); + + meAdapter.notifyDataSetChanged(); + } + }); + + builder.create().show(); + } else if (option == Location.ACCURACY_RANDOMIZED) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.title_location_accuracy_randomized); + + View body = LayoutInflater.from(context).inflate(R.layout.dialog_location_randomized, null); + builder.setView(body); + + final EditText rangeField = (EditText) body.findViewById(R.id.random_range); + + long existingRange = prefs.getLong(Location.ACCURACY_MODE_RANDOMIZED_RANGE, Location.ACCURACY_MODE_RANDOMIZED_RANGE_DEFAULT); + + rangeField.setText("" + existingRange); + + builder.setPositiveButton(R.string.action_continue, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + SharedPreferences.Editor e = prefs.edit(); + + long randomRange = Long.parseLong(rangeField.getText().toString()); + + e.putLong(Location.ACCURACY_MODE_RANDOMIZED_RANGE, randomRange); + + e.putInt(Location.ACCURACY_MODE, option); + e.apply(); + + meAdapter.notifyDataSetChanged(); + } + }); + + builder.create().show(); + } else if (option == Location.ACCURACY_USER) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.title_location_accuracy_user); + + View body = LayoutInflater.from(context).inflate(R.layout.dialog_location_user, null); + builder.setView(body); + + final EditText locationField = (EditText) body.findViewById(R.id.user_location); + + String existingLocation = prefs.getString(Location.ACCURACY_MODE_USER_LOCATION, Location.ACCURACY_MODE_USER_LOCATION_DEFAULT); + + locationField.setText(existingLocation); + + builder.setPositiveButton(R.string.action_continue, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + SharedPreferences.Editor e = prefs.edit(); + + String location = locationField.getText().toString(); + + e.putString(Location.ACCURACY_MODE_USER_LOCATION, location); + + try { + List
results = (new Geocoder(context)).getFromLocationName(location, 1); + + if (results.size() > 0) { + Address match = results.get(0); + + e.putFloat(Location.ACCURACY_MODE_USER_LOCATION_LATITUDE, (float) match.getLatitude()); + e.putFloat(Location.ACCURACY_MODE_USER_LOCATION_LONGITUDE, (float) match.getLongitude()); + } else { + Toast.makeText(context, R.string.toast_location_lookup_failed, Toast.LENGTH_LONG).show(); + + me.onCheckedChanged(compoundButton, checked); + } + } catch (IOException e1) { + e1.printStackTrace(); + } + + e.putInt(Location.ACCURACY_MODE, option); + e.apply(); + + meAdapter.notifyDataSetChanged(); + } + }); + + builder.create().show(); + } else if (option == Location.ACCURACY_DISABLED) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.title_location_accuracy_disabled); + builder.setMessage(R.string.message_location_accuracy_disabled); + + builder.setPositiveButton(R.string.action_continue, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + SharedPreferences.Editor e = prefs.edit(); + e.putInt(Location.ACCURACY_MODE, option); + e.apply(); + + meAdapter.notifyDataSetChanged(); + } + }); + + builder.create().show(); + } + + } + }); + + return convertView; + } + }; + + listView.setAdapter(accuracyAdapter); + + accuracy.view = listView; + + actions.add(accuracy); + + return actions; + } + + public static View getDisclosureDataView(final GeneratorViewHolder holder) { + final Context context = holder.itemView.getContext(); + + if (Location.useKindleLocationServices()) + { + // TODO + throw new RuntimeException("Throw rocks at developer to implement Kindle support."); + } + else if (Location.useGoogleLocationServices(holder.itemView.getContext())) + { + final MapView mapView = new MapView(context); + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + mapView.setLayoutParams(params); + + mapView.onCreate(null); + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + final boolean useHybrid = prefs.getBoolean(Location.SETTING_DISPLAY_HYBRID_MAP, Location.SETTING_DISPLAY_HYBRID_MAP_DEFAULT); + + IconGenerator iconGen = new IconGenerator(context); + + Drawable shapeDrawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_location_heatmap_marker, null); + iconGen.setBackground(shapeDrawable); + + View view = new View(context); + view.setLayoutParams(new ViewGroup.LayoutParams(8, 8)); + iconGen.setContentView(view); + + final Bitmap bitmap = iconGen.makeIcon(); + + mapView.getMapAsync(new OnMapReadyCallback() { + public void onMapReady(GoogleMap googleMap) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + googleMap.setMyLocationEnabled(true); + } + + if (useHybrid) { + googleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + } else { + googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); + } + + googleMap.getUiSettings().setZoomControlsEnabled(true); + googleMap.getUiSettings().setMyLocationButtonEnabled(false); + googleMap.getUiSettings().setMapToolbarEnabled(false); + googleMap.getUiSettings().setAllGesturesEnabled(false); + + Location me = Location.getInstance(context); + + double lastLatitude = 0.0; + double lastLongitude = 0.0; + long timestamp = 0; + + final List locations = new ArrayList<>(); + + String where = Location.HISTORY_OBSERVED + " > ?"; + String[] args = { "" + (System.currentTimeMillis() - (1000 * 60 * 60 * 24)) }; + + Cursor c = me.mDatabase.query(Location.TABLE_HISTORY, null, where, args, null, null, Location.HISTORY_OBSERVED); + + while (c.moveToNext()) { + lastLatitude = c.getDouble(c.getColumnIndex(Location.HISTORY_LATITUDE)); + lastLongitude = c.getDouble(c.getColumnIndex(Location.HISTORY_LONGITUDE)); + + LatLng location = new LatLng(lastLatitude, lastLongitude); + + locations.add(location); + } + + c.close(); + + LatLngBounds.Builder builder = new LatLngBounds.Builder(); + + for (LatLng latlng : locations) { + builder.include(latlng); + } + + final DisplayMetrics metrics = new DisplayMetrics(); + ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(metrics); + + if (locations.size() > 0) { + googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), (int) (16 * metrics.density))); + } + + for (LatLng latLng : locations) { + googleMap.addMarker(new MarkerOptions() + .position(latLng) + .icon(BitmapDescriptorFactory.fromBitmap(bitmap))); + } + + mapView.onResume(); + } + }); + + return mapView; + } + else + { + // TODO + throw new RuntimeException("Throw rocks at developer to implement generic location support."); + } + } + + public static void bindDisclosureViewHolder(final GeneratorViewHolder holder) { + Class currentClass = new Object() { }.getClass().getEnclosingClass(); + + String identifier = currentClass.getCanonicalName(); + + TextView generatorLabel = (TextView) holder.itemView.findViewById(R.id.label_generator); + + generatorLabel.setText(Location.getGeneratorTitle(holder.itemView.getContext())); + } + public android.location.Location getLastKnownLocation() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.mContext); + int selected = prefs.getInt(Location.ACCURACY_MODE, Location.ACCURACY_BEST); + + if (selected == Location.ACCURACY_USER) { + double latitude = prefs.getFloat(Location.ACCURACY_MODE_USER_LOCATION_LATITUDE, 0); + double longitude = prefs.getFloat(Location.ACCURACY_MODE_USER_LOCATION_LONGITUDE, 0); + + android.location.Location location = new android.location.Location(""); + location.setLatitude(latitude); + location.setLongitude(longitude); + + return location; + } else if (selected == Location.ACCURACY_DISABLED) { + android.location.Location location = new android.location.Location(""); + location.setLatitude(41.8781); + location.setLongitude(-87.6298); + + return location; + } + if (this.mLastLocation != null) { return this.mLastLocation; } @@ -380,26 +935,55 @@ public android.location.Location getLastKnownLocation() { android.location.Location last = null; - if (ContextCompat.checkSelfPermission(this.mContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && + if (ContextCompat.checkSelfPermission(this.mContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this.mContext, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - Log.e("FC", "LOCATION PERMISSIONS GRANTED..."); - last = locations.getLastKnownLocation(LocationManager.GPS_PROVIDER); - Log.e("FC", "GPS: " + last); - if (last == null) { last = locations.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); - - Log.e("FC", "NETWORK: " + last); } } + if (selected == Location.ACCURACY_RANDOMIZED) { + // http://gis.stackexchange.com/a/68275/10230 + + double latitude = last.getLatitude(); + double longitude = last.getLongitude(); + + double radius = prefs.getLong(Location.ACCURACY_MODE_RANDOMIZED_RANGE, Location.ACCURACY_MODE_RANDOMIZED_RANGE_DEFAULT); + + double radiusInDegrees = radius / 111000; + + Random r = new SecureRandom(); + + double u = r.nextDouble(); + double v = r.nextDouble(); + + double w = radiusInDegrees * Math.sqrt(u); + double t = 2 * Math.PI * v; + double x = w * Math.cos(t); + double y = w * Math.sin(t); + + // Adjust the x-coordinate for the shrinking of the east-west distances + longitude = longitude + (x / Math.cos(latitude)); + latitude = y + latitude; + + last.setLongitude(longitude); + last.setLatitude(latitude); + } + return last; } - public static void broadcastLatestDataPoint(Context context) { - Generators.getInstance(context).transmitData(Location.GENERATOR_IDENTIFIER, new Bundle()); + public void setUpdateInterval(long interval) { + this.mUpdateInterval = interval; + + this.stopGenerator(); + this.startGenerator(); + } + + public Cursor queryHistory(String[] cols, String where, String[] args, String orderBy) { + return this.mDatabase.query(Location.TABLE_HISTORY, cols, where, args, null, null, orderBy); } } diff --git a/src/com/audacious_software/passive_data_kit/generators/device/ScreenState.java b/src/com/audacious_software/passive_data_kit/generators/device/ScreenState.java index 4d314aa..d8a7432 100755 --- a/src/com/audacious_software/passive_data_kit/generators/device/ScreenState.java +++ b/src/com/audacious_software/passive_data_kit/generators/device/ScreenState.java @@ -1,13 +1,15 @@ package com.audacious_software.passive_data_kit.generators.device; import android.content.BroadcastReceiver; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; import android.view.Display; import android.view.LayoutInflater; import android.view.View; @@ -16,20 +18,18 @@ import android.widget.LinearLayout; import android.widget.TextView; +import com.audacious_software.passive_data_kit.PassiveDataKit; import com.audacious_software.passive_data_kit.activities.generators.DataPointViewHolder; import com.audacious_software.passive_data_kit.diagnostics.DiagnosticAction; import com.audacious_software.passive_data_kit.generators.Generator; import com.audacious_software.passive_data_kit.generators.Generators; import com.audacious_software.pdk.passivedatakit.R; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - +import java.io.File; import java.text.DateFormat; import java.util.ArrayList; import java.util.Calendar; -import java.util.Date; +import java.util.List; public class ScreenState extends Generator{ private static final String GENERATOR_IDENTIFIER = "pdk-screen-state"; @@ -37,20 +37,25 @@ public class ScreenState extends Generator{ private static final String ENABLED = "com.audacious_software.passive_data_kit.generators.device.ScreenState.ENABLED"; private static final boolean ENABLED_DEFAULT = true; - private static final String SCREEN_STATE_KEY = "screen_state"; - private static final String STATE_DOZE = "doze"; - private static final String STATE_DOZE_SUSPEND = "doze_suspend"; - private static final String STATE_ON = "on"; - private static final String STATE_OFF = "off"; - private static final String STATE_UNKNOWN = "unknown"; - private static final String SCREEN_HISTORY_KEY = "com.audacious_software.passive_data_kit.generators.device.ScreenState.SCREEN_HISTORY_KEY";; - private static final String SCREEN_HISTORY_TIMESTAMP = "ts"; - private static final String SCREEN_HISTORY_STATE = "state"; + public static final String STATE_DOZE = "doze"; + public static final String STATE_DOZE_SUSPEND = "doze_suspend"; + public static final String STATE_ON = "on"; + public static final String STATE_OFF = "off"; + public static final String STATE_UNKNOWN = "unknown"; + + private static final String DATABASE_PATH = "pdk-screen-state.sqlite"; + private static final int DATABASE_VERSION = 2; + + public static final String HISTORY_OBSERVED = "observed"; + public static final String HISTORY_STATE = "state"; + public static final String TABLE_HISTORY = "history"; private static ScreenState sInstance = null; private BroadcastReceiver mReceiver = null; + private SQLiteDatabase mDatabase = null; + public static ScreenState getInstance(Context context) { if (ScreenState.sInstance == null) { ScreenState.sInstance = new ScreenState(context.getApplicationContext()); @@ -68,10 +73,18 @@ public static void start(final Context context) { } private void startGenerator() { + final ScreenState me = this; + this.mReceiver = new BroadcastReceiver() { @Override - public void onReceive(Context context, Intent intent) { - Bundle bundle = new Bundle(); + public void onReceive(final Context context, Intent intent) { + long now = System.currentTimeMillis(); + + ContentValues values = new ContentValues(); + values.put(ScreenState.HISTORY_OBSERVED, now); + + Bundle update = new Bundle(); + update.putLong(ScreenState.HISTORY_OBSERVED, now); WindowManager window = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = window.getDefaultDisplay(); @@ -81,62 +94,36 @@ public void onReceive(Context context, Intent intent) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { switch (display.getState()) { case Display.STATE_DOZE: - bundle.putString(ScreenState.SCREEN_STATE_KEY, ScreenState.STATE_DOZE); + values.put(ScreenState.HISTORY_STATE, ScreenState.STATE_DOZE); break; case Display.STATE_DOZE_SUSPEND: - bundle.putString(ScreenState.SCREEN_STATE_KEY, ScreenState.STATE_DOZE_SUSPEND); + values.put(ScreenState.HISTORY_STATE, ScreenState.STATE_DOZE_SUSPEND); break; case Display.STATE_ON: - bundle.putString(ScreenState.SCREEN_STATE_KEY, ScreenState.STATE_ON); + values.put(ScreenState.HISTORY_STATE, ScreenState.STATE_ON); break; case Display.STATE_OFF: - bundle.putString(ScreenState.SCREEN_STATE_KEY, ScreenState.STATE_OFF); + values.put(ScreenState.HISTORY_STATE, ScreenState.STATE_OFF); break; case Display.STATE_UNKNOWN: - bundle.putString(ScreenState.SCREEN_STATE_KEY, ScreenState.STATE_UNKNOWN); + values.put(ScreenState.HISTORY_STATE, ScreenState.STATE_UNKNOWN); break; } } else { if (Intent.ACTION_SCREEN_OFF.equals(action)) { - bundle.putString(ScreenState.SCREEN_STATE_KEY, ScreenState.STATE_OFF); + values.put(ScreenState.HISTORY_STATE, ScreenState.STATE_OFF); } else if (Intent.ACTION_SCREEN_ON.equals(action)) { - bundle.putString(ScreenState.SCREEN_STATE_KEY, ScreenState.STATE_ON); + values.put(ScreenState.HISTORY_STATE, ScreenState.STATE_ON); } else { - bundle.putString(ScreenState.SCREEN_STATE_KEY, ScreenState.STATE_UNKNOWN); + values.put(ScreenState.HISTORY_STATE, ScreenState.STATE_UNKNOWN); } } - Generators.getInstance(context).transmitData(ScreenState.GENERATOR_IDENTIFIER, bundle); - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - try { - JSONArray history = new JSONArray(prefs.getString(ScreenState.SCREEN_HISTORY_KEY, "[]")); - - JSONObject latest = new JSONObject(); - latest.put(ScreenState.SCREEN_HISTORY_TIMESTAMP, System.currentTimeMillis()); - - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - latest.put(ScreenState.SCREEN_HISTORY_STATE, display.getState()); - } else { - if (Intent.ACTION_SCREEN_OFF.equals(action)) { - latest.put(ScreenState.SCREEN_HISTORY_STATE, 0x01); - } else if (Intent.ACTION_SCREEN_ON.equals(action)) { - latest.put(ScreenState.SCREEN_HISTORY_STATE, 0x02); - } else { - latest.put(ScreenState.SCREEN_HISTORY_STATE, 0x00); - } - } - - history.put(latest); + me.mDatabase.insert(ScreenState.TABLE_HISTORY, null, values); - SharedPreferences.Editor e = prefs.edit(); + update.putString(ScreenState.HISTORY_STATE, values.getAsString(ScreenState.HISTORY_STATE)); - e.putString(ScreenState.SCREEN_HISTORY_KEY, history.toString()); - e.apply(); - } catch (JSONException e) { - e.printStackTrace(); - } + Generators.getInstance(context).notifyGeneratorUpdated(ScreenState.GENERATOR_IDENTIFIER, update); } }; @@ -147,7 +134,23 @@ public void onReceive(Context context, Intent intent) { Generators.getInstance(this.mContext).registerCustomViewClass(ScreenState.GENERATOR_IDENTIFIER, ScreenState.class); - this.mReceiver.onReceive(this.mContext, null); + File path = PassiveDataKit.getGeneratorsStorage(this.mContext); + + path = new File(path, ScreenState.DATABASE_PATH); + + this.mDatabase = SQLiteDatabase.openOrCreateDatabase(path, null); + + int version = this.getDatabaseVersion(this.mDatabase); + + switch (version) { + case 0: + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_screen_state_create_history_table)); + case 1: + this.mDatabase.execSQL("DROP TABLE history"); + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_screen_state_create_history_table)); + } + + this.setDatabaseVersion(this.mDatabase, ScreenState.DATABASE_VERSION); } public static boolean isEnabled(Context context) { @@ -168,95 +171,111 @@ public static ArrayList diagnostics(Context context) { return new ArrayList<>(); } - public static void bindViewHolder(DataPointViewHolder holder, final Bundle dataPoint) { + public static void bindViewHolder(DataPointViewHolder holder) { final Context context = holder.itemView.getContext(); - try { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - JSONArray history = new JSONArray(prefs.getString(ScreenState.SCREEN_HISTORY_KEY, "[]")); - -// Log.e("PDK", "SCREEN HISTORY: " + history.toString(2)); + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); - Calendar cal = Calendar.getInstance(); - cal.set(Calendar.HOUR_OF_DAY, 0); - cal.set(Calendar.MINUTE, 0); - cal.set(Calendar.SECOND, 0); - cal.set(Calendar.MILLISECOND, 0); + long zeroStart = cal.getTimeInMillis(); + cal.add(Calendar.DATE, -1); - long zeroStart = cal.getTimeInMillis(); - cal.add(Calendar.DATE, -1); + LinearLayout zeroTimeline = (LinearLayout) holder.itemView.findViewById(R.id.day_zero_value); - LinearLayout zeroTimeline = (LinearLayout) holder.itemView.findViewById(R.id.day_zero_value); + ScreenState.populateTimeline(context, zeroTimeline, zeroStart); - ScreenState.populateTimeline(context, zeroTimeline, zeroStart, history); + long oneStart = cal.getTimeInMillis(); + cal.add(Calendar.DATE, -1); - long oneStart = cal.getTimeInMillis(); - cal.add(Calendar.DATE, -1); + LinearLayout oneTimeline = (LinearLayout) holder.itemView.findViewById(R.id.day_one_value); - LinearLayout oneTimeline = (LinearLayout) holder.itemView.findViewById(R.id.day_one_value); + ScreenState.populateTimeline(context, oneTimeline, oneStart); - ScreenState.populateTimeline(context, oneTimeline, oneStart, history); + long twoStart = cal.getTimeInMillis(); - long twoStart = cal.getTimeInMillis(); + LinearLayout twoTimeline = (LinearLayout) holder.itemView.findViewById(R.id.day_two_value); - LinearLayout twoTimeline = (LinearLayout) holder.itemView.findViewById(R.id.day_two_value); + ScreenState.populateTimeline(context, twoTimeline, twoStart); - ScreenState.populateTimeline(context, twoTimeline, twoStart, history); - } catch (JSONException e) { - e.printStackTrace(); - } + ScreenState generator = ScreenState.getInstance(context); - double timestamp = dataPoint.getBundle(Generator.PDK_METADATA).getDouble(Generator.TIMESTAMP); + Cursor c = generator.mDatabase.query(ScreenState.TABLE_HISTORY, null, null, null, null, null, ScreenState.HISTORY_OBSERVED + " DESC"); + View cardContent = holder.itemView.findViewById(R.id.card_content); + View cardEmpty = holder.itemView.findViewById(R.id.card_empty); TextView dateLabel = (TextView) holder.itemView.findViewById(R.id.generator_data_point_date); - dateLabel.setText(Generator.formatTimestamp(context, timestamp)); + if (c.moveToNext()) { + cardContent.setVisibility(View.VISIBLE); + cardEmpty.setVisibility(View.GONE); - Calendar cal = Calendar.getInstance(); - DateFormat format = android.text.format.DateFormat.getDateFormat(context); + long timestamp = c.getLong(c.getColumnIndex(ScreenState.HISTORY_OBSERVED)) / 1000; - TextView zeroDayLabel = (TextView) holder.itemView.findViewById(R.id.day_zero_label); - zeroDayLabel.setText(format.format(cal.getTime())); + dateLabel.setText(Generator.formatTimestamp(context, timestamp)); - cal.add(Calendar.DATE, -1); + cal = Calendar.getInstance(); + DateFormat format = android.text.format.DateFormat.getDateFormat(context); - TextView oneDayLabel = (TextView) holder.itemView.findViewById(R.id.day_one_label); - oneDayLabel.setText(format.format(cal.getTime())); + TextView zeroDayLabel = (TextView) holder.itemView.findViewById(R.id.day_zero_label); + zeroDayLabel.setText(format.format(cal.getTime())); - cal.add(Calendar.DATE, -1); + cal.add(Calendar.DATE, -1); + + TextView oneDayLabel = (TextView) holder.itemView.findViewById(R.id.day_one_label); + oneDayLabel.setText(format.format(cal.getTime())); + + cal.add(Calendar.DATE, -1); + + TextView twoDayLabel = (TextView) holder.itemView.findViewById(R.id.day_two_label); + twoDayLabel.setText(format.format(cal.getTime())); + } else { + cardContent.setVisibility(View.GONE); + cardEmpty.setVisibility(View.VISIBLE); + + dateLabel.setText(R.string.label_never_pdk); + } - TextView twoDayLabel = (TextView) holder.itemView.findViewById(R.id.day_two_label); - twoDayLabel.setText(format.format(cal.getTime())); + c.close(); } - private static void populateTimeline(Context context, LinearLayout timeline, long start, JSONArray history) { + private static void populateTimeline(Context context, LinearLayout timeline, long start) { timeline.removeAllViews(); + ScreenState generator = ScreenState.getInstance(context); + long end = start + (24 * 60 * 60 * 1000); - long now = System.currentTimeMillis(); + String where = ScreenState.HISTORY_OBSERVED + " >= ? AND " + ScreenState.HISTORY_OBSERVED + " < ?"; + String[] args = { "" + start, "" + end }; - int lastState = -1; + Cursor c = generator.mDatabase.query(ScreenState.TABLE_HISTORY, null, where, args, null, null, ScreenState.HISTORY_OBSERVED); - ArrayList activeStates = new ArrayList<>(); + ArrayList activeStates = new ArrayList<>(); ArrayList activeTimestamps = new ArrayList<>(); - for (int i = 0; i < history.length(); i++) { - try { - JSONObject point = history.getJSONObject(i); + while (c.moveToNext()) { + long timestamp = c.getLong(c.getColumnIndex(ScreenState.HISTORY_OBSERVED)); - long timestamp = point.getLong(ScreenState.SCREEN_HISTORY_TIMESTAMP); - int state = point.getInt(ScreenState.SCREEN_HISTORY_STATE); + activeTimestamps.add(timestamp); - if (timestamp < start) { - lastState = state; - } else if (timestamp < end) { - activeStates.add(state); - activeTimestamps.add(timestamp); - } - } catch (JSONException e) { - e.printStackTrace(); - } + String state = c.getString(c.getColumnIndex(ScreenState.HISTORY_STATE)); + activeStates.add(state); + } + + c.close(); + + String lastState = ScreenState.STATE_UNKNOWN; + + String lastWhere = ScreenState.HISTORY_OBSERVED + " < ?"; + String[] lastArgs = { "" + start }; + + c = generator.mDatabase.query(ScreenState.TABLE_HISTORY, null, lastWhere, lastArgs, null, null, ScreenState.HISTORY_OBSERVED + " DESC"); + + if (c.moveToNext()) { + lastState = c.getString(c.getColumnIndex(ScreenState.HISTORY_STATE)); } if (activeStates.size() > 0) { @@ -265,25 +284,24 @@ private static void populateTimeline(Context context, LinearLayout timeline, lon View startView = new View(context); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - if (lastState == -1) { + if (ScreenState.STATE_UNKNOWN.equals(lastState)) { - } else if (firstState == Display.STATE_ON) { + } else if (ScreenState.STATE_ON.equals(lastState)) { startView.setBackgroundColor(0xff4CAF50); - } else if (firstState == Display.STATE_OFF) { + } else if (ScreenState.STATE_OFF.equals(lastState)) { startView.setBackgroundColor(0xff263238); - } else if (firstState == Display.STATE_DOZE) { + } else if (ScreenState.STATE_DOZE.equals(lastState)) { startView.setBackgroundColor(0xff1b5e20); - } else if (firstState == Display.STATE_DOZE_SUSPEND) { + } else if (ScreenState.STATE_DOZE_SUSPEND.equals(lastState)) { startView.setBackgroundColor(0xff1b5e20); } } else { - if (lastState == -1) { + if (ScreenState.STATE_UNKNOWN.equals(lastState)) { - } else if (firstState == 0x02) { + } else if (ScreenState.STATE_ON.equals(lastState)) { startView.setBackgroundColor(0xff4CAF50); - } else if (firstState == 0x01) { + } else if (ScreenState.STATE_OFF.equals(lastState)) { startView.setBackgroundColor(0xff263238); } } @@ -293,28 +311,30 @@ private static void populateTimeline(Context context, LinearLayout timeline, lon timeline.addView(startView); + long now = System.currentTimeMillis(); + if (activeStates.size() == 1) { View v = new View(context); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - if (firstState == Display.STATE_ON) { + if (ScreenState.STATE_ON.equals(firstState)) { v.setBackgroundColor(0xff4CAF50); - } else if (firstState == Display.STATE_OFF) { + } else if (ScreenState.STATE_OFF.equals(firstState)) { v.setBackgroundColor(0xff263238); - } else if (firstState == Display.STATE_DOZE) { + } else if (ScreenState.STATE_DOZE.equals(firstState)) { v.setBackgroundColor(0xff3f51b5); - } else if (firstState == Display.STATE_DOZE_SUSPEND) { + } else if (ScreenState.STATE_DOZE_SUSPEND.equals(firstState)) { v.setBackgroundColor(0xff3f51b5); } } else { - if (firstState == 0x02) { + if (ScreenState.STATE_ON.equals(firstState)) { v.setBackgroundColor(0xff4CAF50); - } else if (firstState == 0x01) { + } else if (ScreenState.STATE_OFF.equals(firstState)) { v.setBackgroundColor(0xff263238); } } - if (end > now) { + if (end > System.currentTimeMillis()) { params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, now - firstTimestamp); v.setLayoutParams(params); } else { @@ -328,24 +348,24 @@ private static void populateTimeline(Context context, LinearLayout timeline, lon long currentTimestamp = activeTimestamps.get(i); long priorTimestamp = activeTimestamps.get(i - 1); - long priorState = activeStates.get(i - 1); + String priorState = activeStates.get(i - 1); View v = new View(context); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - if (priorState == Display.STATE_ON) { + if (ScreenState.STATE_ON.equals(priorState)) { v.setBackgroundColor(0xff4CAF50); - } else if (priorState == Display.STATE_OFF) { + } else if (ScreenState.STATE_OFF.equals(priorState)) { v.setBackgroundColor(0xff263238); - } else if (priorState == Display.STATE_DOZE) { + } else if (ScreenState.STATE_DOZE.equals(priorState)) { v.setBackgroundColor(0xff3f51b5); - } else if (priorState == Display.STATE_DOZE_SUSPEND) { + } else if (ScreenState.STATE_DOZE_SUSPEND.equals(priorState)) { v.setBackgroundColor(0xff3f51b5); } } else { - if (priorState == 0x02) { + if (ScreenState.STATE_ON.equals(priorState)) { v.setBackgroundColor(0xff4CAF50); - } else if (priorState == 0x01) { + } else if (ScreenState.STATE_OFF.equals(priorState)) { v.setBackgroundColor(0xff263238); } } @@ -357,17 +377,17 @@ private static void populateTimeline(Context context, LinearLayout timeline, lon } long finalTimestamp = activeTimestamps.get(activeTimestamps.size() - 1); - long finalState = activeStates.get(activeStates.size() - 1); + String finalState = activeStates.get(activeStates.size() - 1); View v = new View(context); - if (finalState == Display.STATE_ON) { + if (ScreenState.STATE_ON.equals(finalState)) { v.setBackgroundColor(0xff4CAF50); - } else if (finalState == Display.STATE_OFF) { + } else if (ScreenState.STATE_OFF.equals(finalState)) { v.setBackgroundColor(0xff263238); - } else if (finalState == Display.STATE_DOZE) { + } else if (ScreenState.STATE_DOZE.equals(finalState)) { v.setBackgroundColor(0xff3f51b5); - } else if (finalState == Display.STATE_DOZE_SUSPEND) { + } else if (ScreenState.STATE_DOZE_SUSPEND.equals(finalState)) { v.setBackgroundColor(0xff3f51b5); } @@ -390,6 +410,8 @@ private static void populateTimeline(Context context, LinearLayout timeline, lon timeline.addView(v); } + } else { + } } @@ -398,7 +420,28 @@ public static View fetchView(ViewGroup parent) return LayoutInflater.from(parent.getContext()).inflate(R.layout.card_generator_screen_state, parent, false); } - public static void broadcastLatestDataPoint(Context context) { - Generators.getInstance(context).transmitData(ScreenState.GENERATOR_IDENTIFIER, new Bundle()); + @Override + public List fetchPayloads() { + return new ArrayList<>(); + } + + public static long latestPointGenerated(Context context) { + long timestamp = 0; + + ScreenState me = ScreenState.getInstance(context); + + Cursor c = me.mDatabase.query(ScreenState.TABLE_HISTORY, null, null, null, null, null, ScreenState.HISTORY_OBSERVED + " DESC"); + + if (c.moveToNext()) { + timestamp = c.getLong(c.getColumnIndex(ScreenState.HISTORY_OBSERVED)); + } + + c.close(); + + return timestamp; + } + + public Cursor queryHistory(String[] cols, String where, String[] args, String orderBy) { + return this.mDatabase.query(ScreenState.TABLE_HISTORY, cols, where, args, null, null, orderBy); } } diff --git a/src/com/audacious_software/passive_data_kit/generators/diagnostics/AppEvent.java b/src/com/audacious_software/passive_data_kit/generators/diagnostics/AppEvent.java new file mode 100755 index 0000000..5eab4d1 --- /dev/null +++ b/src/com/audacious_software/passive_data_kit/generators/diagnostics/AppEvent.java @@ -0,0 +1,441 @@ +package com.audacious_software.passive_data_kit.generators.diagnostics; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.audacious_software.passive_data_kit.PassiveDataKit; +import com.audacious_software.passive_data_kit.activities.generators.DataPointViewHolder; +import com.audacious_software.passive_data_kit.diagnostics.DiagnosticAction; +import com.audacious_software.passive_data_kit.generators.Generator; +import com.audacious_software.passive_data_kit.generators.Generators; +import com.audacious_software.pdk.passivedatakit.R; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AppEvent extends Generator{ + private static final String GENERATOR_IDENTIFIER = "pdk-app-event"; + + private static final String ENABLED = "com.audacious_software.passive_data_kit.generators.diagnostics.AppEvent.ENABLED"; + private static final boolean ENABLED_DEFAULT = true; + + private static final String DATABASE_PATH = "pdk-app-event.sqlite"; + private static final int DATABASE_VERSION = 1; + + public static final String HISTORY_OBSERVED = "observed"; + public static final String HISTORY_EVENT_NAME = "event_name"; + public static final String HISTORY_EVENT_DETAILS = "event_details"; + public static final String TABLE_HISTORY = "history"; + + private static AppEvent sInstance = null; + + private SQLiteDatabase mDatabase = null; + + public static AppEvent getInstance(Context context) { + if (AppEvent.sInstance == null) { + AppEvent.sInstance = new AppEvent(context.getApplicationContext()); + } + + return AppEvent.sInstance; + } + + public AppEvent(Context context) { + super(context); + } + + public static void start(final Context context) { + AppEvent.getInstance(context).startGenerator(); + } + + private void startGenerator() { + final AppEvent me = this; + + Generators.getInstance(this.mContext).registerCustomViewClass(AppEvent.GENERATOR_IDENTIFIER, AppEvent.class); + + File path = PassiveDataKit.getGeneratorsStorage(this.mContext); + + path = new File(path, AppEvent.DATABASE_PATH); + + this.mDatabase = SQLiteDatabase.openOrCreateDatabase(path, null); + + int version = this.getDatabaseVersion(this.mDatabase); + + switch (version) { + case 0: + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_app_events_create_history_table)); + } + + this.setDatabaseVersion(this.mDatabase, AppEvent.DATABASE_VERSION); + } + + public static boolean isEnabled(Context context) { + SharedPreferences prefs = Generators.getInstance(context).getSharedPreferences(context); + + return prefs.getBoolean(AppEvent.ENABLED, AppEvent.ENABLED_DEFAULT); + } + + public static boolean isRunning(Context context) { + return (AppEvent.sInstance != null); + } + + public static ArrayList diagnostics(Context context) { + return new ArrayList<>(); + } + + public static void bindViewHolder(DataPointViewHolder holder) { +/* final Context context = holder.itemView.getContext(); + + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + long zeroStart = cal.getTimeInMillis(); + cal.add(Calendar.DATE, -1); + + LinearLayout zeroTimeline = (LinearLayout) holder.itemView.findViewById(R.id.day_zero_value); + + AppEvents.populateTimeline(context, zeroTimeline, zeroStart); + + long oneStart = cal.getTimeInMillis(); + cal.add(Calendar.DATE, -1); + + LinearLayout oneTimeline = (LinearLayout) holder.itemView.findViewById(R.id.day_one_value); + + AppEvents.populateTimeline(context, oneTimeline, oneStart); + + long twoStart = cal.getTimeInMillis(); + + LinearLayout twoTimeline = (LinearLayout) holder.itemView.findViewById(R.id.day_two_value); + + AppEvents.populateTimeline(context, twoTimeline, twoStart); + + AppEvents generator = AppEvents.getInstance(context); + + Cursor c = generator.mDatabase.query(AppEvents.TABLE_HISTORY, null, null, null, null, null, AppEvents.HISTORY_OBSERVED + " DESC"); + + View cardContent = holder.itemView.findViewById(R.id.card_content); + View cardEmpty = holder.itemView.findViewById(R.id.card_empty); + TextView dateLabel = (TextView) holder.itemView.findViewById(R.id.generator_data_point_date); + + if (c.moveToNext()) { + cardContent.setVisibility(View.VISIBLE); + cardEmpty.setVisibility(View.GONE); + + long timestamp = c.getLong(c.getColumnIndex(AppEvents.HISTORY_OBSERVED)) / 1000; + + dateLabel.setText(Generator.formatTimestamp(context, timestamp)); + + cal = Calendar.getInstance(); + DateFormat format = android.text.format.DateFormat.getDateFormat(context); + + TextView zeroDayLabel = (TextView) holder.itemView.findViewById(R.id.day_zero_label); + zeroDayLabel.setText(format.format(cal.getTime())); + + cal.add(Calendar.DATE, -1); + + TextView oneDayLabel = (TextView) holder.itemView.findViewById(R.id.day_one_label); + oneDayLabel.setText(format.format(cal.getTime())); + + cal.add(Calendar.DATE, -1); + + TextView twoDayLabel = (TextView) holder.itemView.findViewById(R.id.day_two_label); + twoDayLabel.setText(format.format(cal.getTime())); + } else { + cardContent.setVisibility(View.GONE); + cardEmpty.setVisibility(View.VISIBLE); + + dateLabel.setText(R.string.label_never_pdk); + } + + c.close(); + */ + } + + /* + private static void populateTimeline(Context context, LinearLayout timeline, long start) { + timeline.removeAllViews(); + + AppEvents generator = AppEvents.getInstance(context); + + long end = start + (24 * 60 * 60 * 1000); + + String where = AppEvents.HISTORY_OBSERVED + " >= ? AND " + AppEvents.HISTORY_OBSERVED + " < ?"; + String[] args = { "" + start, "" + end }; + + Cursor c = generator.mDatabase.query(AppEvents.TABLE_HISTORY, null, where, args, null, null, AppEvents.HISTORY_OBSERVED); + + ArrayList activeStates = new ArrayList<>(); + ArrayList activeTimestamps = new ArrayList<>(); + + while (c.moveToNext()) { + long timestamp = c.getLong(c.getColumnIndex(AppEvents.HISTORY_OBSERVED)); + + activeTimestamps.add(timestamp); + + String state = c.getString(c.getColumnIndex(AppEvents.HISTORY_STATE)); + activeStates.add(state); + } + + c.close(); + + String lastState = AppEvents.STATE_UNKNOWN; + + String lastWhere = AppEvents.HISTORY_OBSERVED + " < ?"; + String[] lastArgs = { "" + start }; + + c = generator.mDatabase.query(AppEvents.TABLE_HISTORY, null, lastWhere, lastArgs, null, null, AppEvents.HISTORY_OBSERVED + " DESC"); + + if (c.moveToNext()) { + lastState = c.getString(c.getColumnIndex(AppEvents.HISTORY_STATE)); + } + + if (activeStates.size() > 0) { + long firstTimestamp = activeTimestamps.get(0); + long firstState = activeTimestamps.get(0); + + View startView = new View(context); + + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + if (AppEvents.STATE_UNKNOWN.equals(lastState)) { + + } else if (AppEvents.STATE_ON.equals(lastState)) { + startView.setBackgroundColor(0xff4CAF50); + } else if (AppEvents.STATE_OFF.equals(lastState)) { + startView.setBackgroundColor(0xff263238); + } else if (AppEvents.STATE_DOZE.equals(lastState)) { + startView.setBackgroundColor(0xff1b5e20); + } else if (AppEvents.STATE_DOZE_SUSPEND.equals(lastState)) { + startView.setBackgroundColor(0xff1b5e20); + } + } else { + if (AppEvents.STATE_UNKNOWN.equals(lastState)) { + + } else if (AppEvents.STATE_ON.equals(lastState)) { + startView.setBackgroundColor(0xff4CAF50); + } else if (AppEvents.STATE_OFF.equals(lastState)) { + startView.setBackgroundColor(0xff263238); + } + } + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, firstTimestamp - start); + startView.setLayoutParams(params); + + timeline.addView(startView); + + long now = System.currentTimeMillis(); + + if (activeStates.size() == 1) { + View v = new View(context); + + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + if (AppEvents.STATE_ON.equals(firstState)) { + v.setBackgroundColor(0xff4CAF50); + } else if (AppEvents.STATE_OFF.equals(firstState)) { + v.setBackgroundColor(0xff263238); + } else if (AppEvents.STATE_DOZE.equals(firstState)) { + v.setBackgroundColor(0xff3f51b5); + } else if (AppEvents.STATE_DOZE_SUSPEND.equals(firstState)) { + v.setBackgroundColor(0xff3f51b5); + } + } else { + if (AppEvents.STATE_ON.equals(firstState)) { + v.setBackgroundColor(0xff4CAF50); + } else if (AppEvents.STATE_OFF.equals(firstState)) { + v.setBackgroundColor(0xff263238); + } + } + + if (end > System.currentTimeMillis()) { + params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, now - firstTimestamp); + v.setLayoutParams(params); + } else { + params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, end - firstTimestamp); + v.setLayoutParams(params); + } + + timeline.addView(v); + } else { + for (int i = 1; i < activeStates.size(); i++) { + long currentTimestamp = activeTimestamps.get(i); + + long priorTimestamp = activeTimestamps.get(i - 1); + String priorState = activeStates.get(i - 1); + + View v = new View(context); + + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + if (AppEvents.STATE_ON.equals(priorState)) { + v.setBackgroundColor(0xff4CAF50); + } else if (AppEvents.STATE_OFF.equals(priorState)) { + v.setBackgroundColor(0xff263238); + } else if (AppEvents.STATE_DOZE.equals(priorState)) { + v.setBackgroundColor(0xff3f51b5); + } else if (AppEvents.STATE_DOZE_SUSPEND.equals(priorState)) { + v.setBackgroundColor(0xff3f51b5); + } + } else { + if (AppEvents.STATE_ON.equals(priorState)) { + v.setBackgroundColor(0xff4CAF50); + } else if (AppEvents.STATE_OFF.equals(priorState)) { + v.setBackgroundColor(0xff263238); + } + } + + params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, currentTimestamp - priorTimestamp); + v.setLayoutParams(params); + + timeline.addView(v); + } + + long finalTimestamp = activeTimestamps.get(activeTimestamps.size() - 1); + String finalState = activeStates.get(activeStates.size() - 1); + + View v = new View(context); + + if (AppEvents.STATE_ON.equals(finalState)) { + v.setBackgroundColor(0xff4CAF50); + } else if (AppEvents.STATE_OFF.equals(finalState)) { + v.setBackgroundColor(0xff263238); + } else if (AppEvents.STATE_DOZE.equals(finalState)) { + v.setBackgroundColor(0xff3f51b5); + } else if (AppEvents.STATE_DOZE_SUSPEND.equals(finalState)) { + v.setBackgroundColor(0xff3f51b5); + } + + if (end > now) { + params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, now - finalTimestamp); + v.setLayoutParams(params); + } else { + params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, end - finalTimestamp); + v.setLayoutParams(params); + } + + timeline.addView(v); + } + + if (end > now) { + View v = new View(context); + + params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, end - now); + v.setLayoutParams(params); + + timeline.addView(v); + } + } else { + + } + } + +*/ + public static View fetchView(ViewGroup parent) + { + return LayoutInflater.from(parent.getContext()).inflate(R.layout.card_generator_generic, parent, false); + } + + @Override + public List fetchPayloads() { + return new ArrayList<>(); + } + + public static long latestPointGenerated(Context context) { + long timestamp = 0; + + AppEvent me = AppEvent.getInstance(context); + + Cursor c = me.mDatabase.query(AppEvent.TABLE_HISTORY, null, null, null, null, null, AppEvent.HISTORY_OBSERVED + " DESC"); + + if (c.moveToNext()) { + timestamp = c.getLong(c.getColumnIndex(AppEvent.HISTORY_OBSERVED)); + } + + c.close(); + + return timestamp; + } + + public Cursor queryHistory(String[] cols, String where, String[] args, String orderBy) { + return this.mDatabase.query(AppEvent.TABLE_HISTORY, cols, where, args, null, null, orderBy); + } + + public boolean logEvent(String eventName, Map eventDetails) { + try { + long now = System.currentTimeMillis(); + + ContentValues values = new ContentValues(); + values.put(AppEvent.HISTORY_OBSERVED, now); + values.put(AppEvent.HISTORY_EVENT_NAME, eventName); + + Bundle detailsBundle = new Bundle(); + JSONObject detailsJson = new JSONObject(); + + for (String key : eventDetails.keySet()) { + Object value = eventDetails.get(key); + + if (value instanceof Double) { + Double doubleValue = ((Double) value); + + detailsBundle.putDouble(key, doubleValue); + detailsJson.put(key, doubleValue.doubleValue()); + } else if (value instanceof Float) { + Float floatValue = ((Float) value); + + detailsBundle.putDouble(key, floatValue.doubleValue()); + detailsJson.put(key, floatValue.doubleValue()); + } else if (value instanceof Long) { + Long longValue = ((Long) value); + + detailsBundle.putLong(key, longValue.longValue()); + detailsJson.put(key, longValue.longValue()); + } else if (value instanceof Integer) { + Integer intValue = ((Integer) value); + + detailsBundle.putLong(key, intValue.longValue()); + detailsJson.put(key, intValue.longValue()); + } else if (value instanceof String) { + detailsBundle.putString(key, value.toString()); + detailsJson.put(key, value.toString()); + } else { + detailsBundle.putString(key, "Unknown Class: " + value.getClass().getCanonicalName()); + detailsJson.put(key, "Unknown Class: " + value.getClass().getCanonicalName()); + } + } + + values.put(AppEvent.HISTORY_EVENT_DETAILS, detailsJson.toString(2)); + + this.mDatabase.insert(AppEvent.TABLE_HISTORY, null, values); + + Bundle update = new Bundle(); + update.putLong(AppEvent.HISTORY_OBSERVED, now); + update.putString(AppEvent.HISTORY_EVENT_NAME, values.getAsString(AppEvent.HISTORY_EVENT_NAME)); + update.putBundle(AppEvent.HISTORY_EVENT_DETAILS, detailsBundle); + + Generators.getInstance(this.mContext).notifyGeneratorUpdated(AppEvent.GENERATOR_IDENTIFIER, update); + + return true; + } catch (JSONException e) { + e.printStackTrace(); + } + + return false; + } + + public void logThrowable(Throwable t) { + + } +} diff --git a/src/com/audacious_software/passive_data_kit/generators/services/GoogleAwareness.java b/src/com/audacious_software/passive_data_kit/generators/services/GoogleAwareness.java index 796b1c2..0688e7d 100755 --- a/src/com/audacious_software/passive_data_kit/generators/services/GoogleAwareness.java +++ b/src/com/audacious_software/passive_data_kit/generators/services/GoogleAwareness.java @@ -19,15 +19,13 @@ import com.audacious_software.passive_data_kit.generators.Generators; import com.audacious_software.pdk.passivedatakit.R; import com.google.android.gms.awareness.Awareness; -import com.google.android.gms.awareness.snapshot.DetectedActivityResult; import com.google.android.gms.awareness.snapshot.HeadphoneStateResult; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.ResultCallback; -import com.google.android.gms.location.ActivityRecognitionResult; -import com.google.android.gms.location.DetectedActivity; import java.util.ArrayList; +import java.util.List; /** * Created by cjkarr on 6/28/2016. @@ -105,7 +103,7 @@ private ArrayList runDiagostics() { int permissionCheck = ContextCompat.checkSelfPermission(this.mContext, Manifest.permission.ACCESS_FINE_LOCATION); if (permissionCheck != PackageManager.PERMISSION_GRANTED) { - actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_location_permission), new Runnable() { + actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_location_permission_title), me.mContext.getString(R.string.diagnostic_missing_location_permission), new Runnable() { @Override public void run() { handler.post(new Runnable() { @@ -129,7 +127,7 @@ public void run() { if (permissionCheck != PackageManager.PERMISSION_GRANTED) { Log.e("PDK", "3.3"); - actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_activity_recognition_permission), new Runnable() { + actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_activity_recognition_permission_title), me.mContext.getString(R.string.diagnostic_missing_activity_recognition_permission), new Runnable() { @Override public void run() { handler.post(new Runnable() { @@ -219,4 +217,9 @@ public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { Log.e("PDK", "GA onConnectionFailed"); this.mGoogleApiClient = null; } + + @Override + public List fetchPayloads() { + return new ArrayList<>(); + } } diff --git a/src/com/audacious_software/passive_data_kit/generators/vr/DaydreamViewController.java b/src/com/audacious_software/passive_data_kit/generators/vr/DaydreamViewController.java index 6cbc304..531ed71 100755 --- a/src/com/audacious_software/passive_data_kit/generators/vr/DaydreamViewController.java +++ b/src/com/audacious_software/passive_data_kit/generators/vr/DaydreamViewController.java @@ -14,10 +14,11 @@ import com.audacious_software.passive_data_kit.generators.Generator; import com.audacious_software.passive_data_kit.generators.Generators; import com.audacious_software.pdk.passivedatakit.R; -import com.google.vr.sdk.controller.Controller; -import com.google.vr.sdk.controller.ControllerManager; +// import com.google.vr.sdk.controller.Controller; +/// import com.google.vr.sdk.controller.ControllerManager; import java.util.ArrayList; +import java.util.List; /** * Created by cjkarr on 11/20/2016. @@ -32,8 +33,8 @@ public class DaydreamViewController extends Generator { private static DaydreamViewController sInstance = null; private BroadcastReceiver mReceiver = null; - private ControllerManager mControllerManager = null; - private Controller mController = null; +// private ControllerManager mControllerManager = null; +// private Controller mController = null; public static DaydreamViewController getInstance(Context context) { if (DaydreamViewController.sInstance == null) { @@ -49,7 +50,7 @@ public DaydreamViewController(Context context) { public static void start(final Context context) { DaydreamViewController.getInstance(context).startGenerator(); - +/* DaydreamViewController.sInstance.mControllerManager = new ControllerManager(context, new ControllerManager.EventListener() { @Override public void onApiStatusChanged(int status) { @@ -78,6 +79,8 @@ public void onUpdate() { }); DaydreamViewController.sInstance.mControllerManager.start(); + +*/ } private void startGenerator() { @@ -103,9 +106,9 @@ public static ArrayList diagnostics(Context context) { } public static void bindViewHolder(DataPointViewHolder holder, final Bundle dataPoint) { + /* final Context context = holder.itemView.getContext(); - /* try { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); JSONArray history = new JSONArray(prefs.getString(ScreenState.SCREEN_HISTORY_KEY, "[]")); @@ -171,8 +174,12 @@ public static View fetchView(ViewGroup parent) return LayoutInflater.from(parent.getContext()).inflate(R.layout.card_generator_daydream_vr_controller, parent, false); } - public static void broadcastLatestDataPoint(Context context) { - Generators.getInstance(context).transmitData(DaydreamViewController.GENERATOR_IDENTIFIER, new Bundle()); + @Override + public List fetchPayloads() { + return null; } + public static void broadcastLatestDataPoint(Context context) { +// Generators.getInstance(context).transmitData(DaydreamViewController.GENERATOR_IDENTIFIER, new Bundle()); + } } diff --git a/src/com/audacious_software/passive_data_kit/generators/wearables/MicrosoftBand.java b/src/com/audacious_software/passive_data_kit/generators/wearables/MicrosoftBand.java index 295aade..246e7d3 100755 --- a/src/com/audacious_software/passive_data_kit/generators/wearables/MicrosoftBand.java +++ b/src/com/audacious_software/passive_data_kit/generators/wearables/MicrosoftBand.java @@ -65,6 +65,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.List; @SuppressWarnings("unused") public class MicrosoftBand extends Generator @@ -543,7 +544,7 @@ private void transmitData(BandSensorManager sensors) { this.mHeartRateVariabilityDataPoints.clear(); } - Generators.getInstance(this.mContext).transmitData(MicrosoftBand.GENERATOR_IDENTIFIER, bundle); +// Generators.getInstance(this.mContext).transmitData(MicrosoftBand.GENERATOR_IDENTIFIER, bundle); } public static ArrayList diagnostics(Context context) @@ -560,7 +561,7 @@ private ArrayList runDiagostics() { final MicrosoftBand me = this; if (MicrosoftBand.sInstance.mBandClient == null) { - actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_msft_band_client), new Runnable() { + actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_msft_band_client_title), me.mContext.getString(R.string.diagnostic_missing_msft_band_client), new Runnable() { @Override public void run() { @@ -574,7 +575,7 @@ public void run() { } })); } else if (!MicrosoftBand.sInstance.mBandClient.isConnected()) { - actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_msft_band_client), new Runnable() { + actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_msft_band_client_title), me.mContext.getString(R.string.diagnostic_missing_msft_band_client), new Runnable() { @Override public void run() { @@ -595,7 +596,7 @@ public void run() { if (this.canAccessSensor(sensors, MicrosoftBand.HeartRateDataPoint.class) || this.canAccessSensor(sensors, MicrosoftBand.HeartRateVariabilityDataPoint.class)) { if (sensors.getCurrentHeartRateConsent() != UserConsent.GRANTED) { - actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_msft_band_auth), new Runnable() { + actions.add(new DiagnosticAction(me.mContext.getString(R.string.diagnostic_missing_msft_band_auth_title), me.mContext.getString(R.string.diagnostic_missing_msft_band_auth), new Runnable() { @Override public void run() { @@ -1429,4 +1430,9 @@ public static void bindViewHolder(DataPointViewHolder holder, Bundle dataPoint) uvLevel.setText(context.getString(R.string.generator_value_not_applicable)); } } + + @Override + public List fetchPayloads() { + return new ArrayList<>(); + } } diff --git a/src/com/audacious_software/passive_data_kit/generators/wearables/WithingsDevice.java b/src/com/audacious_software/passive_data_kit/generators/wearables/WithingsDevice.java new file mode 100755 index 0000000..8aaab44 --- /dev/null +++ b/src/com/audacious_software/passive_data_kit/generators/wearables/WithingsDevice.java @@ -0,0 +1,1175 @@ +package com.audacious_software.passive_data_kit.generators.wearables; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.util.Base64; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.audacious_software.passive_data_kit.PassiveDataKit; +import com.audacious_software.passive_data_kit.activities.generators.DataPointViewHolder; +import com.audacious_software.passive_data_kit.diagnostics.DiagnosticAction; +import com.audacious_software.passive_data_kit.generators.Generator; +import com.audacious_software.passive_data_kit.generators.Generators; +import com.audacious_software.passive_data_kit.generators.diagnostics.AppEvent; +import com.audacious_software.pdk.passivedatakit.R; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.UUID; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class WithingsDevice extends Generator { + private static final String GENERATOR_IDENTIFIER = "pdk-withings-device"; + + private static final String ENABLED = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.ENABLED"; + private static final boolean ENABLED_DEFAULT = true; + + private static final String DATASTREAM = "datastream"; + private static final String DATASTREAM_ACTIVITY_MEASURES = "activity-measures"; + + private static final String TABLE_ACTIVITY_MEASURE_HISTORY = "activity_measure_history"; + private static final String ACTIVITY_MEASURE_HISTORY_DATE_START = "date_start"; + private static final String ACTIVITY_MEASURE_HISTORY_TIMEZONE = "timezone"; + private static final String ACTIVITY_MEASURE_STEPS = "steps"; + private static final String ACTIVITY_MEASURE_DISTANCE = "distance"; + private static final String ACTIVITY_MEASURE_ACTIVE_CALORIES = "active_calories"; + private static final String ACTIVITY_MEASURE_TOTAL_CALORIES = "total_calories"; + private static final String ACTIVITY_MEASURE_ELEVATION = "elevation"; + private static final String ACTIVITY_MEASURE_SOFT_ACTIVITY_DURATION = "soft_activity_duration"; + private static final String ACTIVITY_MEASURE_MODERATE_ACTIVITY_DURATION = "moderate_activity_duration"; + private static final String ACTIVITY_MEASURE_INTENSE_ACTIVITY_DURATION = "intense_activity_duration"; + + private static final String TABLE_BODY_MEASURE_HISTORY = "body_measure_history"; + private static final String BODY_MEASURE_STATUS_UNKNOWN = "unknown"; + private static final String BODY_MEASURE_STATUS_USER_DEVICE = "user-device"; + private static final String BODY_MEASURE_STATUS_SHARED_DEVICE = "shared-device"; + private static final String BODY_MEASURE_STATUS_MANUAL_ENTRY = "manual-entry"; + private static final String BODY_MEASURE_STATUS_MANUAL_ENTRY_CREATION = "manual-entry-creation"; + private static final String BODY_MEASURE_STATUS_AUTO_DEVICE = "auto-device"; + private static final String BODY_MEASURE_STATUS_MEASURE_CONFIRMED = "measure-confirmed"; + + private static final String BODY_MEASURE_CATEGORY_UNKNOWN = "unknown"; + private static final String BODY_MEASURE_CATEGORY_REAL_MEASUREMENTS = "real-measurements"; + private static final String BODY_MEASURE_CATEGORY_USER_OBJECTIVES = "user-objectives"; + + private static final String BODY_MEASURE_TYPE_UNKNOWN = "unknown"; + private static final String BODY_MEASURE_TYPE_WEIGHT = "weight"; + private static final String BODY_MEASURE_TYPE_HEIGHT = "height"; + private static final String BODY_MEASURE_TYPE_FAT_FREE_MASS = "fat-free-mass"; + private static final String BODY_MEASURE_TYPE_FAT_RATIO = "fat-ratio"; + private static final String BODY_MEASURE_TYPE_FAT_MASS_WEIGHT = "fat-mass-weight"; + private static final String BODY_MEASURE_TYPE_DIASTOLIC_BLOOD_PRESSURE = "diastolic-blood-pressure"; + private static final String BODY_MEASURE_TYPE_SYSTOLIC_BLOOD_PRESSURE = "systolic-blood-pressure"; + private static final String BODY_MEASURE_TYPE_HEART_PULSE = "heart-pulse"; + private static final String BODY_MEASURE_TYPE_TEMPERATURE = "temperature"; + private static final String BODY_MEASURE_TYPE_OXYGEN_SATURATION = "oxygen-saturation"; + private static final String BODY_MEASURE_TYPE_BODY_TEMPERATURE = "body-temperature"; + private static final String BODY_MEASURE_TYPE_SKIN_TEMPERATURE = "skin-temperature"; + private static final String BODY_MEASURE_TYPE_MUSCLE_MASS = "muscle-mass"; + private static final String BODY_MEASURE_TYPE_HYDRATION = "hydration"; + private static final String BODY_MEASURE_TYPE_BONE_MASS = "bone-mass"; + private static final String BODY_MEASURE_TYPE_PULSE_WAVE_VELOCITY = "pulse-wave-velocity"; + + private static final String BODY_MEASURE_HISTORY_DATE = "measure_date"; + private static final String BODY_MEASURE_HISTORY_STATUS = "measure_status"; + private static final String BODY_MEASURE_HISTORY_CATEGORY = "measure_category"; + private static final String BODY_MEASURE_HISTORY_TYPE = "measure_type"; + private static final String BODY_MEASURE_HISTORY_VALUE = "measure_value"; + + private static final String TABLE_INTRADAY_ACTIVITY_HISTORY = "intraday_activity_history"; + + private static final String TABLE_SLEEP_MEASURE_HISTORY = "sleep_measure_history"; + private static final String SLEEP_MEASURE_MODEL_UNKNOWN = "unknown"; + private static final String SLEEP_MEASURE_MODEL_ACTIVITY_TRACKER = "activity-tracker"; + private static final String SLEEP_MEASURE_MODEL_AURA = "aura"; + + private static final String SLEEP_MEASURE_STATE_UNKNOWN = "unknown"; + private static final String SLEEP_MEASURE_STATE_AWAKE = "awake"; + private static final String SLEEP_MEASURE_STATE_LIGHT_SLEEP = "light-sleep"; + private static final String SLEEP_MEASURE_STATE_DEEP_SLEEP = "deep-sleep"; + private static final String SLEEP_MEASURE_STATE_REM_SLEEP = "rem-sleep"; + + private static final String SLEEP_MEASURE_START_DATE = "start_date"; + private static final String SLEEP_MEASURE_END_DATE = "end_date"; + private static final String SLEEP_MEASURE_STATE = "state"; + private static final String SLEEP_MEASURE_MEASUREMENT_DEVICE = "measurement_device"; + + + private static final String TABLE_SLEEP_SUMMARY_HISTORY = "sleep_summary_history"; + private static final String SLEEP_SUMMARY_MODEL_UNKNOWN = "unknown"; + private static final String SLEEP_SUMMARY_MODEL_ACTIVITY_TRACKER = "activity-tracker"; + private static final String SLEEP_SUMMARY_MODEL_AURA = "aura"; + + private static final String SLEEP_SUMMARY_START_DATE = "start_date"; + private static final String SLEEP_SUMMARY_END_DATE = "end_date"; + private static final String SLEEP_SUMMARY_TIMEZONE = "timezone"; + private static final String SLEEP_SUMMARY_MEASUREMENT_DEVICE = "measurement_device"; + private static final String SLEEP_SUMMARY_WAKE_DURATION = "wake_duration"; + private static final String SLEEP_SUMMARY_LIGHT_SLEEP_DURATION = "light_sleep_duration"; + private static final String SLEEP_SUMMARY_DEEP_SLEEP_DURATION = "deep_sleep_duration"; + private static final String SLEEP_SUMMARY_TO_SLEEP_DURATION = "to_sleep_duration"; + private static final String SLEEP_SUMMARY_WAKE_COUNT = "wake_count"; + private static final String SLEEP_SUMMARY_REM_SLEEP_DURATION = "rem_sleep_duration"; + private static final String SLEEP_SUMMARY_TO_WAKE_DURATION = "to_wake_duration"; + + private static final String TABLE_WORKOUT_HISTORY = "workout_history"; + + private static final String HISTORY_OBSERVED = "observed"; + private static final String DATABASE_PATH = "pdk-withings-device.sqlite";; + private static final int DATABASE_VERSION = 1; + + private static final String LAST_DATA_FETCH = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.LAST_DATA_FETCH"; + + private static final String DATA_FETCH_INTERVAL = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.DATA_FETCH_INTERVAL"; + private static final long DATA_FETCH_INTERVAL_DEFAULT = (60 * 60 * 1000); + + private static final String ACTIVITY_MEASURES_ENABLED = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.ACTIVITY_MEASURES_ENABLED"; + private static final boolean ACTIVITY_MEASURES_ENABLED_DEFAULT = true; + + private static final String BODY_MEASURES_ENABLED = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.BODY_MEASURES_ENABLED"; + private static final boolean BODY_MEASURES_ENABLED_DEFAULT = true; + + private static final String INTRADAY_ACTIVITY_ENABLED = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.INTRADAY_ACTIVITY_ENABLED"; + private static final boolean INTRADAY_ACTIVITY_ENABLED_DEFAULT = false; + + private static final String SLEEP_MEASURES_ENABLED = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.SLEEP_MEASURES_ENABLED"; + private static final boolean SLEEP_MEASURES_ENABLED_DEFAULT = true; + + private static final String SLEEP_SUMMARY_ENABLED = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.SLEEP_SUMMARY_ENABLED"; + private static final boolean SLEEP_SUMMARY_ENABLED_DEFAULT = true; + + private static final String WORKOUTS_ENABLED = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.WORKOUTS_ENABLED"; + private static final boolean WORKOUTS_ENABLED_DEFAULT = true; + + public static final String OPTION_OAUTH_CALLBACK_URL = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.OPTION_CALLBACK_URL"; + public static final String OPTION_OAUTH_CONSUMER_KEY = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.OPTION_OAUTH_CONSUMER_KEY"; + public static final String OPTION_OAUTH_CONSUMER_SECRET = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.OPTION_OAUTH_CONSUMER_SECRET"; + private static final String OPTION_OAUTH_ACCESS_TOKEN = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN"; + private static final String OPTION_OAUTH_ACCESS_TOKEN_SECRET = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN_SECRET"; + private static final String OPTION_OAUTH_REQUEST_SECRET = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.OPTION_OAUTH_REQUEST_SECRET"; + private static final String OPTION_OAUTH_ACCESS_USER_ID = "com.audacious_software.passive_data_kit.generators.wearables.WithingsDevice.OPTION_OAUTH_ACCESS_USER_ID"; + + private static final String API_ACTION_ACTIVITY_URL = "https://wbsapi.withings.net/v2/measure?action=getactivity"; + private static final String API_ACTION_BODY_MEASURES_URL = "https://wbsapi.withings.net/measure?action=getmeas"; + private static final String API_ACTION_INTRADAY_ACTIVITY_URL = "https://wbsapi.withings.net/v2/measure?action=getintradayactivity"; + private static final String API_ACTION_SLEEP_MEASURES_URL = "https://wbsapi.withings.net/v2/sleep?action=get"; + private static final String API_ACTION_SLEEP_SUMMARY_URL = "https://wbsapi.withings.net/v2/sleep?action=getsummary"; + private static final String API_ACTION_WORKOUTS_URL = "https://wbsapi.withings.net/v2/measure?action=getworkouts"; + public static final String API_OAUTH_CALLBACK_PATH = "/oauth/withings"; + + private static WithingsDevice sInstance = null; + private Context mContext = null; + private SQLiteDatabase mDatabase = null; + private Handler mHandler = null; + private Map mProperties = new HashMap<>(); + + public static WithingsDevice getInstance(Context context) { + if (WithingsDevice.sInstance == null) { + WithingsDevice.sInstance = new WithingsDevice(context.getApplicationContext()); + } + + return WithingsDevice.sInstance; + } + + public WithingsDevice(Context context) { + super(context); + + this.mContext = context.getApplicationContext(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.mContext); + + SharedPreferences.Editor e = prefs.edit(); + e.remove(WithingsDevice.LAST_DATA_FETCH); + e.apply(); + } + + public static void start(final Context context) { + WithingsDevice.getInstance(context).startGenerator(); + } + + private void startGenerator() { + final WithingsDevice me = this; + + if (this.mHandler != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + this.mHandler.getLooper().quitSafely(); + } else { + this.mHandler.getLooper().quit(); + } + + this.mHandler = null; + } + + final Runnable fetchData = new Runnable() { + @Override + public void run() { + Log.e("PDK", "WITHINGS FETCH DATA"); + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(me.mContext); + long fetchInterval = prefs.getLong(WithingsDevice.DATA_FETCH_INTERVAL, WithingsDevice.DATA_FETCH_INTERVAL_DEFAULT); + + if (me.approvalGranted()) { + Log.e("PDK", "WITHINGS APPROVED"); + + long lastFetch = prefs.getLong(WithingsDevice.LAST_DATA_FETCH, 0); + + long now = System.currentTimeMillis(); + + if (now - lastFetch > fetchInterval) { + Log.e("PDK", "TIME TO FETCH"); + + Runnable r = new Runnable() { + @Override + public void run() { + if (prefs.getBoolean(WithingsDevice.ACTIVITY_MEASURES_ENABLED, WithingsDevice.ACTIVITY_MEASURES_ENABLED_DEFAULT)) { + me.fetchActivityMeasures(); + } + + if (prefs.getBoolean(WithingsDevice.BODY_MEASURES_ENABLED, WithingsDevice.BODY_MEASURES_ENABLED_DEFAULT)) { + me.fetchBodyMeasures(); + } + + if (prefs.getBoolean(WithingsDevice.INTRADAY_ACTIVITY_ENABLED, WithingsDevice.INTRADAY_ACTIVITY_ENABLED_DEFAULT)) { + me.fetchIntradayActivities(); + } + + if (prefs.getBoolean(WithingsDevice.SLEEP_MEASURES_ENABLED, WithingsDevice.SLEEP_MEASURES_ENABLED_DEFAULT)) { + me.fetchSleepMeasures(); + } + + if (prefs.getBoolean(WithingsDevice.SLEEP_SUMMARY_ENABLED, WithingsDevice.SLEEP_SUMMARY_ENABLED_DEFAULT)) { + me.fetchSleepSummary(); + } + + if (prefs.getBoolean(WithingsDevice.WORKOUTS_ENABLED, WithingsDevice.WORKOUTS_ENABLED_DEFAULT)) { + me.fetchWorkouts(); + } + } + }; + + Thread t = new Thread(r); + t.start(); + + SharedPreferences.Editor e = prefs.edit(); + e.putLong(WithingsDevice.LAST_DATA_FETCH, now); + e.apply(); + } else { + Log.e("PDK", "NOT TIME TO FETCH"); + } + } else { + Log.e("PDK", "WITHINGS NOT APPROVED"); + } + + if (me.mHandler != null) { + me.mHandler.postDelayed(this, fetchInterval); + } + } + }; + + File path = PassiveDataKit.getGeneratorsStorage(this.mContext); + + path = new File(path, WithingsDevice.DATABASE_PATH); + + this.mDatabase = SQLiteDatabase.openOrCreateDatabase(path, null); + + int version = this.getDatabaseVersion(this.mDatabase); + + switch (version) { + case 0: + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_withings_create_activity_measure_history_table)); + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_withings_create_body_measure_history_table)); + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_withings_create_intraday_activity_history_table)); + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_withings_create_sleep_measure_history_table)); + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_withings_create_sleep_summary_history_table)); + this.mDatabase.execSQL(this.mContext.getString(R.string.pdk_generator_withings_create_workout_history_table)); + } + + this.setDatabaseVersion(this.mDatabase, WithingsDevice.DATABASE_VERSION); + + Runnable r = new Runnable() { + @Override + public void run() { + Looper.prepare(); + + me.mHandler = new Handler(); + + Looper.loop(); + } + }; + + Thread t = new Thread(r); + t.start(); + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + me.mHandler.post(fetchData); + + Generators.getInstance(this.mContext).registerCustomViewClass(WithingsDevice.GENERATOR_IDENTIFIER, WithingsDevice.class); + } + + private String getProperty(String key) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.mContext); + + if (WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN.equals(key)) { + return prefs.getString(WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN, null); + } else if (WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN_SECRET.equals(key)) { + return prefs.getString(WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN_SECRET, null); + } else if (WithingsDevice.OPTION_OAUTH_ACCESS_USER_ID.equals(key)) { + return prefs.getString(WithingsDevice.OPTION_OAUTH_ACCESS_USER_ID, null); + } + + return this.mProperties.get(key); + } + + private JSONObject queryApi(String apiUrl) { + final WithingsDevice me = this; + + String apiKey = this.getProperty(WithingsDevice.OPTION_OAUTH_CONSUMER_KEY); + String apiSecret = this.getProperty(WithingsDevice.OPTION_OAUTH_CONSUMER_SECRET); + String token = this.getProperty(WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN); + String tokenSecret = this.getProperty(WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN_SECRET); + + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + String startDate = null; + String endDate = null; + + long startTime = 0; + long endTime = 0; + + if (apiKey != null && apiSecret != null && token != null && tokenSecret != null) { + if (WithingsDevice.API_ACTION_ACTIVITY_URL.equals(apiUrl) || + WithingsDevice.API_ACTION_SLEEP_SUMMARY_URL.equals(apiUrl) || + WithingsDevice.API_ACTION_WORKOUTS_URL.equals(apiUrl)) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + + startDate = format.format(cal.getTime()); + + cal.add(Calendar.DATE, 1); + + endDate = format.format(cal.getTime()); + } else if (WithingsDevice.API_ACTION_BODY_MEASURES_URL.equals(apiUrl) || + WithingsDevice.API_ACTION_INTRADAY_ACTIVITY_URL.equals(apiUrl) || + WithingsDevice.API_ACTION_SLEEP_MEASURES_URL.equals(apiUrl)) { + + startTime = cal.getTimeInMillis() / 1000; + + cal.add(Calendar.DATE, 1); + + endTime = cal.getTimeInMillis() / 1000; + } + Uri apiUri = Uri.parse(apiUrl); + + Uri.Builder builder = new Uri.Builder(); + builder.scheme(apiUri.getScheme()); + builder.authority(apiUri.getAuthority()); + builder.path(apiUri.getPath()); + + try { + String signature = "GET&" + URLEncoder.encode(builder.build().toString(), "UTF-8"); + + String action = apiUri.getQueryParameter("action"); + String nonce = UUID.randomUUID().toString(); + + builder.appendQueryParameter("action", action); + + if (endTime != 0) { + builder.appendQueryParameter("enddate", "" + endTime); + } + + if (endDate != null) { + builder.appendQueryParameter("enddateymd", endDate); + } + + builder.appendQueryParameter("oauth_consumer_key", apiKey); + builder.appendQueryParameter("oauth_nonce", nonce); + builder.appendQueryParameter("oauth_signature_method", "HMAC-SHA1"); + builder.appendQueryParameter("oauth_timestamp", "" + (System.currentTimeMillis() / 1000)); + builder.appendQueryParameter("oauth_token", token); + builder.appendQueryParameter("oauth_version", "1.0"); + + if (startTime != 0) { + builder.appendQueryParameter("startdate", "" + startTime); + } + + if (startDate != null) { + builder.appendQueryParameter("startdateymd", startDate); + } + + builder.appendQueryParameter("userid", this.getProperty(WithingsDevice.OPTION_OAUTH_ACCESS_USER_ID)); + + Uri baseUri = builder.build(); + + signature += "&" + URLEncoder.encode(baseUri.getEncodedQuery(), "UTF-8"); + + String key = apiSecret + "&" + tokenSecret; + + SecretKeySpec secret = new SecretKeySpec(key.getBytes(), "HmacSHA1"); + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(secret); + + byte[] bytes = mac.doFinal(signature.getBytes(Charset.forName("UTF-8"))); + + signature = Base64.encodeToString(bytes, Base64.DEFAULT); + + builder.appendQueryParameter("oauth_signature", signature.trim()); + + Uri uri = builder.build(); + + OkHttpClient client = new OkHttpClient(); + + Request request = new Request.Builder() + .url(uri.toString()) + .build(); + + Response response = client.newCall(request).execute(); + + if (response.isSuccessful()) { + return new JSONObject(response.body().string()); + } + } catch (NoSuchAlgorithmException e) { + AppEvent.getInstance(me.mContext).logThrowable(e); + } catch (UnsupportedEncodingException e) { + AppEvent.getInstance(me.mContext).logThrowable(e); + } catch (IOException e) { + AppEvent.getInstance(me.mContext).logThrowable(e); + } catch (InvalidKeyException e) { + AppEvent.getInstance(me.mContext).logThrowable(e); + } catch (JSONException e) { + AppEvent.getInstance(me.mContext).logThrowable(e); + } + } + + return null; + } + + private void fetchActivityMeasures() { + JSONObject response = this.queryApi(WithingsDevice.API_ACTION_ACTIVITY_URL); + + if (response != null) { + try { + if (response.getInt("status") == 0) { + JSONObject body = response.getJSONObject("body"); + JSONArray activities = body.getJSONArray("activities"); + + for (int i = 0; i < activities.length(); i++) { + JSONObject activity = activities.getJSONObject(i); + + Calendar cal = Calendar.getInstance(); + + String[] tokens = activity.getString("date").split("-"); + + cal.set(Calendar.YEAR, Integer.parseInt(tokens[0])); + cal.set(Calendar.MONTH, Integer.parseInt(tokens[1])); + cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(tokens[2])); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + ContentValues values = new ContentValues(); + values.put(WithingsDevice.HISTORY_OBSERVED, System.currentTimeMillis()); + + values.put(WithingsDevice.ACTIVITY_MEASURE_HISTORY_DATE_START, cal.getTimeInMillis()); + values.put(WithingsDevice.ACTIVITY_MEASURE_HISTORY_TIMEZONE, activity.getString("timezone")); + values.put(WithingsDevice.ACTIVITY_MEASURE_STEPS, activity.getDouble("steps")); + values.put(WithingsDevice.ACTIVITY_MEASURE_DISTANCE, activity.getDouble("distance")); + values.put(WithingsDevice.ACTIVITY_MEASURE_ACTIVE_CALORIES, activity.getDouble("calories")); + values.put(WithingsDevice.ACTIVITY_MEASURE_TOTAL_CALORIES, activity.getDouble("totalcalories")); + values.put(WithingsDevice.ACTIVITY_MEASURE_ELEVATION, activity.getDouble("elevation")); + values.put(WithingsDevice.ACTIVITY_MEASURE_SOFT_ACTIVITY_DURATION, activity.getDouble("soft")); + values.put(WithingsDevice.ACTIVITY_MEASURE_MODERATE_ACTIVITY_DURATION, activity.getDouble("moderate")); + values.put(WithingsDevice.ACTIVITY_MEASURE_INTENSE_ACTIVITY_DURATION, activity.getDouble("intense")); + + this.mDatabase.insert(WithingsDevice.TABLE_ACTIVITY_MEASURE_HISTORY, null, values); + + Bundle updated = new Bundle(); + + updated.putLong(WithingsDevice.HISTORY_OBSERVED, System.currentTimeMillis()); + updated.putLong(WithingsDevice.ACTIVITY_MEASURE_HISTORY_DATE_START, cal.getTimeInMillis()); + updated.putString(WithingsDevice.ACTIVITY_MEASURE_HISTORY_TIMEZONE, activity.getString("timezone")); + updated.putDouble(WithingsDevice.ACTIVITY_MEASURE_STEPS, activity.getDouble("steps")); + updated.putDouble(WithingsDevice.ACTIVITY_MEASURE_DISTANCE, activity.getDouble("distance")); + updated.putDouble(WithingsDevice.ACTIVITY_MEASURE_ACTIVE_CALORIES, activity.getDouble("calories")); + updated.putDouble(WithingsDevice.ACTIVITY_MEASURE_TOTAL_CALORIES, activity.getDouble("totalcalories")); + updated.putDouble(WithingsDevice.ACTIVITY_MEASURE_ELEVATION, activity.getDouble("elevation")); + updated.putDouble(WithingsDevice.ACTIVITY_MEASURE_SOFT_ACTIVITY_DURATION, activity.getDouble("soft")); + updated.putDouble(WithingsDevice.ACTIVITY_MEASURE_MODERATE_ACTIVITY_DURATION, activity.getDouble("moderate")); + updated.putDouble(WithingsDevice.ACTIVITY_MEASURE_INTENSE_ACTIVITY_DURATION, activity.getDouble("intense")); + updated.putString(WithingsDevice.DATASTREAM, WithingsDevice.DATASTREAM_ACTIVITY_MEASURES); + + Generators.getInstance(this.mContext).notifyGeneratorUpdated(WithingsDevice.GENERATOR_IDENTIFIER, updated); + } + } + } catch (JSONException e) { + AppEvent.getInstance(this.mContext).logThrowable(e); + } + } + } + + private void fetchBodyMeasures() { + JSONObject response = this.queryApi(WithingsDevice.API_ACTION_BODY_MEASURES_URL); + + if (response != null) { + try { + if (response.getInt("status") == 0) { + JSONObject body = response.getJSONObject("body"); + JSONArray measureGroups = body.getJSONArray("measuregrps"); + + for (int i = 0; i < measureGroups.length(); i++) { + JSONObject measureGroup = measureGroups.getJSONObject(i); + + long measureDate = measureGroup.getLong("date"); + long now = System.currentTimeMillis(); + + String status = WithingsDevice.BODY_MEASURE_STATUS_UNKNOWN; + + switch (measureGroup.getInt("attrib")) { + case 0: + status = WithingsDevice.BODY_MEASURE_STATUS_USER_DEVICE; + break; + case 1: + status = WithingsDevice.BODY_MEASURE_STATUS_SHARED_DEVICE; + break; + case 2: + status = WithingsDevice.BODY_MEASURE_STATUS_MANUAL_ENTRY; + break; + case 4: + status = WithingsDevice.BODY_MEASURE_STATUS_MANUAL_ENTRY_CREATION; + break; + case 5: + status = WithingsDevice.BODY_MEASURE_STATUS_AUTO_DEVICE; + break; + case 7: + status = WithingsDevice.BODY_MEASURE_STATUS_MEASURE_CONFIRMED; + break; + } + + String category = WithingsDevice.BODY_MEASURE_CATEGORY_UNKNOWN; + + switch (measureGroup.getInt("category")) { + case 1: + category = WithingsDevice.BODY_MEASURE_CATEGORY_REAL_MEASUREMENTS; + break; + case 2: + category = WithingsDevice.BODY_MEASURE_CATEGORY_USER_OBJECTIVES; + break; + } + + JSONArray measures = measureGroup.getJSONArray("measures"); + + for (int j = 0; j < measures.length(); j++) { + JSONObject measure = measures.optJSONObject(j); + + ContentValues values = new ContentValues(); + values.put(WithingsDevice.HISTORY_OBSERVED, now); + + String type = WithingsDevice.BODY_MEASURE_TYPE_UNKNOWN; + + switch (measure.getInt("type")) { + case 1: + type = WithingsDevice.BODY_MEASURE_TYPE_WEIGHT; + break; + case 4: + type = WithingsDevice.BODY_MEASURE_TYPE_HEIGHT; + break; + case 5: + type = WithingsDevice.BODY_MEASURE_TYPE_FAT_FREE_MASS; + break; + case 6: + type = WithingsDevice.BODY_MEASURE_TYPE_FAT_RATIO; + break; + case 8: + type = WithingsDevice.BODY_MEASURE_TYPE_FAT_MASS_WEIGHT; + break; + case 9: + type = WithingsDevice.BODY_MEASURE_TYPE_DIASTOLIC_BLOOD_PRESSURE; + break; + case 10: + type = WithingsDevice.BODY_MEASURE_TYPE_SYSTOLIC_BLOOD_PRESSURE; + break; + case 11: + type = WithingsDevice.BODY_MEASURE_TYPE_HEART_PULSE; + break; + case 12: + type = WithingsDevice.BODY_MEASURE_TYPE_TEMPERATURE; + break; + case 54: + type = WithingsDevice.BODY_MEASURE_TYPE_OXYGEN_SATURATION; + break; + case 71: + type = WithingsDevice.BODY_MEASURE_TYPE_BODY_TEMPERATURE; + break; + case 73: + type = WithingsDevice.BODY_MEASURE_TYPE_SKIN_TEMPERATURE; + break; + case 76: + type = WithingsDevice.BODY_MEASURE_TYPE_MUSCLE_MASS; + break; + case 77: + type = WithingsDevice.BODY_MEASURE_TYPE_HYDRATION; + break; + case 88: + type = WithingsDevice.BODY_MEASURE_TYPE_BONE_MASS; + break; + case 91: + type = WithingsDevice.BODY_MEASURE_TYPE_PULSE_WAVE_VELOCITY; + break; + } + + double value = measure.getDouble("value") * Math.pow(10, measure.getDouble("unit")); + + values.put(WithingsDevice.BODY_MEASURE_HISTORY_DATE, measureDate); + values.put(WithingsDevice.BODY_MEASURE_HISTORY_STATUS, status); + values.put(WithingsDevice.BODY_MEASURE_HISTORY_CATEGORY, category); + values.put(WithingsDevice.BODY_MEASURE_HISTORY_TYPE, type); + values.put(WithingsDevice.BODY_MEASURE_HISTORY_VALUE, value); + + this.mDatabase.insert(WithingsDevice.TABLE_BODY_MEASURE_HISTORY, null, values); + + Bundle updated = new Bundle(); + + updated.putLong(WithingsDevice.HISTORY_OBSERVED, System.currentTimeMillis()); + updated.putLong(WithingsDevice.BODY_MEASURE_HISTORY_DATE, measureDate); + updated.putString(WithingsDevice.BODY_MEASURE_HISTORY_STATUS, status); + updated.putString(WithingsDevice.BODY_MEASURE_HISTORY_CATEGORY, category); + updated.putString(WithingsDevice.BODY_MEASURE_HISTORY_TYPE, type); + updated.putDouble(WithingsDevice.BODY_MEASURE_HISTORY_VALUE, value); + + Generators.getInstance(this.mContext).notifyGeneratorUpdated(WithingsDevice.GENERATOR_IDENTIFIER, updated); + } + } + } + } catch (JSONException e) { + AppEvent.getInstance(this.mContext).logThrowable(e); + } + } + } + + private void fetchIntradayActivities() { + JSONObject response = this.queryApi(WithingsDevice.API_ACTION_INTRADAY_ACTIVITY_URL); + + if (response != null) { + // TODO + } + } + + private void fetchSleepMeasures() { + JSONObject response = this.queryApi(WithingsDevice.API_ACTION_SLEEP_MEASURES_URL); + + if (response != null) { + try { + if (response.getInt("status") == 0) { + JSONObject body = response.getJSONObject("body"); + + String model = WithingsDevice.SLEEP_MEASURE_MODEL_UNKNOWN; + + switch(body.getInt("model")) { + case 16: + model = WithingsDevice.SLEEP_MEASURE_MODEL_ACTIVITY_TRACKER; + break; + case 32: + model = WithingsDevice.SLEEP_MEASURE_MODEL_AURA; + break; + } + + JSONArray series = body.getJSONArray("series"); + + for (int i = 0; i < series.length(); i++) { + JSONObject item = series.getJSONObject(i); + + long now = System.currentTimeMillis(); + + String state = WithingsDevice.SLEEP_MEASURE_STATE_UNKNOWN; + + switch (item.getInt("state")) { + case 0: + state = WithingsDevice.SLEEP_MEASURE_STATE_AWAKE; + break; + case 1: + state = WithingsDevice.SLEEP_MEASURE_STATE_LIGHT_SLEEP; + break; + case 2: + state = WithingsDevice.SLEEP_MEASURE_STATE_DEEP_SLEEP; + break; + case 3: + state = WithingsDevice.SLEEP_MEASURE_STATE_REM_SLEEP; + break; + } + + ContentValues values = new ContentValues(); + values.put(WithingsDevice.HISTORY_OBSERVED, now); + values.put(WithingsDevice.SLEEP_MEASURE_START_DATE, item.getLong("startdate")); + values.put(WithingsDevice.SLEEP_MEASURE_END_DATE, item.getLong("enddate")); + values.put(WithingsDevice.SLEEP_MEASURE_STATE, state); + values.put(WithingsDevice.SLEEP_MEASURE_MEASUREMENT_DEVICE, model); + + this.mDatabase.insert(WithingsDevice.TABLE_SLEEP_MEASURE_HISTORY, null, values); + + Bundle updated = new Bundle(); + updated.putLong(WithingsDevice.HISTORY_OBSERVED, System.currentTimeMillis()); + updated.putLong(WithingsDevice.SLEEP_MEASURE_START_DATE, item.getLong("startdate")); + updated.putLong(WithingsDevice.SLEEP_MEASURE_END_DATE, item.getLong("enddate")); + updated.putString(WithingsDevice.SLEEP_MEASURE_STATE, state); + updated.putString(WithingsDevice.SLEEP_MEASURE_MEASUREMENT_DEVICE, model); + + Generators.getInstance(this.mContext).notifyGeneratorUpdated(WithingsDevice.GENERATOR_IDENTIFIER, updated); + } + } + } catch (JSONException e) { + AppEvent.getInstance(this.mContext).logThrowable(e); + } + } + } + + private void fetchSleepSummary() { + JSONObject response = this.queryApi(WithingsDevice.API_ACTION_SLEEP_SUMMARY_URL); + + if (response != null) { + try { + if (response.getInt("status") == 0) { + JSONObject body = response.getJSONObject("body"); + + JSONArray series = body.getJSONArray("series"); + + long now = System.currentTimeMillis(); + + for (int i = 0; i < series.length(); i++) { + JSONObject item = series.getJSONObject(i); + + String timezone = body.getString("timezone"); + + String model = WithingsDevice.SLEEP_SUMMARY_MODEL_UNKNOWN; + + switch(body.getInt("model")) { + case 16: + model = WithingsDevice.SLEEP_SUMMARY_MODEL_ACTIVITY_TRACKER; + break; + case 32: + model = WithingsDevice.SLEEP_SUMMARY_MODEL_AURA; + break; + } + + JSONObject data = item.getJSONObject("data"); + + ContentValues values = new ContentValues(); + values.put(WithingsDevice.HISTORY_OBSERVED, now); + values.put(WithingsDevice.SLEEP_SUMMARY_START_DATE, item.getLong("startdate")); + values.put(WithingsDevice.SLEEP_SUMMARY_END_DATE, item.getLong("enddate")); + values.put(WithingsDevice.SLEEP_SUMMARY_TIMEZONE, timezone); + values.put(WithingsDevice.SLEEP_SUMMARY_MEASUREMENT_DEVICE, model); + values.put(WithingsDevice.SLEEP_SUMMARY_WAKE_DURATION, data.getDouble("wakeupduration")); + values.put(WithingsDevice.SLEEP_SUMMARY_LIGHT_SLEEP_DURATION, data.getDouble("lightsleepduration")); + values.put(WithingsDevice.SLEEP_SUMMARY_DEEP_SLEEP_DURATION, data.getDouble("deepsleepduration")); + values.put(WithingsDevice.SLEEP_SUMMARY_TO_SLEEP_DURATION, data.getDouble("durationtosleep")); + values.put(WithingsDevice.SLEEP_SUMMARY_WAKE_COUNT, data.getDouble("wakeupcount")); + + if (data.has("remsleepduration")) { + values.put(WithingsDevice.SLEEP_SUMMARY_REM_SLEEP_DURATION, data.getDouble("remsleepduration")); + } + + if (data.has("durationtowakeup")) { + values.put(WithingsDevice.SLEEP_SUMMARY_TO_WAKE_DURATION, data.getDouble("durationtowakeup")); + } + + this.mDatabase.insert(WithingsDevice.TABLE_SLEEP_MEASURE_HISTORY, null, values); + + Bundle updated = new Bundle(); + updated.putLong(WithingsDevice.HISTORY_OBSERVED, now); + updated.putLong(WithingsDevice.SLEEP_SUMMARY_START_DATE, item.getLong("startdate")); + updated.putLong(WithingsDevice.SLEEP_SUMMARY_END_DATE, item.getLong("enddate")); + updated.putString(WithingsDevice.SLEEP_SUMMARY_TIMEZONE, timezone); + updated.putString(WithingsDevice.SLEEP_SUMMARY_MEASUREMENT_DEVICE, model); + updated.putDouble(WithingsDevice.SLEEP_SUMMARY_WAKE_DURATION, data.getDouble("wakeupduration")); + updated.putDouble(WithingsDevice.SLEEP_SUMMARY_LIGHT_SLEEP_DURATION, data.getDouble("lightsleepduration")); + updated.putDouble(WithingsDevice.SLEEP_SUMMARY_DEEP_SLEEP_DURATION, data.getDouble("deepsleepduration")); + updated.putDouble(WithingsDevice.SLEEP_SUMMARY_TO_SLEEP_DURATION, data.getDouble("durationtosleep")); + updated.putDouble(WithingsDevice.SLEEP_SUMMARY_WAKE_COUNT, data.getDouble("wakeupcount")); + + if (data.has("remsleepduration")) { + updated.putDouble(WithingsDevice.SLEEP_SUMMARY_REM_SLEEP_DURATION, data.getDouble("remsleepduration")); + } + + if (data.has("durationtowakeup")) { + updated.putDouble(WithingsDevice.SLEEP_SUMMARY_TO_WAKE_DURATION, data.getDouble("durationtowakeup")); + } + + Generators.getInstance(this.mContext).notifyGeneratorUpdated(WithingsDevice.GENERATOR_IDENTIFIER, updated); + } + } + } catch (JSONException e) { + AppEvent.getInstance(this.mContext).logThrowable(e); + } + } + } + + private void fetchWorkouts() { + JSONObject response = this.queryApi(WithingsDevice.API_ACTION_WORKOUTS_URL); + + if (response != null) { + + } + } + + private boolean approvalGranted() { + String apiToken = this.getProperty(WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN); + + if (apiToken != null) { + return true; + } + + return false; + } + + public static boolean isEnabled(Context context) { + SharedPreferences prefs = Generators.getInstance(context).getSharedPreferences(context); + + return prefs.getBoolean(WithingsDevice.ENABLED, WithingsDevice.ENABLED_DEFAULT); + } + + public static boolean isRunning(Context context) { + if (WithingsDevice.sInstance == null) { + return false; + } + + return WithingsDevice.sInstance.mHandler != null; + } + + public static ArrayList diagnostics(final Context context) { + final WithingsDevice me = WithingsDevice.getInstance(context); + + ArrayList actions = new ArrayList<>(); + + actions.add(new DiagnosticAction(context.getString(R.string.diagnostic_withings_auth_required_title), context.getString(R.string.diagnostic_withings_auth_required), new Runnable() { + + @Override + public void run() { + Runnable r = new Runnable() { + @Override + public void run() { + try { + String requestUrl = "https://oauth.withings.com/account/request_token"; + + Uri apiUri = Uri.parse(requestUrl); + + Uri.Builder builder = new Uri.Builder(); + builder.scheme(apiUri.getScheme()); + builder.authority(apiUri.getAuthority()); + builder.path(apiUri.getPath()); + + String signature = "GET&" + URLEncoder.encode(builder.build().toString(), "UTF-8"); + + String callbackUrl = me.getProperty(WithingsDevice.OPTION_OAUTH_CALLBACK_URL); + String apiKey = me.getProperty(WithingsDevice.OPTION_OAUTH_CONSUMER_KEY); + String apiSecret = me.getProperty(WithingsDevice.OPTION_OAUTH_CONSUMER_SECRET); + + String nonce = UUID.randomUUID().toString(); + + builder.appendQueryParameter("oauth_callback", callbackUrl); + builder.appendQueryParameter("oauth_consumer_key", apiKey); + builder.appendQueryParameter("oauth_nonce", nonce); + builder.appendQueryParameter("oauth_signature_method", "HMAC-SHA1"); + builder.appendQueryParameter("oauth_timestamp", "" + (System.currentTimeMillis() / 1000)); + builder.appendQueryParameter("oauth_version", "1.0"); + + Uri baseUri = builder.build(); + + signature += "&" + URLEncoder.encode(baseUri.getEncodedQuery(), "UTF-8"); + + String key = apiSecret + "&"; + + SecretKeySpec secret = new SecretKeySpec(key.getBytes(), "HmacSHA1"); + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(secret); + + byte[] bytes = mac.doFinal(signature.getBytes()); + + signature = Base64.encodeToString(bytes, Base64.DEFAULT).trim(); + + builder.appendQueryParameter("oauth_signature", signature); + + Uri uri = builder.build(); + + OkHttpClient client = new OkHttpClient(); + + Request request = new Request.Builder() + .url(uri.toString()) + .build(); + + Response response = client.newCall(request).execute(); + + String responseBody = response.body().string(); + + StringTokenizer st = new StringTokenizer(responseBody, "&"); + + String requestToken = null; + String requestSecret = null; + + while (st.hasMoreTokens()) { + String authToken = st.nextToken(); + + if (authToken.startsWith("oauth_token=")) { + requestToken = authToken.replace("oauth_token=", ""); + } else if (authToken.startsWith("oauth_token_secret=")) { + requestSecret = authToken.replace("oauth_token_secret=", ""); + } + } + + key = apiSecret + "&" + requestSecret; + + me.setProperty(WithingsDevice.OPTION_OAUTH_REQUEST_SECRET, requestSecret); + + builder = new Uri.Builder(); + builder.scheme("https"); + builder.authority("oauth.withings.com"); + builder.path("/account/authorize"); + + signature = "GET&" + URLEncoder.encode(builder.build().toString(), "UTF-8"); + + nonce = UUID.randomUUID().toString(); + + builder.appendQueryParameter("oauth_consumer_key", apiKey); + builder.appendQueryParameter("oauth_nonce", nonce); + builder.appendQueryParameter("oauth_signature_method", "HMAC-SHA1"); + builder.appendQueryParameter("oauth_timestamp", "" + (System.currentTimeMillis() / 1000)); + builder.appendQueryParameter("oauth_token", requestToken); + builder.appendQueryParameter("oauth_version", "1.0"); + + baseUri = builder.build(); + + signature += "&" + URLEncoder.encode(baseUri.getEncodedQuery(), "UTF-8"); + + secret = new SecretKeySpec(key.getBytes(), "HmacSHA1"); + mac = Mac.getInstance("HmacSHA1"); + mac.init(secret); + + bytes = mac.doFinal(signature.getBytes(Charset.forName("UTF-8"))); + + signature = Base64.encodeToString(bytes, Base64.DEFAULT); + + builder.appendQueryParameter("oauth_signature", signature.trim()); + + Intent intent = new Intent(Intent.ACTION_VIEW, builder.build()); + context.startActivity(intent); + } catch (NoSuchAlgorithmException e) { + AppEvent.getInstance(context).logThrowable(e); + } catch (InvalidKeyException e) { + AppEvent.getInstance(context).logThrowable(e); + } catch (UnsupportedEncodingException e) { + AppEvent.getInstance(context).logThrowable(e); + } catch (IOException e) { + AppEvent.getInstance(context).logThrowable(e); + } + } + }; + + Thread t = new Thread(r); + t.start(); + } + })); + + return actions; + } + + public void finishAuthentication(final Uri responseUri) { + final WithingsDevice me = this; + + Runnable r = new Runnable() { + @Override + public void run() { + try { + String requestUrl = "https://oauth.withings.com/account/access_token"; + + Uri apiUri = Uri.parse(requestUrl); + + Uri.Builder builder = new Uri.Builder(); + builder.scheme(apiUri.getScheme()); + builder.authority(apiUri.getAuthority()); + builder.path(apiUri.getPath()); + + String signature = "GET&" + URLEncoder.encode(builder.build().toString(), "UTF-8"); + + String callbackUrl = me.getProperty(WithingsDevice.OPTION_OAUTH_CALLBACK_URL); + String apiKey = me.getProperty(WithingsDevice.OPTION_OAUTH_CONSUMER_KEY); + String apiSecret = me.getProperty(WithingsDevice.OPTION_OAUTH_CONSUMER_SECRET); + + String nonce = UUID.randomUUID().toString(); + + builder.appendQueryParameter("oauth_consumer_key", apiKey); + builder.appendQueryParameter("oauth_nonce", nonce); + builder.appendQueryParameter("oauth_signature_method", "HMAC-SHA1"); + builder.appendQueryParameter("oauth_timestamp", "" + (System.currentTimeMillis() / 1000)); + builder.appendQueryParameter("oauth_token", responseUri.getQueryParameter("oauth_token")); + builder.appendQueryParameter("oauth_version", "1.0"); + + Uri baseUri = builder.build(); + + signature += "&" + URLEncoder.encode(baseUri.getEncodedQuery(), "UTF-8"); + + String key = apiSecret + "&" + me.getProperty(WithingsDevice.OPTION_OAUTH_REQUEST_SECRET); + + SecretKeySpec secret = new SecretKeySpec(key.getBytes(), "HmacSHA1"); + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(secret); + + byte[] bytes = mac.doFinal(signature.getBytes()); + + signature = Base64.encodeToString(bytes, Base64.DEFAULT).trim(); + + builder.appendQueryParameter("oauth_signature", signature); + + Uri uri = builder.build(); + + OkHttpClient client = new OkHttpClient(); + + Request request = new Request.Builder() + .url(uri.toString()) + .build(); + + Response response = client.newCall(request).execute(); + + String responseBody = response.body().string(); + + StringTokenizer st = new StringTokenizer(responseBody, "&"); + + while (st.hasMoreTokens()) { + String tokenString = st.nextToken(); + + if (tokenString.startsWith("oauth_token=")) { + me.setProperty(WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN, tokenString.replace("oauth_token=", "")); + } else if (tokenString.startsWith("oauth_token_secret=")) { + me.setProperty(WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN_SECRET, tokenString.replace("oauth_token_secret=", "")); + } else if (tokenString.startsWith("userid=")) { + me.setProperty(WithingsDevice.OPTION_OAUTH_ACCESS_USER_ID, tokenString.replace("userid=", "")); + } + } + + me.fetchActivityMeasures(); + + } catch (UnsupportedEncodingException e) { + AppEvent.getInstance(me.mContext).logThrowable(e); + } catch (NoSuchAlgorithmException e) { + AppEvent.getInstance(me.mContext).logThrowable(e); + } catch (IOException e) { + AppEvent.getInstance(me.mContext).logThrowable(e); + } catch (InvalidKeyException e) { + AppEvent.getInstance(me.mContext).logThrowable(e); + } + } + }; + + Thread t = new Thread(r); + t.start(); + } + + public static void bindViewHolder(DataPointViewHolder holder) { +/* + final Context context = holder.itemView.getContext(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + long lastTimestamp = 0; + long lastDuration = 0; + String callType = null; + + long totalIncoming = 0; + long totalOutgoing = 0; + long totalMissed = 0; + long total = 0; + + WithingsDevice generator = WithingsDevice.getInstance(holder.itemView.getContext()); + + View cardContent = holder.itemView.findViewById(R.id.card_content); + View cardEmpty = holder.itemView.findViewById(R.id.card_empty); + TextView dateLabel = (TextView) holder.itemView.findViewById(R.id.generator_data_point_date); +*/ + } + + @Override + public List fetchPayloads() { + return new ArrayList<>(); + } + + public static View fetchView(ViewGroup parent) + { + return LayoutInflater.from(parent.getContext()).inflate(R.layout.card_generator_withings_device, parent, false); + } + + public static long latestPointGenerated(Context context) { + long timestamp = 0; + + WithingsDevice me = WithingsDevice.getInstance(context); + +// Cursor c = me.mDatabase.query(WithingsDevice.TABLE_HISTORY, null, null, null, null, null, WithingsDevice.HISTORY_OBSERVED + " DESC"); +// +// if (c.moveToNext()) { +// timestamp = c.getLong(c.getColumnIndex(WithingsDevice.HISTORY_OBSERVED)); +// } +// +// c.close(); + + return timestamp; + } + + public void setProperty(String key, String value) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.mContext); + + if (WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN.equals(key)) { + SharedPreferences.Editor e = prefs.edit(); + e.putString(WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN, value); + e.apply(); + } else if (WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN_SECRET.equals(key)) { + SharedPreferences.Editor e = prefs.edit(); + e.putString(WithingsDevice.OPTION_OAUTH_ACCESS_TOKEN_SECRET, value); + e.apply(); + } else if (WithingsDevice.OPTION_OAUTH_ACCESS_USER_ID.equals(key)) { + SharedPreferences.Editor e = prefs.edit(); + e.putString(WithingsDevice.OPTION_OAUTH_ACCESS_USER_ID, value); + e.apply(); + } + + this.mProperties.put(key, value); + } +} diff --git a/src/com/audacious_software/passive_data_kit/transmitters/HttpTransmitter.java b/src/com/audacious_software/passive_data_kit/transmitters/HttpTransmitter.java index 93f74d3..1d9a2cb 100755 --- a/src/com/audacious_software/passive_data_kit/transmitters/HttpTransmitter.java +++ b/src/com/audacious_software/passive_data_kit/transmitters/HttpTransmitter.java @@ -46,7 +46,7 @@ import okhttp3.Response; import okio.Buffer; -public class HttpTransmitter extends Transmitter implements Generators.NewDataPointListener { +public class HttpTransmitter extends Transmitter implements Generators.GeneratorUpdatedListener { public static final String UPLOAD_URI = "com.audacious_software.passive_data_kit.transmitters.HttpTransmitter.UPLOAD_URI"; public static final String USER_ID = "com.audacious_software.passive_data_kit.transmitters.HttpTransmitter.USER_ID"; private static final String HASH_ALGORITHM = "com.audacious_software.passive_data_kit.transmitters.HttpTransmitter.HASH_ALGORITHM"; @@ -133,16 +133,23 @@ else if (!options.containsKey(HttpTransmitter.USER_ID)) { this.mContext = context.getApplicationContext(); - Generators.getInstance(this.mContext).addNewDataPointListener(this); + Generators.getInstance(this.mContext).addNewGeneratorUpdatedListener(this); } - private boolean shouldAttemptUpload() { + private boolean shouldAttemptUpload(boolean force) { + if (force) { + return true; + } if (this.mWifiOnly) { - return DeviceInformation.wifiAvailable(this.mContext); + if (DeviceInformation.wifiAvailable(this.mContext) == false) { + return false; + } } if (this.mChargingOnly) { - return DeviceInformation.isPluggedIn(this.mContext); + if (DeviceInformation.isPluggedIn(this.mContext) == false) { + return false; + } } return true; @@ -156,7 +163,7 @@ public void transmit(boolean force) { this.mLastAttempt = 0; } - if (now - this.mLastAttempt < this.mUploadInterval || !this.shouldAttemptUpload()) { + if (now - this.mLastAttempt < this.mUploadInterval || !this.shouldAttemptUpload(force)) { return; } @@ -314,7 +321,6 @@ private int transmitHttpPayload(String payload) { builder = builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"payload\""), RequestBody.create(null, payload)); - RequestBody requestBody = builder.build(); if (this.mUserAgent == null) { @@ -418,12 +424,25 @@ public long transmittedSize() { } @Override - public void onNewDataPoint(String identifier, Bundle data) { + public void onGeneratorUpdated(String identifier, long timestamp, Bundle data) { if (data.keySet().size() > 1) { // Only transmit non-empty bundles... + timestamp = timestamp / 1000; // Convert to seconds... + + Generators generators = Generators.getInstance(this.mContext); + + Bundle metadata = new Bundle(); + if (data.containsKey(Generator.PDK_METADATA)) { - data.getBundle(Generator.PDK_METADATA).putString(Generator.SOURCE, this.mUserId); + metadata = data.getBundle(Generator.PDK_METADATA); } + metadata.putString(Generator.IDENTIFIER, identifier); + metadata.putDouble(Generator.TIMESTAMP, timestamp); + metadata.putString(Generator.GENERATOR, generators.getGeneratorFullName(identifier)); + metadata.putString(Generator.SOURCE, generators.getSource()); + metadata.putString(Generator.SOURCE, this.mUserId); + data.putBundle(Generator.PDK_METADATA, metadata); + if (this.mJsonGenerator == null) { this.mCurrentFile = new File(this.getPendingFolder(), System.currentTimeMillis() + HttpTransmitter.TEMP_EXTENSION); @@ -591,6 +610,10 @@ else if (value instanceof Bundle) { } } + public void setUserId(String userId) { + this.mUserId = userId; + } + public static class IncompleteConfigurationException extends RuntimeException { public IncompleteConfigurationException(String message) { super(message);