From ed7d0dedbdfdc94ffb8cff55f725df20b9142f46 Mon Sep 17 00:00:00 2001 From: Stephan Schultz Date: Fri, 2 Nov 2018 13:17:17 +0100 Subject: [PATCH 01/15] Created recording activity --- app/build.gradle | 2 ++ app/src/main/AndroidManifest.xml | 6 +++++ .../HomeActivity.java | 7 ++++- .../RecordingActivity.java | 15 +++++++++++ .../main/res/layout/activity_recording.xml | 27 +++++++++++++++++++ app/src/main/res/layout/content_recording.xml | 12 +++++++++ app/src/main/res/menu/beacon_view.xml | 7 ++++- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 5 ++++ app/src/main/res/values/styles.xml | 11 +++++++- 10 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java create mode 100644 app/src/main/res/layout/activity_recording.xml create mode 100644 app/src/main/res/layout/content_recording.xml diff --git a/app/build.gradle b/app/build.gradle index 42415c1..7b877f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,6 +37,8 @@ dependencies { implementation 'com.android.support:design:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'com.google.android.material:material:1.0.0' + implementation 'com.google.android.gms:play-services-location:16.0.0' implementation "com.polidea.rxandroidble:rxandroidble:1.4.3" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b4d2e16..8b23f84 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,11 +18,17 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> + + + diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/HomeActivity.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/HomeActivity.java index 4aa8109..fee7175 100644 --- a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/HomeActivity.java +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/HomeActivity.java @@ -14,6 +14,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.EditText; import com.nexenio.bleindoorpositioningdemo.bluetooth.BluetoothClient; import com.nexenio.bleindoorpositioningdemo.location.AndroidLocationProvider; @@ -56,8 +57,12 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { + case R.id.menu_record: + Intent intent = new Intent(this, RecordingActivity.class); + startActivity(intent); + return true; case R.id.menu_filter: - Log.w(TAG, "BeaconFilter"); + Log.w(TAG, "Filter"); return true; default: break; diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java new file mode 100644 index 0000000..d6509a2 --- /dev/null +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java @@ -0,0 +1,15 @@ +package com.nexenio.bleindoorpositioningdemo; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; + +public class RecordingActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_recording); + + } + +} diff --git a/app/src/main/res/layout/activity_recording.xml b/app/src/main/res/layout/activity_recording.xml new file mode 100644 index 0000000..8416626 --- /dev/null +++ b/app/src/main/res/layout/activity_recording.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_recording.xml b/app/src/main/res/layout/content_recording.xml new file mode 100644 index 0000000..4019ecb --- /dev/null +++ b/app/src/main/res/layout/content_recording.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/menu/beacon_view.xml b/app/src/main/res/menu/beacon_view.xml index f1444f0..0ddc130 100644 --- a/app/src/main/res/menu/beacon_view.xml +++ b/app/src/main/res/menu/beacon_view.xml @@ -2,9 +2,14 @@ + + 16dp 16dp + 16dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fffde19..cde600e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,6 +27,9 @@ Frequency in Hz Variance in dBm + + Record + Filter Beacon Type @@ -39,6 +42,7 @@ @string/beacon_major + @string/title_record @string/title_filter Coloring Instances @@ -55,5 +59,6 @@ Location services disabled Bluetooth disabled Enable + RecordingActivity diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e0ae3b7..59308cf 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,10 +1,19 @@ - + + + - - + + - From 85fdb0d6c080f9a2ffcf4e7d3d844228286a49c4 Mon Sep 17 00:00:00 2001 From: Stephan Schultz Date: Wed, 14 Nov 2018 18:16:33 +0100 Subject: [PATCH 04/15] Upgraded to gradle 5.0 --- BLE Indoor Positioning/build.gradle | 35 ++++++++++++------------ app/build.gradle | 16 +++++------ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +-- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/BLE Indoor Positioning/build.gradle b/BLE Indoor Positioning/build.gradle index 2c5c061..8fbe31b 100644 --- a/BLE Indoor Positioning/build.gradle +++ b/BLE Indoor Positioning/build.gradle @@ -8,8 +8,8 @@ buildscript { } plugins { - id 'net.researchgate.release' version '2.4.0' - id 'com.jfrog.bintray' version '1.7.3' + id 'net.researchgate.release' version '2.7.0' + id 'com.jfrog.bintray' version '1.8.4' } apply plugin: 'java-library' @@ -21,6 +21,7 @@ apply plugin: 'jacoco' dependencies { compile 'com.lemmingapex.trilateration:trilateration:1.0.2' compile 'com.google.code.gson:gson:2.8.5' + testCompile 'net.steppschuh.markdowngenerator:markdowngenerator:1.3.0.0' testImplementation 'junit:junit:4.12' } @@ -97,6 +98,21 @@ def pomConfig = { } } +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allJava +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} + publishing { publications { mavenJava(MavenPublication) { @@ -123,21 +139,6 @@ publishing { } } -task sourcesJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allJava -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -} - -artifacts { - archives sourcesJar - archives javadocJar -} - // configure release plugin. See https://github.com/researchgate/gradle-release#configuration release { failOnUnversionedFiles = false diff --git a/app/build.gradle b/app/build.gradle index f792a1c..f240787 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,11 +31,11 @@ android { dependencies { implementation project(':BLE Indoor Positioning') - implementation 'androidx.appcompat:appcompat:1.0.0-beta01' - implementation 'androidx.legacy:legacy-support-v4:1.0.0-beta01' - implementation 'androidx.media:media:1.0.0-beta01' - implementation 'com.google.android.material:material:1.0.0-beta01' - implementation 'androidx.constraintlayout:constraintlayout:1.1.2' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.media:media:1.0.0' + implementation 'com.google.android.material:material:1.1.0-alpha01' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.google.android.material:material:1.0.0' @@ -44,8 +44,8 @@ dependencies { implementation "com.polidea.rxandroidble:rxandroidble:1.4.3" testImplementation 'junit:junit:4.12' - testImplementation 'androidx.test:runner:1.1.0-alpha4' - testImplementation 'androidx.test:rules:1.1.0-alpha4' + testImplementation 'androidx.test:runner:1.1.0' + testImplementation 'androidx.test:rules:1.1.0' testImplementation 'org.robolectric:robolectric:4.0-beta-1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' } diff --git a/build.gradle b/build.gradle index e5a39a8..ee4b3c9 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0-alpha01' + classpath 'com.android.tools.build:gradle:3.4.0-alpha03' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b2f1a62..b9d65f2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 25 13:40:28 CEST 2018 +#Wed Nov 14 17:36:46 CET 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-milestone-1-all.zip From 3399d70c0c191978cf189ba9513f1c69bcb774bb Mon Sep 17 00:00:00 2001 From: Stephan Schultz Date: Thu, 15 Nov 2018 13:03:04 +0100 Subject: [PATCH 05/15] Integrated recording views, implemented persisting and restoring of form values --- .../testutil/benchmark/BeaconInfo.java | 16 ++- .../testutil/benchmark/DeviceInfo.java | 5 + .../testutil/benchmark/RssiMeasurements.java | 8 ++ .../RecordingActivity.java | 106 ++++++++++++++++++ .../main/res/layout/activity_recording.xml | 1 + .../res/layout/content_recording_beacon.xml | 6 +- .../res/layout/content_recording_device.xml | 4 +- app/src/main/res/values/strings.xml | 1 + 8 files changed, 137 insertions(+), 10 deletions(-) diff --git a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/BeaconInfo.java b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/BeaconInfo.java index 7b2e19c..355b578 100644 --- a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/BeaconInfo.java +++ b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/BeaconInfo.java @@ -2,13 +2,19 @@ public class BeaconInfo { + public static final String KEY_BEACON_NAME = "beaconName"; + public static final String KEY_BEACON_MODEL = "beaconModel"; + public static final String KEY_BEACON_MANUFACTURER = "beaconManufacturer"; + public static final String KEY_BEACON_ADVERTISING_FREQUENCY = "beaconAdvertisingFrequency"; + public static final String KEY_BEACON_TRANSMISSION_POWER = "beaconTransmissionPower"; + private String name; private String model; private String manufacturer; - private int advertizingFrequency; + private int advertisingFrequency; private int transmissionPower; @@ -39,12 +45,12 @@ public void setManufacturer(String manufacturer) { this.manufacturer = manufacturer; } - public int getAdvertizingFrequency() { - return advertizingFrequency; + public int getAdvertisingFrequency() { + return advertisingFrequency; } - public void setAdvertizingFrequency(int advertizingFrequency) { - this.advertizingFrequency = advertizingFrequency; + public void setAdvertisingFrequency(int advertisingFrequency) { + this.advertisingFrequency = advertisingFrequency; } public int getTransmissionPower() { diff --git a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/DeviceInfo.java b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/DeviceInfo.java index c40d57f..3fa1a03 100644 --- a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/DeviceInfo.java +++ b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/DeviceInfo.java @@ -2,6 +2,11 @@ public class DeviceInfo { + public static final String KEY_DEVICE_ID = "deviceName"; + public static final String KEY_DEVICE_MODEL = "deviceModel"; + public static final String KEY_DEVICE_MANUFACTURER = "deviceManufacturer"; + public static final String KEY_DEVICE_OS_VERSION = "deviceOsVersion"; + private String name; private String model; diff --git a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/RssiMeasurements.java b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/RssiMeasurements.java index 187aa24..713ce32 100644 --- a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/RssiMeasurements.java +++ b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/RssiMeasurements.java @@ -2,6 +2,14 @@ public class RssiMeasurements { + public static final String KEY_DEVICE_INFO = "deviceInfo"; + public static final String KEY_BEACON_INFO = "beaconInfo"; + public static final String KEY_START_TIMESTAMP = "startTimestamp"; + public static final String KEY_END_TIMESTAMP = "endTimestamp"; + public static final String KEY_NOTES = "notes"; + public static final String KEY_DISTANCE = "distance"; + public static final String KEY_RSSIS = "rssis"; + private DeviceInfo deviceInfo; private BeaconInfo beaconInfo; diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java index 5d5db34..4c1f5d1 100644 --- a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java @@ -1,15 +1,121 @@ package com.nexenio.bleindoorpositioningdemo; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.textfield.TextInputEditText; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; +import android.provider.Settings; +import android.view.View; + +import com.nexenio.bleindoorpositioning.testutil.benchmark.BeaconInfo; +import com.nexenio.bleindoorpositioning.testutil.benchmark.DeviceInfo; +import com.nexenio.bleindoorpositioning.testutil.benchmark.RssiMeasurements; + import androidx.appcompat.app.AppCompatActivity; public class RecordingActivity extends AppCompatActivity { + private MaterialButton recordButton; + + // general + private TextInputEditText distanceEditText; + private TextInputEditText notesEditText; + + // device info + private TextInputEditText deviceIdEditText; + private TextInputEditText deviceModelEditText; + private TextInputEditText deviceManufacturerEditText; + private TextInputEditText deviceOsVersionEditText; + + // beacon info + private TextInputEditText beaconNameEditText; + private TextInputEditText beaconModelEditText; + private TextInputEditText beaconManufacturerEditText; + private TextInputEditText beaconTransmissionPowerEditText; + private TextInputEditText beaconAdvertisingFrequencyEditText; + + private SharedPreferences sharedPreferences; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_recording); + sharedPreferences = getPreferences(Context.MODE_PRIVATE); + + initializeLayout(); + restoreFormValues(); + } + + private void initializeLayout() { + recordButton = findViewById(R.id.recordButton); + + distanceEditText = findViewById(R.id.recordDistanceEditText); + notesEditText = findViewById(R.id.recordNotesEditText); + + deviceIdEditText = findViewById(R.id.recordDeviceIdEditText); + deviceModelEditText = findViewById(R.id.recordDeviceModelEditText); + deviceManufacturerEditText = findViewById(R.id.recordDeviceManufacturerEditText); + deviceOsVersionEditText = findViewById(R.id.recordDeviceOsVersionEditText); + + beaconNameEditText = findViewById(R.id.recordBeaconNameEditText); + beaconModelEditText = findViewById(R.id.recordBeaconModelEditText); + beaconManufacturerEditText = findViewById(R.id.recordBeaconManufacturerEditText); + beaconTransmissionPowerEditText = findViewById(R.id.recordBeaconTransmissionPowerEditText); + beaconAdvertisingFrequencyEditText = findViewById(R.id.recordBeaconAdvertisingFrequencyEditText); + } + + public void onRecordButtonClicked(View view) { + persistFormValues(); + } + + private void persistFormValues() { + SharedPreferences.Editor editor = sharedPreferences.edit(); + + editor.putString(RssiMeasurements.KEY_DISTANCE, distanceEditText.getText().toString()); + editor.putString(RssiMeasurements.KEY_NOTES, notesEditText.getText().toString()); + + editor.putString(DeviceInfo.KEY_DEVICE_ID, deviceIdEditText.getText().toString()); + editor.putString(DeviceInfo.KEY_DEVICE_MODEL, deviceModelEditText.getText().toString()); + editor.putString(DeviceInfo.KEY_DEVICE_MANUFACTURER, deviceManufacturerEditText.getText().toString()); + editor.putString(DeviceInfo.KEY_DEVICE_OS_VERSION, deviceOsVersionEditText.getText().toString()); + + editor.putString(BeaconInfo.KEY_BEACON_NAME, beaconNameEditText.getText().toString()); + editor.putString(BeaconInfo.KEY_BEACON_MODEL, beaconModelEditText.getText().toString()); + editor.putString(BeaconInfo.KEY_BEACON_MANUFACTURER, beaconManufacturerEditText.getText().toString()); + editor.putString(BeaconInfo.KEY_BEACON_TRANSMISSION_POWER, beaconTransmissionPowerEditText.getText().toString()); + editor.putString(BeaconInfo.KEY_BEACON_ADVERTISING_FREQUENCY, beaconAdvertisingFrequencyEditText.getText().toString()); + + editor.apply(); + } + + private void restoreFormValues() { + distanceEditText.setText(sharedPreferences.getString(RssiMeasurements.KEY_DISTANCE, "1")); + notesEditText.setText(sharedPreferences.getString(RssiMeasurements.KEY_NOTES, "")); + + deviceIdEditText.setText(sharedPreferences.getString(DeviceInfo.KEY_DEVICE_ID, getDeviceId(this))); + deviceModelEditText.setText(sharedPreferences.getString(DeviceInfo.KEY_DEVICE_MODEL, Build.MODEL)); + deviceManufacturerEditText.setText(sharedPreferences.getString(DeviceInfo.KEY_DEVICE_MANUFACTURER, Build.MANUFACTURER)); + deviceOsVersionEditText.setText(sharedPreferences.getString(DeviceInfo.KEY_DEVICE_OS_VERSION, getOsVersion())); + + beaconNameEditText.setText(sharedPreferences.getString(BeaconInfo.KEY_BEACON_NAME, "")); + beaconModelEditText.setText(sharedPreferences.getString(BeaconInfo.KEY_BEACON_MODEL, "")); + beaconManufacturerEditText.setText(sharedPreferences.getString(BeaconInfo.KEY_BEACON_MANUFACTURER, "")); + beaconTransmissionPowerEditText.setText(sharedPreferences.getString(BeaconInfo.KEY_BEACON_TRANSMISSION_POWER, "")); + beaconAdvertisingFrequencyEditText.setText(sharedPreferences.getString(BeaconInfo.KEY_BEACON_ADVERTISING_FREQUENCY, "")); + } + + @SuppressLint("HardwareIds") + private static String getDeviceId(Context context) { + return Settings.Secure.getString(context.getContentResolver(), "android_id"); + } + + private static String getOsVersion() { + return "Android " + Build.VERSION.RELEASE + " (SDK " + Build.VERSION.SDK_INT + ")"; } } diff --git a/app/src/main/res/layout/activity_recording.xml b/app/src/main/res/layout/activity_recording.xml index 8309e24..62918ff 100644 --- a/app/src/main/res/layout/activity_recording.xml +++ b/app/src/main/res/layout/activity_recording.xml @@ -24,6 +24,7 @@ diff --git a/app/src/main/res/layout/content_recording_beacon.xml b/app/src/main/res/layout/content_recording_beacon.xml index 0641862..0ce1e8e 100644 --- a/app/src/main/res/layout/content_recording_beacon.xml +++ b/app/src/main/res/layout/content_recording_beacon.xml @@ -12,7 +12,7 @@ style="@style/DefaultTextInputLayout"> @@ -22,7 +22,7 @@ style="@style/DefaultTextInputLayout"> @@ -32,7 +32,7 @@ style="@style/DefaultTextInputLayout"> diff --git a/app/src/main/res/layout/content_recording_device.xml b/app/src/main/res/layout/content_recording_device.xml index 614ba1b..26c9b44 100644 --- a/app/src/main/res/layout/content_recording_device.xml +++ b/app/src/main/res/layout/content_recording_device.xml @@ -7,11 +7,11 @@ style="@style/DefaultHeadingText"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e5a0d1..8192382 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,6 +34,7 @@ Beacon Info Notes An optional comment + ID Name Manufacturer Model From e3aede137e35485fb00a49911d7fa96d842b9200 Mon Sep 17 00:00:00 2001 From: Stephan Schultz Date: Thu, 15 Nov 2018 14:02:08 +0100 Subject: [PATCH 06/15] Implemented recording of RSSIs --- .../testutil/benchmark/DeviceInfo.java | 10 +- .../testutil/benchmark/RssiMeasurements.java | 20 ++- .../RecordingActivity.java | 133 ++++++++++++++++-- .../res/layout/content_recording_device.xml | 4 + 4 files changed, 147 insertions(+), 20 deletions(-) diff --git a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/DeviceInfo.java b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/DeviceInfo.java index 3fa1a03..da14621 100644 --- a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/DeviceInfo.java +++ b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/DeviceInfo.java @@ -7,7 +7,7 @@ public class DeviceInfo { public static final String KEY_DEVICE_MANUFACTURER = "deviceManufacturer"; public static final String KEY_DEVICE_OS_VERSION = "deviceOsVersion"; - private String name; + private String id; private String model; @@ -18,12 +18,12 @@ public class DeviceInfo { public DeviceInfo() { } - public String getName() { - return name; + public String getId() { + return id; } - public void setName(String name) { - this.name = name; + public void setId(String id) { + this.id = id; } public String getModel() { diff --git a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/RssiMeasurements.java b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/RssiMeasurements.java index 713ce32..9a3dc4a 100644 --- a/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/RssiMeasurements.java +++ b/BLE Indoor Positioning/src/main/java/com/nexenio/bleindoorpositioning/testutil/benchmark/RssiMeasurements.java @@ -14,7 +14,9 @@ public class RssiMeasurements { private BeaconInfo beaconInfo; - private long timestamp; + private long startTimestamp; + + private long endTimestamp; private String notes; @@ -41,12 +43,20 @@ public void setBeaconInfo(BeaconInfo beaconInfo) { this.beaconInfo = beaconInfo; } - public long getTimestamp() { - return timestamp; + public long getStartTimestamp() { + return startTimestamp; + } + + public void setStartTimestamp(long startTimestamp) { + this.startTimestamp = startTimestamp; + } + + public long getEndTimestamp() { + return endTimestamp; } - public void setTimestamp(long timestamp) { - this.timestamp = timestamp; + public void setEndTimestamp(long endTimestamp) { + this.endTimestamp = endTimestamp; } public String getNotes() { diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java index 4c1f5d1..585f732 100644 --- a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java @@ -9,36 +9,53 @@ import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import android.util.Log; import android.view.View; +import com.nexenio.bleindoorpositioning.ble.advertising.IBeaconAdvertisingPacket; +import com.nexenio.bleindoorpositioning.ble.beacon.BeaconManager; +import com.nexenio.bleindoorpositioning.ble.beacon.FilteredBeaconUpdateListener; +import com.nexenio.bleindoorpositioning.ble.beacon.IBeacon; +import com.nexenio.bleindoorpositioning.ble.beacon.filter.IBeaconFilter; import com.nexenio.bleindoorpositioning.testutil.benchmark.BeaconInfo; import com.nexenio.bleindoorpositioning.testutil.benchmark.DeviceInfo; import com.nexenio.bleindoorpositioning.testutil.benchmark.RssiMeasurements; +import com.nexenio.bleindoorpositioningdemo.bluetooth.BluetoothClient; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; import androidx.appcompat.app.AppCompatActivity; public class RecordingActivity extends AppCompatActivity { - private MaterialButton recordButton; + private static final String TAG = RecordingActivity.class.getSimpleName(); + + private final static UUID RECORDING_UUID = UUID.fromString("61a0523a-a733-4789-ae8f-4f55fcff64f2"); + + private RssiMeasurements rssiMeasurements = new RssiMeasurements(); + private List recordedRssiValues; + private FilteredBeaconUpdateListener> recordingBeaconUpdateListener; + + private SharedPreferences sharedPreferences; - // general private TextInputEditText distanceEditText; private TextInputEditText notesEditText; - // device info private TextInputEditText deviceIdEditText; private TextInputEditText deviceModelEditText; private TextInputEditText deviceManufacturerEditText; private TextInputEditText deviceOsVersionEditText; - // beacon info private TextInputEditText beaconNameEditText; private TextInputEditText beaconModelEditText; private TextInputEditText beaconManufacturerEditText; private TextInputEditText beaconTransmissionPowerEditText; private TextInputEditText beaconAdvertisingFrequencyEditText; - private SharedPreferences sharedPreferences; + private MaterialButton recordButton; @Override protected void onCreate(Bundle savedInstanceState) { @@ -49,6 +66,7 @@ protected void onCreate(Bundle savedInstanceState) { initializeLayout(); restoreFormValues(); + initializeBluetoothScanning(); } private void initializeLayout() { @@ -69,20 +87,104 @@ private void initializeLayout() { beaconAdvertisingFrequencyEditText = findViewById(R.id.recordBeaconAdvertisingFrequencyEditText); } + private void initializeBluetoothScanning() { + Log.d(TAG, "Initializing Bluetooth scanning"); + + BluetoothClient.initialize(this); + IBeaconFilter> recordingBeaconFilter = new IBeaconFilter<>(RECORDING_UUID); + recordingBeaconUpdateListener = new FilteredBeaconUpdateListener>(recordingBeaconFilter) { + @Override + public void onMatchingBeaconUpdated(IBeacon beacon) { + onRecordingBeaconUpdated(beacon); + } + }; + } + + @Override + protected void onStop() { + stopRecording(); + super.onStop(); + } + public void onRecordButtonClicked(View view) { persistFormValues(); + startRecording(); + } + + private RssiMeasurements createRssiMeasurements() { + RssiMeasurements rssiMeasurements = new RssiMeasurements(); + rssiMeasurements.setDeviceInfo(createDeviceInfo()); + rssiMeasurements.setBeaconInfo(createBeaconInfo()); + rssiMeasurements.setDistance(Float.parseFloat(distanceEditText.getText().toString())); + rssiMeasurements.setNotes(notesEditText.getText().toString()); + rssiMeasurements.setStartTimestamp(System.currentTimeMillis()); + return rssiMeasurements; + } + + private DeviceInfo createDeviceInfo() { + DeviceInfo deviceInfo = new DeviceInfo(); + deviceInfo.setId(deviceIdEditText.getText().toString()); + deviceInfo.setModel(deviceModelEditText.getText().toString()); + deviceInfo.setManufacturer(deviceManufacturerEditText.getText().toString()); + deviceInfo.setOsVersion(deviceOsVersionEditText.getText().toString()); + return deviceInfo; + } + + private BeaconInfo createBeaconInfo() { + BeaconInfo beaconInfo = new BeaconInfo(); + beaconInfo.setName(beaconNameEditText.getText().toString()); + beaconInfo.setModel(beaconModelEditText.getText().toString()); + beaconInfo.setManufacturer(beaconManufacturerEditText.getText().toString()); + beaconInfo.setTransmissionPower(Integer.valueOf(beaconTransmissionPowerEditText.getText().toString())); + beaconInfo.setAdvertisingFrequency(Integer.valueOf(beaconAdvertisingFrequencyEditText.getText().toString())); + return beaconInfo; + } + + private void startRecording() { + Log.d(TAG, "Starting recording"); + + rssiMeasurements = createRssiMeasurements(); + recordedRssiValues = new ArrayList<>(); + + BluetoothClient.startScanning(); + BeaconManager.registerBeaconUpdateListener(recordingBeaconUpdateListener); + } + + private void stopRecording() { + Log.d(TAG, "Stopping recording"); + + BluetoothClient.stopScanning(); + BeaconManager.unregisterBeaconUpdateListener(recordingBeaconUpdateListener); + + rssiMeasurements.setEndTimestamp(System.currentTimeMillis()); + rssiMeasurements.setRssis(convertIntArray(recordedRssiValues)); + recordedRssiValues.clear(); + + Log.i(TAG, "RSSI measurements:\n" + rssiMeasurements); + + // TODO: persist JSON file + + // TODO: start share intent for persisted JSON file + } + + private void onRecordingBeaconUpdated(IBeacon beacon) { + IBeaconAdvertisingPacket latestAdvertisingPacket = beacon.getLatestAdvertisingPacket(); + recordedRssiValues.add(latestAdvertisingPacket.getRssi()); } private void persistFormValues() { + Log.d(TAG, "Persisting form values"); + SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(RssiMeasurements.KEY_DISTANCE, distanceEditText.getText().toString()); editor.putString(RssiMeasurements.KEY_NOTES, notesEditText.getText().toString()); - editor.putString(DeviceInfo.KEY_DEVICE_ID, deviceIdEditText.getText().toString()); - editor.putString(DeviceInfo.KEY_DEVICE_MODEL, deviceModelEditText.getText().toString()); - editor.putString(DeviceInfo.KEY_DEVICE_MANUFACTURER, deviceManufacturerEditText.getText().toString()); - editor.putString(DeviceInfo.KEY_DEVICE_OS_VERSION, deviceOsVersionEditText.getText().toString()); + // don't persist dynamic fields + //editor.putString(DeviceInfo.KEY_DEVICE_ID, deviceIdEditText.getText().toString()); + //editor.putString(DeviceInfo.KEY_DEVICE_MODEL, deviceModelEditText.getText().toString()); + //editor.putString(DeviceInfo.KEY_DEVICE_MANUFACTURER, deviceManufacturerEditText.getText().toString()); + //editor.putString(DeviceInfo.KEY_DEVICE_OS_VERSION, deviceOsVersionEditText.getText().toString()); editor.putString(BeaconInfo.KEY_BEACON_NAME, beaconNameEditText.getText().toString()); editor.putString(BeaconInfo.KEY_BEACON_MODEL, beaconModelEditText.getText().toString()); @@ -94,7 +196,9 @@ private void persistFormValues() { } private void restoreFormValues() { - distanceEditText.setText(sharedPreferences.getString(RssiMeasurements.KEY_DISTANCE, "1")); + Log.d(TAG, "Restoring form values"); + + distanceEditText.setText(sharedPreferences.getString(RssiMeasurements.KEY_DISTANCE, "")); notesEditText.setText(sharedPreferences.getString(RssiMeasurements.KEY_NOTES, "")); deviceIdEditText.setText(sharedPreferences.getString(DeviceInfo.KEY_DEVICE_ID, getDeviceId(this))); @@ -109,6 +213,15 @@ private void restoreFormValues() { beaconAdvertisingFrequencyEditText.setText(sharedPreferences.getString(BeaconInfo.KEY_BEACON_ADVERTISING_FREQUENCY, "")); } + private static int[] convertIntArray(List integerList) { + int[] intArray = new int[integerList.size()]; + Iterator iterator = integerList.iterator(); + for (int i = 0; i < intArray.length; i++) { + intArray[i] = iterator.next(); + } + return intArray; + } + @SuppressLint("HardwareIds") private static String getDeviceId(Context context) { return Settings.Secure.getString(context.getContentResolver(), "android_id"); diff --git a/app/src/main/res/layout/content_recording_device.xml b/app/src/main/res/layout/content_recording_device.xml index 26c9b44..127aa12 100644 --- a/app/src/main/res/layout/content_recording_device.xml +++ b/app/src/main/res/layout/content_recording_device.xml @@ -12,6 +12,7 @@ @@ -22,6 +23,7 @@ @@ -32,6 +34,7 @@ @@ -42,6 +45,7 @@ From d0ffe98ab170a8055bb1897bc3b3e0ec4ed57680 Mon Sep 17 00:00:00 2001 From: Stephan Schultz Date: Wed, 6 Feb 2019 16:44:37 +0100 Subject: [PATCH 07/15] Added recording logic --- .../RecordingActivity.java | 14 +++++++++++++- app/src/main/res/layout/activity_recording.xml | 2 +- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java index 585f732..0af7d7b 100644 --- a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java @@ -11,6 +11,7 @@ import android.provider.Settings; import android.util.Log; import android.view.View; +import android.widget.Toast; import com.nexenio.bleindoorpositioning.ble.advertising.IBeaconAdvertisingPacket; import com.nexenio.bleindoorpositioning.ble.beacon.BeaconManager; @@ -57,6 +58,8 @@ public class RecordingActivity extends AppCompatActivity { private MaterialButton recordButton; + private boolean isRecording; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -108,7 +111,11 @@ protected void onStop() { public void onRecordButtonClicked(View view) { persistFormValues(); - startRecording(); + if (isRecording) { + stopRecording(); + } else { + startRecording(); + } } private RssiMeasurements createRssiMeasurements() { @@ -148,10 +155,15 @@ private void startRecording() { BluetoothClient.startScanning(); BeaconManager.registerBeaconUpdateListener(recordingBeaconUpdateListener); + + isRecording = true; + recordButton.setText(getString(R.string.action_stop_recording)); } private void stopRecording() { Log.d(TAG, "Stopping recording"); + isRecording = false; + recordButton.setText(getString(R.string.action_start_recording)); BluetoothClient.stopScanning(); BeaconManager.unregisterBeaconUpdateListener(recordingBeaconUpdateListener); diff --git a/app/src/main/res/layout/activity_recording.xml b/app/src/main/res/layout/activity_recording.xml index 62918ff..65b9699 100644 --- a/app/src/main/res/layout/activity_recording.xml +++ b/app/src/main/res/layout/activity_recording.xml @@ -27,7 +27,7 @@ android:onClick="onRecordButtonClicked" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/title_record"/> + android:text="@string/action_start_recording"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8192382..48a7642 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,8 @@ Record + Start Recording + Stop Recording General Info Device Info Beacon Info From 7eb69ad5a923e961379a79a258aba903abfc89ec Mon Sep 17 00:00:00 2001 From: Stephan Schultz Date: Sat, 20 Apr 2019 14:10:30 +0200 Subject: [PATCH 08/15] Updated gradle and build tools --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 0 3 files changed, 3 insertions(+), 3 deletions(-) mode change 100755 => 100644 gradlew diff --git a/build.gradle b/build.gradle index e5a39a8..cb23dab 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0-alpha01' + classpath 'com.android.tools.build:gradle:3.4.0' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b2f1a62..164161e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 25 13:40:28 CEST 2018 +#Sat Apr 20 13:48:50 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 From bf746967936ad444a81667a6f994b23a4c8cad29 Mon Sep 17 00:00:00 2001 From: Marvin Mirtschin Date: Tue, 10 Sep 2019 15:44:44 +0200 Subject: [PATCH 09/15] implements file persisting and sharing; resolves ui errors --- app/src/main/AndroidManifest.xml | 11 ++ .../ExternalStorageUtils.java | 178 +++++++++++++++++ .../RecordingActivity.java | 185 +++++++++++++++++- .../res/layout/content_recording_general.xml | 2 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/file_paths.xml | 6 + build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 8 files changed, 378 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/nexenio/bleindoorpositioningdemo/ExternalStorageUtils.java create mode 100644 app/src/main/res/xml/file_paths.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 413da9e..a12268f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/ExternalStorageUtils.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/ExternalStorageUtils.java new file mode 100644 index 0000000..5727ea2 --- /dev/null +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/ExternalStorageUtils.java @@ -0,0 +1,178 @@ +package com.nexenio.bleindoorpositioningdemo; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Environment; +import android.os.StatFs; +import android.widget.Toast; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; + +/** + * Created by steppschuh on 04/11/2016. + */ + +public abstract class ExternalStorageUtils { + + /** + * Checks if external storage is available for read and write + */ + public static boolean isExternalStorageWritable() { + String state; + try { + state = Environment.getExternalStorageState(); + } catch (ArrayIndexOutOfBoundsException e) { + return false; + } + return Environment.MEDIA_MOUNTED.equals(state); + } + + /** + * Checks if external storage is available to at least read + */ + public static boolean isExternalStorageReadable() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state) || + Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { + return true; + } + return false; + } + + public static File getDocumentsDirectory(String subFolder) { + File file; + if (subFolder != null) { + file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), subFolder); + } else { + file = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); + } + file.mkdirs(); + return file; + } + + public static File getCacheDirectory(Context context, String subFolder) { + File file; + if (subFolder != null) { + file = new File(context.getExternalCacheDir(), subFolder); + } else { + file = context.getExternalCacheDir(); + } + file.mkdirs(); + return file; + } + + public static long getFileSize(@NonNull File file) { + long size = file.length(); + if (file.isDirectory()) { + for (File f : file.listFiles()) { + if (f.isDirectory()) { + size += getFileSize(f); + } + size += f.length(); + } + } + return size; + } + + /** + * Emits all {@link File}s (that are not directories) in the specified directory. Also includes + * files in sub directories. + */ + public static List getFilesInDirectory(@NonNull File directory) { + List files = new ArrayList<>(); + for (File file : directory.listFiles()) { + if (file.isDirectory()) { + files.addAll(getFilesInDirectory(file)); + } else { + files.add(file); + } + } + return files; + } + + /** + * Returns a list of {@link File}s which have the .json extension in the specified directory. + * Also includes files in subfolders. + */ + public static List getJsonFilesInDirectory(File directory) { + ArrayList jsonFiles = new ArrayList<>(); + File[] files = directory.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + jsonFiles.addAll(getJsonFilesInDirectory(file)); + } else { + if (file.getName().endsWith(".json")) { + jsonFiles.add(file); + } + } + } + return jsonFiles; + } + + /** + * Removes the given files from the external storage. + */ + public static boolean removeFiles(List files) { + boolean allFilesRemoved = true; + for (File file : files) { + allFilesRemoved &= file.delete(); + } + return allFilesRemoved; + } + + /** + * Invokes an implicit share intent for the specified file. + */ + public static void shareFile(File file, Context context) { + if (file == null || !file.canRead()) { + return; + } + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + Uri contentUri = FileProvider.getUriForFile(context, "com.nexenio.bleindoorpositioning.fileprovider", file); + + shareIntent.setTypeAndNormalize(context.getContentResolver().getType(contentUri)); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, file.getName()); + shareIntent.putExtra(Intent.EXTRA_TEXT, file.getName()); + shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri); + + Intent chooserIntent = Intent.createChooser(shareIntent, "Share file"); + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (shareIntent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(chooserIntent); + } else { + Toast.makeText(context, "Unable to share file", Toast.LENGTH_LONG).show(); + } + } + + /** + * Writes the contents of a string to the specified file. + * + * @param data the string that the file should contain + * @param file the file that the string should be written to + * @param append if false, file contents will be overwritten + */ + public static void writeStringToFile(String data, File file, boolean append) throws IOException { + try (FileWriter fw = new FileWriter(file, append)) { + fw.write(data); + } + } + + /** + * Returns the amount of bytes that are available in the specified directory. + */ + public static long getAvailableMemory(@NonNull File directory) { + StatFs stats = new StatFs(directory.getAbsolutePath()); + return stats.getAvailableBlocksLong() * stats.getBlockSizeLong(); + } + +} diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java index 0af7d7b..29ccefd 100644 --- a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java @@ -1,14 +1,21 @@ package com.nexenio.bleindoorpositioningdemo; import com.google.android.material.button.MaterialButton; +import com.google.android.material.snackbar.BaseTransientBottomBar; +import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputEditText; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import android.text.InputFilter; import android.util.Log; import android.view.View; import android.widget.Toast; @@ -23,20 +30,32 @@ import com.nexenio.bleindoorpositioning.testutil.benchmark.RssiMeasurements; import com.nexenio.bleindoorpositioningdemo.bluetooth.BluetoothClient; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.UUID; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; public class RecordingActivity extends AppCompatActivity { private static final String TAG = RecordingActivity.class.getSimpleName(); + private static final String RECORDING_DIRECTORY_NAME = "Rssi_Recordings"; + public static final int REQUEST_CODE_STORAGE_PERMISSIONS = 1; private final static UUID RECORDING_UUID = UUID.fromString("61a0523a-a733-4789-ae8f-4f55fcff64f2"); private RssiMeasurements rssiMeasurements = new RssiMeasurements(); + + @Nullable private List recordedRssiValues; private FilteredBeaconUpdateListener> recordingBeaconUpdateListener; @@ -60,6 +79,10 @@ public class RecordingActivity extends AppCompatActivity { private boolean isRecording; + @Nullable + protected Snackbar errorSnackBar; + protected CoordinatorLayout coordinatorLayout; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -70,11 +93,30 @@ protected void onCreate(Bundle savedInstanceState) { initializeLayout(); restoreFormValues(); initializeBluetoothScanning(); + + if (!hasStoragePermission()) { + requestStoragePermission(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_CODE_STORAGE_PERMISSIONS: { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Storage permission granted"); + } else { + showStoragePermissionMissingError(); + } + break; + } + } } private void initializeLayout() { - recordButton = findViewById(R.id.recordButton); + coordinatorLayout = getWindow().getDecorView().findViewById(R.id.coordinatorLayout); + recordButton = findViewById(R.id.recordButton); distanceEditText = findViewById(R.id.recordDistanceEditText); notesEditText = findViewById(R.id.recordNotesEditText); @@ -88,6 +130,14 @@ private void initializeLayout() { beaconManufacturerEditText = findViewById(R.id.recordBeaconManufacturerEditText); beaconTransmissionPowerEditText = findViewById(R.id.recordBeaconTransmissionPowerEditText); beaconAdvertisingFrequencyEditText = findViewById(R.id.recordBeaconAdvertisingFrequencyEditText); + + // Limit numbers to avoid overflow + InputFilter[] FilterArray = new InputFilter[1]; + FilterArray[0] = new InputFilter.LengthFilter(6); + + distanceEditText.setFilters(FilterArray); + beaconTransmissionPowerEditText.setFilters(FilterArray); + beaconAdvertisingFrequencyEditText.setFilters(FilterArray); } private void initializeBluetoothScanning() { @@ -148,9 +198,16 @@ private BeaconInfo createBeaconInfo() { } private void startRecording() { - Log.d(TAG, "Starting recording"); + try { + rssiMeasurements = createRssiMeasurements(); + } catch (IllegalArgumentException | NullPointerException e) { + if (!showInvalidFields()) { + throw e; + } + return; + } - rssiMeasurements = createRssiMeasurements(); + Log.d(TAG, "Starting recording"); recordedRssiValues = new ArrayList<>(); BluetoothClient.startScanning(); @@ -161,6 +218,9 @@ private void startRecording() { } private void stopRecording() { + if (!isRecording) { + return; + } Log.d(TAG, "Stopping recording"); isRecording = false; recordButton.setText(getString(R.string.action_start_recording)); @@ -170,13 +230,119 @@ private void stopRecording() { rssiMeasurements.setEndTimestamp(System.currentTimeMillis()); rssiMeasurements.setRssis(convertIntArray(recordedRssiValues)); - recordedRssiValues.clear(); + + if (recordedRssiValues != null) { + recordedRssiValues.clear(); + } Log.i(TAG, "RSSI measurements:\n" + rssiMeasurements); + exportMeasurements(); + } - // TODO: persist JSON file + /** + * Indicate text edit fields with missing text. + * + * @return False if no invalid field was found, else true + */ + private boolean showInvalidFields() { + Toast.makeText(this, "Not all information were provided", Toast.LENGTH_LONG).show(); + List textInputEditTexts = getTextInputEditTexts(); + boolean invalidTextField = false; + for (TextInputEditText textInputEditText : textInputEditTexts) { + if (textInputEditText.getText() != null && textInputEditText.getText().toString().equals("")) { + String errorString = "This field cannot be empty"; + textInputEditText.setError(errorString); + invalidTextField = true; + } + } + return invalidTextField; + } - // TODO: start share intent for persisted JSON file + private List getTextInputEditTexts() { + return Arrays.asList(distanceEditText, + notesEditText, + deviceIdEditText, + deviceModelEditText, + deviceManufacturerEditText, + deviceOsVersionEditText, + beaconNameEditText, + beaconModelEditText, + beaconManufacturerEditText, + beaconTransmissionPowerEditText, + beaconAdvertisingFrequencyEditText); + } + + private void exportMeasurements() { + String jsonString = createJsonString(rssiMeasurements); + String fileName = "rssiMeasurements_" + rssiMeasurements.getStartTimestamp() + "_" + rssiMeasurements.getEndTimestamp() + ".json"; + File file = persistJsonFile(fileName, jsonString); + + ExternalStorageUtils.shareFile(file, this); + } + + private static String createJsonString(RssiMeasurements rssiMeasurements) { + GsonBuilder gsonBuilder = new GsonBuilder() + .setPrettyPrinting(); + + Gson gson = gsonBuilder.create(); + return gson.toJson(rssiMeasurements); + } + + private File persistJsonFile(String fileName, String jsonString) { + File documentsDirectory = ExternalStorageUtils.getDocumentsDirectory(RECORDING_DIRECTORY_NAME); + File file = new File(documentsDirectory, fileName); + + System.out.println(jsonString); + + try { + if (!documentsDirectory.exists()) { + System.out.println("Creating directory"); + boolean mkdirs = documentsDirectory.mkdirs(); + System.out.println("Directory created: " + mkdirs); + } + System.out.println("Writing file"); + ExternalStorageUtils.writeStringToFile(jsonString, file, false); + } catch (IOException e) { + e.printStackTrace(); + } + return file; + } + + private boolean hasStoragePermission() { + int permissionResult = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + return permissionResult == PackageManager.PERMISSION_GRANTED; + } + + @SuppressLint("CheckResult") + private void requestStoragePermission() { + Log.d(TAG, "Requesting storage permission"); + ActivityCompat.requestPermissions(this, new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE + }, REQUEST_CODE_STORAGE_PERMISSIONS); + } + + private void showStoragePermissionMissingError() { + // find a container for the snackbar + if (errorSnackBar != null) { + errorSnackBar.dismiss(); + errorSnackBar = null; + } + + View container = coordinatorLayout; + if (container == null) { + container = getWindow().getDecorView(); + } + // create the snackbar + errorSnackBar = Snackbar.make(container, "Permission not granted", Snackbar.LENGTH_LONG); + + errorSnackBar.setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE) + .setActionTextColor(ContextCompat.getColor(this, R.color.primary)) + .setAction(getString(R.string.record_retry), new View.OnClickListener() { + @Override + public void onClick(View view) { + requestStoragePermission(); + } + }).show(); } private void onRecordingBeaconUpdated(IBeacon beacon) { @@ -225,7 +391,12 @@ private void restoreFormValues() { beaconAdvertisingFrequencyEditText.setText(sharedPreferences.getString(BeaconInfo.KEY_BEACON_ADVERTISING_FREQUENCY, "")); } - private static int[] convertIntArray(List integerList) { + private static int[] convertIntArray(@Nullable List integerList) { + if (integerList == null) { + Log.w(TAG, "Can't convert null to int array."); + return new int[0]; + } + int[] intArray = new int[integerList.size()]; Iterator iterator = integerList.iterator(); for (int i = 0; i < intArray.length; i++) { diff --git a/app/src/main/res/layout/content_recording_general.xml b/app/src/main/res/layout/content_recording_general.xml index 4250127..c7acc3f 100644 --- a/app/src/main/res/layout/content_recording_general.xml +++ b/app/src/main/res/layout/content_recording_general.xml @@ -14,7 +14,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48a7642..13f5f35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,6 +47,8 @@ Advertising frequency in Hz (e.g. 10) Distance The measured distance in meters (e.g. 3) + Export json file + Retry Filter diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..9fb12db --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index cb23dab..54ecd9a 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:3.4.1' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b9d65f2..ff6b198 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-milestone-1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip From 01fb5fc08869b6adc2f52378cc3dc71cc6a677f4 Mon Sep 17 00:00:00 2001 From: Marvin Mirtschin Date: Tue, 10 Sep 2019 15:56:18 +0200 Subject: [PATCH 10/15] makes gradlew executable --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From c9f3a852e32909a422f359c5d5ec540c0e84ed21 Mon Sep 17 00:00:00 2001 From: Marvin Mirtschin Date: Tue, 10 Sep 2019 16:20:27 +0200 Subject: [PATCH 11/15] increases robolectrics version number to fix robolectric issue #3955 which also occurs on mac --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f240787..0fbfd8e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,6 +46,6 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation 'androidx.test:runner:1.1.0' testImplementation 'androidx.test:rules:1.1.0' - testImplementation 'org.robolectric:robolectric:4.0-beta-1' + testImplementation 'org.robolectric:robolectric:4.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' } From ad46b750043e69b03da2163387639930807336ca Mon Sep 17 00:00:00 2001 From: Marvin Mirtschin Date: Mon, 16 Sep 2019 13:27:03 +0200 Subject: [PATCH 12/15] adds dialog for file removing; adds uuid text edit for beacon configurations --- .../RecordingActivity.java | 67 +++++++++++++++---- .../main/res/layout/activity_recording.xml | 61 ++++++++++------- .../res/layout/content_recording_general.xml | 12 ++++ app/src/main/res/values/strings.xml | 3 + 4 files changed, 108 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java index 29ccefd..e68fa42 100644 --- a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java @@ -9,12 +9,16 @@ import android.Manifest; import android.annotation.SuppressLint; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; +import android.content.DialogInterface; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import android.text.Editable; import android.text.InputFilter; import android.util.Log; import android.view.View; @@ -40,6 +44,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; @@ -75,6 +80,8 @@ public class RecordingActivity extends AppCompatActivity { private TextInputEditText beaconTransmissionPowerEditText; private TextInputEditText beaconAdvertisingFrequencyEditText; + private TextInputEditText recordingUuidCopyEditText; + private MaterialButton recordButton; private boolean isRecording; @@ -131,6 +138,9 @@ private void initializeLayout() { beaconTransmissionPowerEditText = findViewById(R.id.recordBeaconTransmissionPowerEditText); beaconAdvertisingFrequencyEditText = findViewById(R.id.recordBeaconAdvertisingFrequencyEditText); + recordingUuidCopyEditText = findViewById(R.id.recordingUuidCopyEditText); + setupUuidEditText(); + // Limit numbers to avoid overflow InputFilter[] FilterArray = new InputFilter[1]; FilterArray[0] = new InputFilter.LengthFilter(6); @@ -168,12 +178,33 @@ public void onRecordButtonClicked(View view) { } } + public void onClearRecordingsButtonClicked(View view) { + new AlertDialog.Builder(this) + .setTitle("Remove Recordings") + .setMessage("Do you really want to remove all recordings?") + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + File documentsDirectory = ExternalStorageUtils.getDocumentsDirectory(RECORDING_DIRECTORY_NAME); + List jsonFilesInDirectory = ExternalStorageUtils.getJsonFilesInDirectory(documentsDirectory); + ExternalStorageUtils.removeFiles(jsonFilesInDirectory); + Toast.makeText(RecordingActivity.this, "Removed " + jsonFilesInDirectory.size() + " file(s)", Toast.LENGTH_SHORT).show(); + } + }) + .setNegativeButton(android.R.string.no, null).show(); + } + private RssiMeasurements createRssiMeasurements() { RssiMeasurements rssiMeasurements = new RssiMeasurements(); rssiMeasurements.setDeviceInfo(createDeviceInfo()); rssiMeasurements.setBeaconInfo(createBeaconInfo()); rssiMeasurements.setDistance(Float.parseFloat(distanceEditText.getText().toString())); - rssiMeasurements.setNotes(notesEditText.getText().toString()); + + Editable optionalNotes = notesEditText.getText(); + if (optionalNotes != null) { + rssiMeasurements.setNotes(optionalNotes.toString()); + } + rssiMeasurements.setStartTimestamp(System.currentTimeMillis()); return rssiMeasurements; } @@ -207,6 +238,11 @@ private void startRecording() { return; } + if (!hasStoragePermission()) { + requestStoragePermission(); + return; + } + Log.d(TAG, "Starting recording"); recordedRssiValues = new ArrayList<>(); @@ -239,6 +275,21 @@ private void stopRecording() { exportMeasurements(); } + private void setupUuidEditText() { + recordingUuidCopyEditText.setText(RECORDING_UUID.toString()); + recordingUuidCopyEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Recording Uuid", ((TextInputEditText) v).getText()); + clipboard.setPrimaryClip(clip); + Toast.makeText(RecordingActivity.this, "Copied to Clipboard", Toast.LENGTH_SHORT).show(); + } + } + }); + } + /** * Indicate text edit fields with missing text. * @@ -246,7 +297,7 @@ private void stopRecording() { */ private boolean showInvalidFields() { Toast.makeText(this, "Not all information were provided", Toast.LENGTH_LONG).show(); - List textInputEditTexts = getTextInputEditTexts(); + List textInputEditTexts = getNecessaryTextInputEditTexts(); boolean invalidTextField = false; for (TextInputEditText textInputEditText : textInputEditTexts) { if (textInputEditText.getText() != null && textInputEditText.getText().toString().equals("")) { @@ -258,9 +309,8 @@ private boolean showInvalidFields() { return invalidTextField; } - private List getTextInputEditTexts() { + private List getNecessaryTextInputEditTexts() { return Arrays.asList(distanceEditText, - notesEditText, deviceIdEditText, deviceModelEditText, deviceManufacturerEditText, @@ -291,16 +341,10 @@ private static String createJsonString(RssiMeasurements rssiMeasurements) { private File persistJsonFile(String fileName, String jsonString) { File documentsDirectory = ExternalStorageUtils.getDocumentsDirectory(RECORDING_DIRECTORY_NAME); File file = new File(documentsDirectory, fileName); - - System.out.println(jsonString); - try { if (!documentsDirectory.exists()) { - System.out.println("Creating directory"); - boolean mkdirs = documentsDirectory.mkdirs(); - System.out.println("Directory created: " + mkdirs); + documentsDirectory.mkdirs(); } - System.out.println("Writing file"); ExternalStorageUtils.writeStringToFile(jsonString, file, false); } catch (IOException e) { e.printStackTrace(); @@ -336,7 +380,6 @@ private void showStoragePermissionMissingError() { errorSnackBar = Snackbar.make(container, "Permission not granted", Snackbar.LENGTH_LONG); errorSnackBar.setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE) - .setActionTextColor(ContextCompat.getColor(this, R.color.primary)) .setAction(getString(R.string.record_retry), new View.OnClickListener() { @Override public void onClick(View view) { diff --git a/app/src/main/res/layout/activity_recording.xml b/app/src/main/res/layout/activity_recording.xml index 65b9699..8533163 100644 --- a/app/src/main/res/layout/activity_recording.xml +++ b/app/src/main/res/layout/activity_recording.xml @@ -1,34 +1,49 @@ - - - + + + android:layout_height="match_parent"> + + - + - + - + - + + + - + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_recording_general.xml b/app/src/main/res/layout/content_recording_general.xml index c7acc3f..09c1006 100644 --- a/app/src/main/res/layout/content_recording_general.xml +++ b/app/src/main/res/layout/content_recording_general.xml @@ -30,4 +30,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13f5f35..e0572b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Record Start Recording Stop Recording + Delete all recordings General Info Device Info Beacon Info @@ -49,6 +50,8 @@ The measured distance in meters (e.g. 3) Export json file Retry + Recording Uuid + Copy recording uuid to clipboard Filter From 7c5bb1588e788b523eae0bcc68134f66d6734834 Mon Sep 17 00:00:00 2001 From: Marvin Mirtschin Date: Mon, 16 Sep 2019 14:59:59 +0200 Subject: [PATCH 13/15] refactors invalid text edit highlighting logic --- .../RecordingActivity.java | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java index e68fa42..e6a2f1f 100644 --- a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java @@ -229,15 +229,14 @@ private BeaconInfo createBeaconInfo() { } private void startRecording() { - try { - rssiMeasurements = createRssiMeasurements(); - } catch (IllegalArgumentException | NullPointerException e) { - if (!showInvalidFields()) { - throw e; - } + List invalidTextEdits = getInvalidTextEdits(); + if (!invalidTextEdits.isEmpty()) { + showInvalidFields(invalidTextEdits); return; } + rssiMeasurements = createRssiMeasurements(); + if (!hasStoragePermission()) { requestStoragePermission(); return; @@ -290,23 +289,26 @@ public void onFocusChange(View v, boolean hasFocus) { }); } + private List getInvalidTextEdits() { + List textInputEditTexts = getNecessaryTextInputEditTexts(); + List invalidInputEdits = new ArrayList<>(); + for (TextInputEditText textInputEditText : textInputEditTexts) { + if (textInputEditText.getText() != null && textInputEditText.getText().toString().equals("")) { + invalidInputEdits.add(textInputEditText); + } + } + return invalidInputEdits; + } + /** * Indicate text edit fields with missing text. - * - * @return False if no invalid field was found, else true */ - private boolean showInvalidFields() { + private void showInvalidFields(List textInputEditTexts) { Toast.makeText(this, "Not all information were provided", Toast.LENGTH_LONG).show(); - List textInputEditTexts = getNecessaryTextInputEditTexts(); - boolean invalidTextField = false; + String errorString = "This field cannot be empty"; for (TextInputEditText textInputEditText : textInputEditTexts) { - if (textInputEditText.getText() != null && textInputEditText.getText().toString().equals("")) { - String errorString = "This field cannot be empty"; - textInputEditText.setError(errorString); - invalidTextField = true; - } + textInputEditText.setError(errorString); } - return invalidTextField; } private List getNecessaryTextInputEditTexts() { From a0d24ca77a9cf696dad886e0096c759494ea4c31 Mon Sep 17 00:00:00 2001 From: Steppschuh Date: Tue, 31 Mar 2020 14:40:24 +0200 Subject: [PATCH 14/15] Updated build tools --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 54ecd9a..bb7ce49 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.5.3' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff6b198..c270a91 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Nov 14 17:36:46 CET 2018 +#Tue Mar 31 14:39:23 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip From fc0ec7128d9124cec97a05dd971e70dba0677740 Mon Sep 17 00:00:00 2001 From: Marvin Mirtschin Date: Fri, 1 May 2020 13:07:37 +0200 Subject: [PATCH 15/15] adding zip upload --- .../ExternalStorageUtils.java | 56 ++++++++++++++++++- .../RecordingActivity.java | 32 ++++++++++- .../main/res/layout/activity_recording.xml | 7 +++ app/src/main/res/values/strings.xml | 1 + build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 6 files changed, 93 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/ExternalStorageUtils.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/ExternalStorageUtils.java index 5727ea2..3fb9a42 100644 --- a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/ExternalStorageUtils.java +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/ExternalStorageUtils.java @@ -7,11 +7,17 @@ import android.os.StatFs; import android.widget.Toast; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import androidx.annotation.NonNull; import androidx.core.content.FileProvider; @@ -22,6 +28,8 @@ public abstract class ExternalStorageUtils { + public static final int BUFFER_SIZE = 2048; + /** * Checks if external storage is available for read and write */ @@ -98,18 +106,51 @@ public static List getFilesInDirectory(@NonNull File directory) { return files; } + public static void zip(List files, String zipFile) throws IOException { + BufferedInputStream origin; + try (ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile)))) { + byte[] data = new byte[BUFFER_SIZE]; + + for (File file : files) { + FileInputStream fi = new FileInputStream(file); + origin = new BufferedInputStream(fi, BUFFER_SIZE); + + String filePath = file.getAbsolutePath(); + + try { + ZipEntry entry = new ZipEntry(filePath.substring(filePath.lastIndexOf("/") + 1)); + out.putNextEntry(entry); + int count; + while ((count = origin.read(data, 0, BUFFER_SIZE)) != -1) { + out.write(data, 0, count); + } + } finally { + origin.close(); + } + } + } + } + /** * Returns a list of {@link File}s which have the .json extension in the specified directory. * Also includes files in subfolders. */ public static List getJsonFilesInDirectory(File directory) { + return getFilesInDirectoryForExtension(directory, ".json"); + } + + /** + * Returns a list of {@link File}s which have the specified extension in the specified + * directory. Also includes files in subfolders. + */ + public static List getFilesInDirectoryForExtension(File directory, String extension) { ArrayList jsonFiles = new ArrayList<>(); File[] files = directory.listFiles(); for (File file : files) { if (file.isDirectory()) { jsonFiles.addAll(getJsonFilesInDirectory(file)); } else { - if (file.getName().endsWith(".json")) { + if (file.getName().endsWith(extension)) { jsonFiles.add(file); } } @@ -132,6 +173,13 @@ public static boolean removeFiles(List files) { * Invokes an implicit share intent for the specified file. */ public static void shareFile(File file, Context context) { + shareFile(file, context, file.getName(), file.getName()); + } + + /** + * Invokes an implicit share intent for the specified file. + */ + public static void shareFile(File file, Context context, String subject, String text) { if (file == null || !file.canRead()) { return; } @@ -140,9 +188,11 @@ public static void shareFile(File file, Context context) { Uri contentUri = FileProvider.getUriForFile(context, "com.nexenio.bleindoorpositioning.fileprovider", file); shareIntent.setTypeAndNormalize(context.getContentResolver().getType(contentUri)); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, file.getName()); - shareIntent.putExtra(Intent.EXTRA_TEXT, file.getName()); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + shareIntent.putExtra(Intent.EXTRA_TEXT, text); shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri); + // Todo: Remove + shareIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"marvin.mirtschin@nexenio.com"}); Intent chooserIntent = Intent.createChooser(shareIntent, "Share file"); chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java index e6a2f1f..c90b421 100644 --- a/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java +++ b/app/src/main/java/com/nexenio/bleindoorpositioningdemo/RecordingActivity.java @@ -186,9 +186,9 @@ public void onClearRecordingsButtonClicked(View view) { .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { File documentsDirectory = ExternalStorageUtils.getDocumentsDirectory(RECORDING_DIRECTORY_NAME); - List jsonFilesInDirectory = ExternalStorageUtils.getJsonFilesInDirectory(documentsDirectory); - ExternalStorageUtils.removeFiles(jsonFilesInDirectory); - Toast.makeText(RecordingActivity.this, "Removed " + jsonFilesInDirectory.size() + " file(s)", Toast.LENGTH_SHORT).show(); + List files = ExternalStorageUtils.getFilesInDirectory(documentsDirectory); + ExternalStorageUtils.removeFiles(files); + Toast.makeText(RecordingActivity.this, "Removed " + files.size() + " file(s)", Toast.LENGTH_SHORT).show(); } }) .setNegativeButton(android.R.string.no, null).show(); @@ -324,6 +324,31 @@ private List getNecessaryTextInputEditTexts() { beaconAdvertisingFrequencyEditText); } + public void onExportAllRecordingsButtonClicked(View view) { + exportAllRecordings(); + } + + private void exportAllRecordings() { + File documentsDirectory = ExternalStorageUtils.getDocumentsDirectory(RECORDING_DIRECTORY_NAME); + List jsonFiles = ExternalStorageUtils.getJsonFilesInDirectory(documentsDirectory); + + String fileName = "measurements.zip"; + File zipFile = new File(documentsDirectory, fileName); + + if (zipFile.exists()) { + zipFile.delete(); + } + + try { + ExternalStorageUtils.zip(jsonFiles, zipFile.getAbsolutePath()); + } catch (IOException e) { + Toast.makeText(this, "Unable to export files", Toast.LENGTH_LONG).show(); + Log.e(TAG, "Unable to export measurements", e); + return; + } + ExternalStorageUtils.shareFile(zipFile, this); + } + private void exportMeasurements() { String jsonString = createJsonString(rssiMeasurements); String fileName = "rssiMeasurements_" + rssiMeasurements.getStartTimestamp() + "_" + rssiMeasurements.getEndTimestamp() + ".json"; @@ -349,6 +374,7 @@ private File persistJsonFile(String fileName, String jsonString) { } ExternalStorageUtils.writeStringToFile(jsonString, file, false); } catch (IOException e) { + // TODO: log + toast e.printStackTrace(); } return file; diff --git a/app/src/main/res/layout/activity_recording.xml b/app/src/main/res/layout/activity_recording.xml index 8533163..1470a5f 100644 --- a/app/src/main/res/layout/activity_recording.xml +++ b/app/src/main/res/layout/activity_recording.xml @@ -36,6 +36,13 @@ android:layout_height="wrap_content" android:text="@string/action_start_recording"/> + + Record Start Recording Stop Recording + Export all recordings Delete all recordings General Info Device Info diff --git a/build.gradle b/build.gradle index 54ecd9a..5682d63 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.6.1' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff6b198..4e678bb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Nov 14 17:36:46 CET 2018 +#Fri May 01 12:14:23 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip