diff --git a/ElasticSensorDump/build.gradle b/ElasticSensorDump/build.gradle
index a984d63..ff3745a 100644
--- a/ElasticSensorDump/build.gradle
+++ b/ElasticSensorDump/build.gradle
@@ -2,11 +2,12 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 25
- buildToolsVersion "25.0.2"
+ buildToolsVersion "25.0.3"
defaultConfig {
applicationId "ca.dungeons.sensordump"
- minSdkVersion 20
+ minSdkVersion 25
+ //noinspection OldTargetApi
targetSdkVersion 25
versionCode 16
versionName "1.4.7"
@@ -17,11 +18,15 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ useLibrary 'org.apache.http.legacy'
}
dependencies {
- compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile fileTree(include: ['*.jar'], dir: 'libs')
+ compile 'com.android.support:appcompat-v7:25.3.1'
+ compile 'com.android.support:support-v4:25.3.1'
+ compile 'com.android.support.constraint:constraint-layout:1.0.2'
+ compile 'com.google.android.gms:play-services:11.0.4'
testCompile 'junit:junit:4.12'
- compile 'com.android.support:appcompat-v7:25.1.0'
- compile 'com.android.support:support-v4:25.1.0'
+ compile 'com.android.support:design:26.0.0-alpha1'
}
diff --git a/ElasticSensorDump/src/main/AndroidManifest.xml b/ElasticSensorDump/src/main/AndroidManifest.xml
index b81a73c..7facbc2 100644
--- a/ElasticSensorDump/src/main/AndroidManifest.xml
+++ b/ElasticSensorDump/src/main/AndroidManifest.xml
@@ -4,6 +4,10 @@
+
+
+
+
+
+
-
+ android:label="Settings">
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/AudioRunnable.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/AudioRunnable.java
new file mode 100644
index 0000000..ebcfb29
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/AudioRunnable.java
@@ -0,0 +1,152 @@
+package ca.dungeons.sensordump;
+
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+class AudioRunnable implements Runnable {
+
+ private final String logTag = "audioLogger";
+
+ /** We use this to indicate to the sensor thread if we have data to send. */
+ boolean hasData = false;
+
+ /** Use this control variable to stop the recording of audio data. */
+ private boolean stopThread = false;
+
+ /** A reference to the current audio sample "loudness" in terms of percentage of mic capability.*/
+ private float amplitude = 0;
+
+ /** A reference to the current audio sample frequency. */
+ private float frequency = 0;
+
+ /** The sampling rate of the audio recording. */
+ private final int SAMPLE_RATE = 44100;
+
+ /** Short type array to feed to the recording API. */
+ private short[] audioBuffer;
+
+ /** Minimum buffer size required by AudioRecord API. */
+ private int bufferSize;
+
+
+ /** Default constructor.
+ * Determine minimum buffer size, get data from Android audio api.
+ * Set variables before executing the runnable.
+ */
+ AudioRunnable(){
+
+ // Buffer size in bytes.
+ bufferSize = AudioRecord.getMinBufferSize(
+ SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT
+ );
+
+ // A check to make sure we are doing math on valid objects.
+ if( bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE ){
+ bufferSize = SAMPLE_RATE * 2;
+ }
+
+ }
+
+ /** Stop the audio logging thread. */
+ void setStopAudioThread(){
+ stopThread = true;
+ }
+
+ /** Main entrance. */
+ @SuppressWarnings("ConstantConditions")
+ @Override
+ public void run() {
+
+ // ?????
+ audioBuffer = new short[bufferSize / 2];
+
+ // New instance of Android audio recording api.
+ AudioRecord audioRecord = new AudioRecord(
+ MediaRecorder.AudioSource.DEFAULT,
+ SAMPLE_RATE,
+ AudioFormat.CHANNEL_IN_MONO,
+ AudioFormat.ENCODING_PCM_16BIT,
+ bufferSize
+ );
+
+ if( audioRecord.getState() != AudioRecord.STATE_INITIALIZED ){
+ Log.e("Audio Error", "AudioRecord has not been initialized properly.");
+ return;
+ }
+
+ while( !stopThread ){
+
+ audioRecord.read( audioBuffer, 0, audioBuffer.length );
+
+ float lowest = 0;
+ float highest = 0;
+ int zeroes = 0;
+ int last_value = 0;
+
+
+ if( audioBuffer != null ){
+ // Exploring the buffer. Record the highest and lowest readings
+ for( short anAudioBuffer : audioBuffer ){
+
+ lowest = anAudioBuffer < lowest ? anAudioBuffer : lowest;
+
+ highest = anAudioBuffer > highest ? anAudioBuffer : highest;
+
+ // Down and coming up
+ if( anAudioBuffer > 0 && last_value < 0 ){
+ zeroes++;
+ }
+
+ // Up and down
+ if( anAudioBuffer < 0 && last_value > 0 ){
+ zeroes++;
+ }
+
+ last_value = anAudioBuffer;
+
+ // Calculate highest and lowest peak difference as a % of the max possible
+ // value
+ amplitude = ( highest - lowest ) / 65536 * 100;
+
+ // Take the count of the peaks in the time that we had based on the sample
+ // rate to calculate frequency
+ if( audioBuffer != null ){
+ float seconds = (float) audioBuffer.length / SAMPLE_RATE;
+ frequency = (float) zeroes / seconds / 2;
+
+ hasData = true;
+ }
+
+ }
+ }
+ }
+
+ audioRecord.stop();
+ audioRecord.release();
+ Log.i( logTag, "Audio recording stopping.");
+
+ }
+
+ /** Called on the sensor thread, delivers data to the sensor message handler. */
+ JSONObject getAudioData( JSONObject passedJson ){
+ if(passedJson != null ){
+ try{
+ passedJson.put("frequency", frequency );
+ passedJson.put("amplitude", amplitude);
+ }catch( JSONException jsonEx ) {
+ Log.e( logTag, "Error adding data to json. " );
+ return passedJson;
+ }
+ }
+ audioBuffer = new short[bufferSize / 2];
+ hasData = false;
+ return passedJson;
+ }
+
+
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeCaptureActivity.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeCaptureActivity.java
new file mode 100644
index 0000000..b0c1bc7
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeCaptureActivity.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ca.dungeons.sensordump;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.hardware.Camera;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.widget.Toast;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.common.api.CommonStatusCodes;
+
+import com.google.android.gms.vision.MultiProcessor;
+import com.google.android.gms.vision.barcode.Barcode;
+import com.google.android.gms.vision.barcode.BarcodeDetector;
+
+import java.io.IOException;
+
+
+/**
+ * Activity for the multi-tracker app. This app detects barcodes and displays the value with the
+ * rear facing camera. During detection overlay graphics are drawn to indicate the position,
+ * size, and ID of each barcode.
+ */
+public final class BarcodeCaptureActivity extends AppCompatActivity implements BarcodeGraphicTracker.BarcodeUpdateListener {
+ private static final String TAG = "Barcode-reader";
+
+ // intent request code to handle updating play services if needed.
+ private static final int RC_HANDLE_GMS = 9001;
+
+ // permission request codes need to be < 256
+ private static final int RC_HANDLE_CAMERA_PERM = 2;
+
+ // constants used to pass extra data in the intent
+ public static final String AutoFocus = "AutoFocus";
+ public static final String UseFlash = "UseFlash";
+ public static final String BarcodeObject = "Barcode";
+
+ private CameraSource mCameraSource;
+ private CameraSourcePreview mPreview;
+ private GraphicOverlay mGraphicOverlay;
+
+ // helper objects for detecting taps and pinches.
+ private ScaleGestureDetector scaleGestureDetector;
+ private GestureDetector gestureDetector;
+
+ /**
+ * Initializes the UI and creates the detector pipeline.
+ */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.barcode_capture);
+
+ mPreview = (CameraSourcePreview) findViewById(R.id.preview);
+ mGraphicOverlay = ( GraphicOverlay ) findViewById(R.id.graphicOverlay);
+
+ if( mGraphicOverlay != null ){
+ Log.e( TAG, "mGraphicOverlay NOT NULL" );
+
+ }else{
+ Log.e( TAG, "mGraphicOverlay NULL!!" );
+ }
+
+ // read parameters from the intent used to launch the activity.
+ boolean autoFocus = getIntent().getBooleanExtra(AutoFocus, false);
+ boolean useFlash = getIntent().getBooleanExtra(UseFlash, false);
+
+ // Check for the camera permission before accessing the camera. If the
+ // permission is not granted yet, request permission.
+ int rc = ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
+ if (rc == PackageManager.PERMISSION_GRANTED) {
+ createCameraSource(autoFocus, useFlash);
+ } else {
+ requestCameraPermission();
+ }
+
+ gestureDetector = new GestureDetector(this, new CaptureGestureListener());
+ scaleGestureDetector = new ScaleGestureDetector(this, new ScaleListener());
+
+
+ }
+
+ /**
+ * Handles the requesting of the camera permission. This includes
+ * showing a "Snackbar" message of why the permission is needed then
+ * sending the request.
+ */
+ private void requestCameraPermission() {
+ Log.w(TAG, "Camera permission is not granted. Requesting permission");
+
+ final String[] permissions = new String[]{Manifest.permission.CAMERA};
+
+ if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
+ Manifest.permission.CAMERA)) {
+ ActivityCompat.requestPermissions(this, permissions, RC_HANDLE_CAMERA_PERM);
+ return;
+ }
+
+ final Activity thisActivity = this;
+
+ View.OnClickListener listener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ActivityCompat.requestPermissions(thisActivity, permissions,
+ RC_HANDLE_CAMERA_PERM);
+ }
+ };
+
+ findViewById(R.id.topLayout).setOnClickListener(listener);
+ Snackbar.make(mGraphicOverlay, R.string.permission_camera_rationale,
+ Snackbar.LENGTH_INDEFINITE)
+ .setAction(R.string.ok, listener)
+ .show();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent e) {
+ boolean b = scaleGestureDetector.onTouchEvent(e);
+
+ boolean c = gestureDetector.onTouchEvent(e);
+
+ return b || c || super.onTouchEvent(e);
+ }
+
+ /**
+ * Creates and starts the camera. Note that this uses a higher resolution in comparison
+ * to other detection examples to enable the barcode detector to detect small barcodes
+ * at long distances.
+ *
+ * Suppressing InlinedApi since there is a check that the minimum version is met before using
+ * the constant.
+ */
+ @SuppressLint("InlinedApi")
+ private void createCameraSource(boolean autoFocus, boolean useFlash) {
+ Context context = getApplicationContext();
+
+ // A barcode detector is created to track barcodes. An associated multi-processor instance
+ // is set to receive the barcode detection results, track the barcodes, and maintain
+ // graphics for each barcode on screen. The factory is used by the multi-processor to
+ // create a separate tracker instance for each barcode.
+ BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(context).build();
+ BarcodeTrackerFactory barcodeFactory = new BarcodeTrackerFactory(mGraphicOverlay, this);
+ barcodeDetector.setProcessor(
+ new MultiProcessor.Builder<>(barcodeFactory).build());
+
+ if (!barcodeDetector.isOperational()) {
+ // Note: The first time that an app using the barcode or face API is installed on a
+ // device, GMS will download a native libraries to the device in order to do detection.
+ // Usually this completes before the app is run for the first time. But if that
+ // download has not yet completed, then the above call will not detect any barcodes
+ // and/or faces.
+ //
+ // isOperational() can be used to check if the required native libraries are currently
+ // available. The detectors will automatically become operational once the library
+ // downloads complete on device.
+ Log.w(TAG, "Detector dependencies are not yet available.");
+
+ // Check for low storage. If there is low storage, the native library will not be
+ // downloaded, so detection will not become operational.
+ IntentFilter lowstorageFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW);
+ boolean hasLowStorage = registerReceiver(null, lowstorageFilter) != null;
+
+ if (hasLowStorage) {
+ Toast.makeText(this, R.string.low_storage_error, Toast.LENGTH_LONG).show();
+ Log.w(TAG, getString(R.string.low_storage_error));
+ }
+ }
+
+ // Creates and starts the camera. Note that this uses a higher resolution in comparison
+ // to other detection examples to enable the barcode detector to detect small barcodes
+ // at long distances.
+ CameraSource.Builder builder = new CameraSource.Builder(getApplicationContext(), barcodeDetector)
+ .setFacing(CameraSource.CAMERA_FACING_BACK)
+ .setRequestedPreviewSize(1600, 1024)
+ .setRequestedFps(15.0f);
+
+
+ builder = builder.setFocusMode(
+ autoFocus ? Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE : null);
+
+
+ mCameraSource = builder
+ .setFlashMode(useFlash ? Camera.Parameters.FLASH_MODE_TORCH : null)
+ .build();
+
+ if( mCameraSource != null ){
+ Log.e( TAG, "mCameraSource NOT NULL " );
+ }else{
+ Log.e( TAG, "mCameraSource NULL!!" );
+ }
+ }
+
+ /**
+ * Restarts the camera.
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ startCameraSource();
+ }
+
+ /**
+ * Stops the camera.
+ */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mPreview != null) {
+ mPreview.stop();
+ }
+ }
+
+ /**
+ * Releases the resources associated with the camera source, the associated detectors, and the
+ * rest of the processing pipeline.
+ */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mPreview != null) {
+ mPreview.release();
+ }
+ }
+
+ /**
+ * Callback for the result from requesting permissions. This method
+ * is invoked for every call on {@link #requestPermissions(String[], int)}.
+ *
+ * Note: It is possible that the permissions request interaction
+ * with the user is interrupted. In this case you will receive empty permissions
+ * and results arrays which should be treated as a cancellation.
+ *
+ *
+ * @param requestCode The request code passed in {@link #requestPermissions(String[], int)}.
+ * @param permissions The requested permissions. Never null.
+ * @param grantResults The grant results for the corresponding permissions
+ * which is either {@link PackageManager#PERMISSION_GRANTED}
+ * or {@link PackageManager#PERMISSION_DENIED}. Never null.
+ * @see #requestPermissions(String[], int)
+ */
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (requestCode != RC_HANDLE_CAMERA_PERM) {
+ Log.d(TAG, "Got unexpected permission result: " + requestCode);
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ return;
+ }
+
+ if (grantResults.length != 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "Camera permission granted - initialize the camera source");
+ // we have permission, so create the camerasource
+ boolean autoFocus = getIntent().getBooleanExtra(AutoFocus,false);
+ boolean useFlash = getIntent().getBooleanExtra(UseFlash, false);
+ createCameraSource(autoFocus, useFlash);
+ return;
+ }
+
+ Log.e(TAG, "Permission not granted: results len = " + grantResults.length +
+ " Result code = " + (grantResults.length > 0 ? grantResults[0] : "(empty)"));
+
+ DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ finish();
+ }
+ };
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Multitracker sample")
+ .setMessage(R.string.no_camera_permission)
+ .setPositiveButton(R.string.ok, listener)
+ .show();
+ }
+
+ /**
+ * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet
+ * (e.g., because onResume was called before the camera source was created), this will be called
+ * again when the camera source is created.
+ */
+ private void startCameraSource() throws SecurityException {
+ // check that the device has play services available.
+ int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(
+ getApplicationContext());
+ if (code != ConnectionResult.SUCCESS) {
+ Dialog dlg =
+ GoogleApiAvailability.getInstance().getErrorDialog(this, code, RC_HANDLE_GMS);
+ dlg.show();
+ }
+
+ if ( mCameraSource != null ) {
+
+ if( mGraphicOverlay != null ){
+ Log.e( TAG, "mGraphicOverlay is null" );
+ }
+
+ try {
+ mPreview.start(mCameraSource, mGraphicOverlay);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to start camera source.", e);
+ mCameraSource.release();
+ mCameraSource = null;
+ }catch( NullPointerException nullPtrEx ){
+ Log.e(TAG, "Unable to start camera source.", nullPtrEx);
+ }
+ }
+
+ }
+
+ /**
+ * onTap returns the tapped barcode result to the calling Activity.
+ *
+ * @param rawX - the raw position of the tap
+ * @param rawY - the raw position of the tap.
+ * @return true if the activity is ending.
+ */
+ private boolean onTap(float rawX, float rawY) {
+
+ Intent data = new Intent();
+
+ // Find tap point in preview frame coordinates.
+ int[] location = new int[2];
+ mGraphicOverlay.getLocationOnScreen(location);
+ float x = (rawX - location[0]) / mGraphicOverlay.getWidthScaleFactor();
+ float y = (rawY - location[1]) / mGraphicOverlay.getHeightScaleFactor();
+
+ // Find the barcode whose center is closest to the tapped point.
+ Barcode best = null;
+ float bestDistance = Float.MAX_VALUE;
+ for (BarcodeGraphic graphic : mGraphicOverlay.getGraphics()) {
+ Barcode barcode = graphic.getBarcode();
+ if (barcode.getBoundingBox().contains((int) x, (int) y)) {
+ // Exact hit, no need to keep looking.
+ best = barcode;
+ break;
+ }
+ float dx = x - barcode.getBoundingBox().centerX();
+ float dy = y - barcode.getBoundingBox().centerY();
+ float distance = (dx * dx) + (dy * dy); // actually squared distance
+ if (distance < bestDistance) {
+ best = barcode;
+ bestDistance = distance;
+ }
+ }
+
+ if (best != null) {
+ data.putExtra(BarcodeObject, best);
+ setResult(CommonStatusCodes.SUCCESS, data);
+ finish();
+ return true;
+ }
+
+ setResult(CommonStatusCodes.ERROR, data);
+ return false;
+ }
+
+ private class CaptureGestureListener extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ return onTap(e.getRawX(), e.getRawY()) || super.onSingleTapConfirmed(e);
+ }
+ }
+
+ private class ScaleListener implements ScaleGestureDetector.OnScaleGestureListener {
+
+ /**
+ * Responds to scaling events for a gesture in progress.
+ * Reported by pointer motion.
+ *
+ * @param detector The detector reporting the event - use this to
+ * retrieve extended info about event state.
+ * @return Whether or not the detector should consider this event
+ * as handled. If an event was not handled, the detector
+ * will continue to accumulate movement until an event is
+ * handled. This can be useful if an application, for example,
+ * only wants to update scaling factors if the change is
+ * greater than 0.01.
+ */
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ return false;
+ }
+
+ /**
+ * Responds to the beginning of a scaling gesture. Reported by
+ * new pointers going down.
+ *
+ * @param detector The detector reporting the event - use this to
+ * retrieve extended info about event state.
+ * @return Whether or not the detector should continue recognizing
+ * this gesture. For example, if a gesture is beginning
+ * with a focal point outside of a region where it makes
+ * sense, onScaleBegin() may return false to ignore the
+ * rest of the gesture.
+ */
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return true;
+ }
+
+ /**
+ * Responds to the end of a scale gesture. Reported by existing
+ * pointers going up.
+ *
+ * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
+ * and {@link ScaleGestureDetector#getFocusY()} will return focal point
+ * of the pointers remaining on the screen.
+ *
+ * @param detector The detector reporting the event - use this to
+ * retrieve extended info about event state.
+ */
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mCameraSource.doZoom(detector.getScaleFactor());
+ }
+ }
+
+ @Override
+ public void onBarcodeDetected(Barcode barcode) {
+ //do something with barcode data returned
+ }
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeGraphic.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeGraphic.java
new file mode 100644
index 0000000..c151bb2
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeGraphic.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ca.dungeons.sensordump;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+import com.google.android.gms.vision.barcode.Barcode;
+
+/**
+ * Graphic instance for rendering barcode position, size, and ID within an associated graphic
+ * overlay view.
+ */
+public class BarcodeGraphic extends GraphicOverlay.Graphic {
+
+ private int mId;
+
+ private static final int COLOR_CHOICES[] = {
+ Color.BLUE,
+ Color.CYAN,
+ Color.GREEN
+ };
+
+ private static int mCurrentColorIndex = 0;
+
+ private Paint mRectPaint;
+ private Paint mTextPaint;
+ private volatile Barcode mBarcode;
+
+ BarcodeGraphic(GraphicOverlay overlay) {
+ super(overlay);
+
+ mCurrentColorIndex = (mCurrentColorIndex + 1) % COLOR_CHOICES.length;
+ final int selectedColor = COLOR_CHOICES[mCurrentColorIndex];
+
+ mRectPaint = new Paint();
+ mRectPaint.setColor(selectedColor);
+ mRectPaint.setStyle(Paint.Style.STROKE);
+ mRectPaint.setStrokeWidth(4.0f);
+
+ mTextPaint = new Paint();
+ mTextPaint.setColor(selectedColor);
+ mTextPaint.setTextSize(36.0f);
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public void setId(int id) {
+ this.mId = id;
+ }
+
+ public Barcode getBarcode() {
+ return mBarcode;
+ }
+
+ /**
+ * Updates the barcode instance from the detection of the most recent frame. Invalidates the
+ * relevant portions of the overlay to trigger a redraw.
+ */
+ void updateItem(Barcode barcode) {
+ mBarcode = barcode;
+ postInvalidate();
+ }
+
+ /**
+ * Draws the barcode annotations for position, size, and raw value on the supplied canvas.
+ */
+ @Override
+ public void draw(Canvas canvas) {
+ Barcode barcode = mBarcode;
+ if (barcode == null) {
+ return;
+ }
+
+ // Draws the bounding box around the barcode.
+ RectF rect = new RectF(barcode.getBoundingBox());
+ rect.left = translateX(rect.left);
+ rect.top = translateY(rect.top);
+ rect.right = translateX(rect.right);
+ rect.bottom = translateY(rect.bottom);
+ canvas.drawRect(rect, mRectPaint);
+
+ // Draws a label at the bottom of the barcode indicate the barcode value that was detected.
+ canvas.drawText(barcode.rawValue, rect.left, rect.bottom, mTextPaint);
+ }
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeGraphicTracker.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeGraphicTracker.java
new file mode 100644
index 0000000..0919ba5
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeGraphicTracker.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ca.dungeons.sensordump;
+
+import android.content.Context;
+import android.support.annotation.UiThread;
+
+import com.google.android.gms.vision.Detector;
+import com.google.android.gms.vision.Tracker;
+import com.google.android.gms.vision.barcode.Barcode;
+
+/**
+ * Generic tracker which is used for tracking or reading a barcode (and can really be used for
+ * any type of item). This is used to receive newly detected items, add a graphical representation
+ * to an overlay, update the graphics as the item changes, and remove the graphics when the item
+ * goes away.
+ */
+public class BarcodeGraphicTracker extends Tracker {
+ private GraphicOverlay mOverlay;
+ private BarcodeGraphic mGraphic;
+
+ private BarcodeUpdateListener mBarcodeUpdateListener;
+
+ /**
+ * Consume the item instance detected from an Activity or Fragment level by implementing the
+ * BarcodeUpdateListener interface method onBarcodeDetected.
+ */
+ public interface BarcodeUpdateListener {
+ @UiThread
+ void onBarcodeDetected(Barcode barcode);
+ }
+
+ BarcodeGraphicTracker(GraphicOverlay mOverlay, BarcodeGraphic mGraphic,
+ Context context) {
+ this.mOverlay = mOverlay;
+ this.mGraphic = mGraphic;
+ if (context instanceof BarcodeUpdateListener) {
+ this.mBarcodeUpdateListener = (BarcodeUpdateListener) context;
+ } else {
+ throw new RuntimeException("Hosting activity must implement BarcodeUpdateListener");
+ }
+ }
+
+ /**
+ * Start tracking the detected item instance within the item overlay.
+ */
+ @Override
+ public void onNewItem(int id, Barcode item) {
+ mGraphic.setId(id);
+ mBarcodeUpdateListener.onBarcodeDetected(item);
+ }
+
+ /**
+ * Update the position/characteristics of the item within the overlay.
+ */
+ @Override
+ public void onUpdate(Detector.Detections detectionResults, Barcode item) {
+ mOverlay.add(mGraphic);
+ mGraphic.updateItem(item);
+ }
+
+ /**
+ * Hide the graphic when the corresponding object was not detected. This can happen for
+ * intermediate frames temporarily, for example if the object was momentarily blocked from
+ * view.
+ */
+ @Override
+ public void onMissing(Detector.Detections detectionResults) {
+ mOverlay.remove(mGraphic);
+ }
+
+ /**
+ * Called when the item is assumed to be gone for good. Remove the graphic annotation from
+ * the overlay.
+ */
+ @Override
+ public void onDone() {
+ mOverlay.remove(mGraphic);
+ }
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeMainActivity.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeMainActivity.java
new file mode 100644
index 0000000..6ef0e13
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeMainActivity.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ca.dungeons.sensordump;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.app.Activity;
+import android.util.Log;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.TextView;
+
+import com.google.android.gms.common.api.CommonStatusCodes;
+import com.google.android.gms.vision.barcode.Barcode;
+
+/**
+ * Main activity demonstrating how to pass extra parameters to an activity that
+ * reads barcodes.
+ */
+public class BarcodeMainActivity extends Activity implements View.OnClickListener {
+
+ // use a compound button so either checkbox or switch widgets work.
+ private CompoundButton autoFocus;
+ private CompoundButton useFlash;
+ private TextView statusMessage;
+ private TextView barcodeValue;
+
+ private static final int RC_BARCODE_CAPTURE = 9001;
+ private static final String TAG = "BarcodeMain";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ statusMessage = (TextView)findViewById(R.id.status_message);
+ barcodeValue = (TextView)findViewById(R.id.barcode_value);
+
+ autoFocus = (CompoundButton) findViewById(R.id.auto_focus);
+ useFlash = (CompoundButton) findViewById(R.id.use_flash);
+
+ findViewById(R.id.read_barcode).setOnClickListener(this);
+ }
+
+ /**
+ * Called when a view has been clicked.
+ *
+ * @param v The view that was clicked.
+ */
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.read_barcode) {
+ // launch barcode activity.
+ Intent intent = new Intent(this, BarcodeCaptureActivity.class);
+ intent.putExtra(BarcodeCaptureActivity.AutoFocus, autoFocus.isChecked());
+ intent.putExtra(BarcodeCaptureActivity.UseFlash, useFlash.isChecked());
+
+ startActivityForResult(intent, RC_BARCODE_CAPTURE);
+ }
+
+ }
+
+ /**
+ * Called when an activity you launched exits, giving you the requestCode
+ * you started it with, the resultCode it returned, and any additional
+ * data from it. The resultCode will be
+ * {@link #RESULT_CANCELED} if the activity explicitly returned that,
+ * didn't return any result, or crashed during its operation.
+ *
+ *
You will receive this call immediately before onResume() when your
+ * activity is re-starting.
+ *
+ *
+ * @param requestCode The integer request code originally supplied to
+ * startActivityForResult(), allowing you to identify who this
+ * result came from.
+ * @param resultCode The integer result code returned by the child activity
+ * through its setResult().
+ * @param data An Intent, which can return result data to the caller
+ * (various data can be attached to Intent "extras").
+ * @see #startActivityForResult
+ * @see #createPendingResult
+ * @see #setResult(int)
+ */
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == RC_BARCODE_CAPTURE) {
+ if (resultCode == CommonStatusCodes.SUCCESS) {
+ if (data != null) {
+ Barcode barcode = data.getParcelableExtra(BarcodeCaptureActivity.BarcodeObject);
+ statusMessage.setText(R.string.barcode_success);
+ barcodeValue.setText(barcode.displayValue);
+ Log.d(TAG, "Barcode read: " + barcode.displayValue);
+
+ data.putExtra( "hostString", barcode.displayValue );
+ setResult( resultCode, data);
+ } else {
+ statusMessage.setText(R.string.barcode_failure);
+ Log.d(TAG, "No barcode captured, intent data is null");
+ }
+ } else {
+ statusMessage.setText(String.format(getString(R.string.barcode_error),
+ CommonStatusCodes.getStatusCodeString(resultCode)));
+ }
+ }
+ else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeTrackerFactory.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeTrackerFactory.java
new file mode 100644
index 0000000..ab4ccc5
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/BarcodeTrackerFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ca.dungeons.sensordump;
+
+import android.content.Context;
+
+import com.google.android.gms.vision.MultiProcessor;
+import com.google.android.gms.vision.Tracker;
+import com.google.android.gms.vision.barcode.Barcode;
+
+/**
+ * Factory for creating a tracker and associated graphic to be associated with a new barcode. The
+ * multi-processor uses this factory to create barcode trackers as needed -- one for each barcode.
+ */
+class BarcodeTrackerFactory implements MultiProcessor.Factory {
+ private GraphicOverlay mGraphicOverlay;
+ private Context mContext;
+
+ public BarcodeTrackerFactory(GraphicOverlay mGraphicOverlay,
+ Context mContext) {
+ this.mGraphicOverlay = mGraphicOverlay;
+ this.mContext = mContext;
+ }
+
+ @Override
+ public Tracker create(Barcode barcode) {
+ BarcodeGraphic graphic = new BarcodeGraphic(mGraphicOverlay);
+ return new BarcodeGraphicTracker(mGraphicOverlay, graphic, mContext);
+ }
+
+}
+
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/CameraSource.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/CameraSource.java
new file mode 100644
index 0000000..0a98ab2
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/CameraSource.java
@@ -0,0 +1,1214 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ca.dungeons.sensordump;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+import android.os.Build;
+import android.os.SystemClock;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresPermission;
+import android.support.annotation.StringDef;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.WindowManager;
+
+import com.google.android.gms.common.images.Size;
+import com.google.android.gms.vision.Detector;
+import com.google.android.gms.vision.Frame;
+
+import java.io.IOException;
+import java.lang.Thread.State;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+// Note: This requires Google Play Services 8.1 or higher, due to using indirect byte buffers for
+// storing images.
+
+/**
+ * Manages the camera in conjunction with an underlying
+ * {@link com.google.android.gms.vision.Detector}. This receives preview frames from the camera at
+ * a specified rate, sending those frames to the detector as fast as it is able to process those
+ * frames.
+ *
+ * This camera source makes a best effort to manage processing on preview frames as fast as
+ * possible, while at the same time minimizing lag. As such, frames may be dropped if the detector
+ * is unable to keep up with the rate of frames generated by the camera. You should use
+ * {@link CameraSource.Builder#setRequestedFps(float)} to specify a frame rate that works well with
+ * the capabilities of the camera hardware and the detector options that you have selected. If CPU
+ * utilization is higher than you'd like, then you may want to consider reducing FPS. If the camera
+ * preview or detector results are too "jerky", then you may want to consider increasing FPS.
+ *
+ * The following Android permission is required to use the camera:
+ *
+ *
android.permissions.CAMERA
+ *
+ */
+@SuppressWarnings("deprecation")
+public class CameraSource {
+ @SuppressLint("InlinedApi")
+ public static final int CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK;
+ @SuppressLint("InlinedApi")
+ public static final int CAMERA_FACING_FRONT = CameraInfo.CAMERA_FACING_FRONT;
+
+ private static final String TAG = "OpenCameraSource";
+
+ /**
+ * The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL
+ * context, we can choose any ID we want here.
+ */
+ private static final int DUMMY_TEXTURE_NAME = 100;
+
+ /**
+ * If the absolute difference between a preview size aspect ratio and a picture size aspect
+ * ratio is less than this tolerance, they are considered to be the same aspect ratio.
+ */
+ private static final float ASPECT_RATIO_TOLERANCE = 0.01f;
+
+ @StringDef({
+ Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
+ Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO,
+ Camera.Parameters.FOCUS_MODE_AUTO,
+ Camera.Parameters.FOCUS_MODE_EDOF,
+ Camera.Parameters.FOCUS_MODE_FIXED,
+ Camera.Parameters.FOCUS_MODE_INFINITY,
+ Camera.Parameters.FOCUS_MODE_MACRO
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface FocusMode {}
+
+ @StringDef({
+ Camera.Parameters.FLASH_MODE_ON,
+ Camera.Parameters.FLASH_MODE_OFF,
+ Camera.Parameters.FLASH_MODE_AUTO,
+ Camera.Parameters.FLASH_MODE_RED_EYE,
+ Camera.Parameters.FLASH_MODE_TORCH
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface FlashMode {}
+
+ private Context mContext;
+
+ private final Object mCameraLock = new Object();
+
+ // Guarded by mCameraLock
+ private Camera mCamera;
+
+ private int mFacing = CAMERA_FACING_BACK;
+
+ /**
+ * Rotation of the device, and thus the associated preview images captured from the device.
+ * See {@link Frame.Metadata#getRotation()}.
+ */
+ private int mRotation;
+
+ private Size mPreviewSize;
+
+ // These values may be requested by the caller. Due to hardware limitations, we may need to
+ // select close, but not exactly the same values for these.
+ private float mRequestedFps = 30.0f;
+ private int mRequestedPreviewWidth = 1024;
+ private int mRequestedPreviewHeight = 768;
+
+
+ private String mFocusMode = null;
+ private String mFlashMode = null;
+
+ // These instances need to be held onto to avoid GC of their underlying resources. Even though
+ // these aren't used outside of the method that creates them, they still must have hard
+ // references maintained to them.
+ private SurfaceView mDummySurfaceView;
+ private SurfaceTexture mDummySurfaceTexture;
+
+ /**
+ * Dedicated thread and associated runnable for calling into the detector with frames, as the
+ * frames become available from the camera.
+ */
+ private Thread mProcessingThread;
+ private FrameProcessingRunnable mFrameProcessor;
+
+ /**
+ * Map to convert between a byte array, received from the camera, and its associated byte
+ * buffer. We use byte buffers internally because this is a more efficient way to call into
+ * native code later (avoids a potential copy).
+ */
+ private Map mBytesToByteBuffer = new HashMap<>();
+
+ //==============================================================================================
+ // Builder
+ //==============================================================================================
+
+ /**
+ * Builder for configuring and creating an associated camera source.
+ */
+ public static class Builder {
+ private final Detector> mDetector;
+ private CameraSource mCameraSource = new CameraSource();
+
+ /**
+ * Creates a camera source builder with the supplied context and detector. Camera preview
+ * images will be streamed to the associated detector upon starting the camera source.
+ */
+ public Builder(Context context, Detector> detector) {
+ if (context == null) {
+ throw new IllegalArgumentException("No context supplied.");
+ }
+ if (detector == null) {
+ throw new IllegalArgumentException("No detector supplied.");
+ }
+
+ mDetector = detector;
+ mCameraSource.mContext = context;
+ }
+
+ /**
+ * Sets the requested frame rate in frames per second. If the exact requested value is not
+ * not available, the best matching available value is selected. Default: 30.
+ */
+ public Builder setRequestedFps(float fps) {
+ if (fps <= 0) {
+ throw new IllegalArgumentException("Invalid fps: " + fps);
+ }
+ mCameraSource.mRequestedFps = fps;
+ return this;
+ }
+
+ public Builder setFocusMode(@FocusMode String mode) {
+ mCameraSource.mFocusMode = mode;
+ return this;
+ }
+
+ public Builder setFlashMode(@FlashMode String mode) {
+ mCameraSource.mFlashMode = mode;
+ return this;
+ }
+
+ /**
+ * Sets the desired width and height of the camera frames in pixels. If the exact desired
+ * values are not available options, the best matching available options are selected.
+ * Also, we try to select a preview size which corresponds to the aspect ratio of an
+ * associated full picture size, if applicable. Default: 1024x768.
+ */
+ public Builder setRequestedPreviewSize(int width, int height) {
+ // Restrict the requested range to something within the realm of possibility. The
+ // choice of 1000000 is a bit arbitrary -- intended to be well beyond resolutions that
+ // devices can support. We bound this to avoid int overflow in the code later.
+ final int MAX = 1000000;
+ if ((width <= 0) || (width > MAX) || (height <= 0) || (height > MAX)) {
+ throw new IllegalArgumentException("Invalid preview size: " + width + "x" + height);
+ }
+ mCameraSource.mRequestedPreviewWidth = width;
+ mCameraSource.mRequestedPreviewHeight = height;
+ return this;
+ }
+
+ /**
+ * Sets the camera to use (either {@link #CAMERA_FACING_BACK} or
+ * {@link #CAMERA_FACING_FRONT}). Default: back facing.
+ */
+ public Builder setFacing(int facing) {
+ if ((facing != CAMERA_FACING_BACK) && (facing != CAMERA_FACING_FRONT)) {
+ throw new IllegalArgumentException("Invalid camera: " + facing);
+ }
+ mCameraSource.mFacing = facing;
+ return this;
+ }
+
+ /**
+ * Creates an instance of the camera source.
+ */
+ public CameraSource build() {
+ mCameraSource.mFrameProcessor = mCameraSource.new FrameProcessingRunnable(mDetector);
+ return mCameraSource;
+ }
+ }
+
+ //==============================================================================================
+ // Bridge Functionality for the Camera1 API
+ //==============================================================================================
+
+ /**
+ * Callback interface used to signal the moment of actual image capture.
+ */
+ public interface ShutterCallback {
+ /**
+ * Called as near as possible to the moment when a photo is captured from the sensor. This
+ * is a good opportunity to play a shutter sound or give other feedback of camera operation.
+ * This may be some time after the photo was triggered, but some time before the actual data
+ * is available.
+ */
+ void onShutter();
+ }
+
+ /**
+ * Callback interface used to supply image data from a photo capture.
+ */
+ public interface PictureCallback {
+ /**
+ * Called when image data is available after a picture is taken. The format of the data
+ * is a jpeg binary.
+ */
+ void onPictureTaken(byte[] data);
+ }
+
+ /**
+ * Callback interface used to notify on completion of camera auto focus.
+ */
+ public interface AutoFocusCallback {
+ /**
+ * Called when the camera auto focus completes. If the camera
+ * does not support auto-focus and autoFocus is called,
+ * onAutoFocus will be called immediately with a fake value of
+ * success set to true.
+ *
+ * The auto-focus routine does not lock auto-exposure and auto-white
+ * balance after it completes.
+ *
+ * @param success true if focus was successful, false if otherwise
+ */
+ void onAutoFocus(boolean success);
+ }
+
+ /**
+ * Callback interface used to notify on auto focus start and stop.
+ *
+ *
This is only supported in continuous autofocus modes -- {@link
+ * Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} and {@link
+ * Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can show
+ * autofocus animation based on this.
+ */
+ public interface AutoFocusMoveCallback {
+ /**
+ * Called when the camera auto focus starts or stops.
+ *
+ * @param start true if focus starts to move, false if focus stops to move
+ */
+ void onAutoFocusMoving(boolean start);
+ }
+
+ //==============================================================================================
+ // Public
+ //==============================================================================================
+
+ /**
+ * Stops the camera and releases the resources of the camera and underlying detector.
+ */
+ public void release() {
+ synchronized (mCameraLock) {
+ stop();
+ mFrameProcessor.release();
+ }
+ }
+
+ /**
+ * Opens the camera and starts sending preview frames to the underlying detector. The preview
+ * frames are not displayed.
+ *
+ * @throws IOException if the camera's preview texture or display could not be initialized
+ */
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public CameraSource start() throws IOException {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ return this;
+ }
+
+ mCamera = createCamera();
+
+ // SurfaceTexture was introduced in Honeycomb (11), so if we are running and
+ // old version of Android. fall back to use SurfaceView.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mDummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME);
+ mCamera.setPreviewTexture(mDummySurfaceTexture);
+ } else {
+ mDummySurfaceView = new SurfaceView(mContext);
+ mCamera.setPreviewDisplay(mDummySurfaceView.getHolder());
+ }
+ mCamera.startPreview();
+
+ mProcessingThread = new Thread(mFrameProcessor);
+ mFrameProcessor.setActive(true);
+ mProcessingThread.start();
+ }
+ return this;
+ }
+
+ /**
+ * Opens the camera and starts sending preview frames to the underlying detector. The supplied
+ * surface holder is used for the preview so frames can be displayed to the user.
+ *
+ * @param surfaceHolder the surface holder to use for the preview frames
+ * @throws IOException if the supplied surface holder could not be used as the preview display
+ */
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public CameraSource start(SurfaceHolder surfaceHolder) throws IOException {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ return this;
+ }
+
+ mCamera = createCamera();
+ mCamera.setPreviewDisplay(surfaceHolder);
+ mCamera.startPreview();
+
+ mProcessingThread = new Thread(mFrameProcessor);
+ mFrameProcessor.setActive(true);
+ mProcessingThread.start();
+ }
+ return this;
+ }
+
+ /**
+ * Closes the camera and stops sending frames to the underlying frame detector.
+ *
+ * This camera source may be restarted again by calling {@link #start()} or
+ * {@link #start(SurfaceHolder)}.
+ *
+ * Call {@link #release()} instead to completely shut down this camera source and release the
+ * resources of the underlying detector.
+ */
+ public void stop() {
+ synchronized (mCameraLock) {
+ mFrameProcessor.setActive(false);
+ if (mProcessingThread != null) {
+ try {
+ // Wait for the thread to complete to ensure that we can't have multiple threads
+ // executing at the same time (i.e., which would happen if we called start too
+ // quickly after stop).
+ mProcessingThread.join();
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Frame processing thread interrupted on release.");
+ }
+ mProcessingThread = null;
+ }
+
+ // clear the buffer to prevent oom exceptions
+ mBytesToByteBuffer.clear();
+
+ if (mCamera != null) {
+ mCamera.stopPreview();
+ mCamera.setPreviewCallbackWithBuffer(null);
+ try {
+ // We want to be compatible back to Gingerbread, but SurfaceTexture
+ // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the
+ // developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't
+ // want to display a preview we use a SurfaceTexture if we are running at least Honeycomb.
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mCamera.setPreviewTexture(null);
+
+ } else {
+ mCamera.setPreviewDisplay(null);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to clear camera preview: " + e);
+ }
+ mCamera.release();
+ mCamera = null;
+ }
+ }
+ }
+
+ /**
+ * Returns the preview size that is currently in use by the underlying camera.
+ */
+ public Size getPreviewSize() {
+ return mPreviewSize;
+ }
+
+ /**
+ * Returns the selected camera; one of {@link #CAMERA_FACING_BACK} or
+ * {@link #CAMERA_FACING_FRONT}.
+ */
+ public int getCameraFacing() {
+ return mFacing;
+ }
+
+ public int doZoom(float scale) {
+ synchronized (mCameraLock) {
+ if (mCamera == null) {
+ return 0;
+ }
+ int currentZoom = 0;
+ int maxZoom;
+ Camera.Parameters parameters = mCamera.getParameters();
+ if (!parameters.isZoomSupported()) {
+ Log.w(TAG, "Zoom is not supported on this device");
+ return currentZoom;
+ }
+ maxZoom = parameters.getMaxZoom();
+
+ currentZoom = parameters.getZoom() + 1;
+ float newZoom;
+ if (scale > 1) {
+ newZoom = currentZoom + scale * (maxZoom / 10);
+ } else {
+ newZoom = currentZoom * scale;
+ }
+ currentZoom = Math.round(newZoom) - 1;
+ if (currentZoom < 0) {
+ currentZoom = 0;
+ } else if (currentZoom > maxZoom) {
+ currentZoom = maxZoom;
+ }
+ parameters.setZoom(currentZoom);
+ mCamera.setParameters(parameters);
+ return currentZoom;
+ }
+ }
+
+ /**
+ * Initiates taking a picture, which happens asynchronously. The camera source should have been
+ * activated previously with {@link #start()} or {@link #start(SurfaceHolder)}. The camera
+ * preview is suspended while the picture is being taken, but will resume once picture taking is
+ * done.
+ *
+ * @param shutter the callback for image capture moment, or null
+ * @param jpeg the callback for JPEG image data, or null
+ */
+ public void takePicture(ShutterCallback shutter, PictureCallback jpeg) {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ PictureStartCallback startCallback = new PictureStartCallback();
+ startCallback.mDelegate = shutter;
+ PictureDoneCallback doneCallback = new PictureDoneCallback();
+ doneCallback.mDelegate = jpeg;
+ mCamera.takePicture(startCallback, null, null, doneCallback);
+ }
+ }
+ }
+
+ /**
+ * Gets the current focus mode setting.
+ *
+ * @return current focus mode. This value is null if the camera is not yet created. Applications should call {@link
+ * #autoFocus(AutoFocusCallback)} to start the focus if focus
+ * mode is FOCUS_MODE_AUTO or FOCUS_MODE_MACRO.
+ * @see Camera.Parameters#FOCUS_MODE_AUTO
+ * @see Camera.Parameters#FOCUS_MODE_INFINITY
+ * @see Camera.Parameters#FOCUS_MODE_MACRO
+ * @see Camera.Parameters#FOCUS_MODE_FIXED
+ * @see Camera.Parameters#FOCUS_MODE_EDOF
+ * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO
+ * @see Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE
+ */
+ @Nullable
+ @FocusMode
+ public String getFocusMode() {
+ return mFocusMode;
+ }
+
+ /**
+ * Sets the focus mode.
+ *
+ * @param mode the focus mode
+ * @return {@code true} if the focus mode is set, {@code false} otherwise
+ * @see #getFocusMode()
+ */
+ public boolean setFocusMode(@FocusMode String mode) {
+ synchronized (mCameraLock) {
+ if (mCamera != null && mode != null) {
+ Camera.Parameters parameters = mCamera.getParameters();
+ if (parameters.getSupportedFocusModes().contains(mode)) {
+ parameters.setFocusMode(mode);
+ mCamera.setParameters(parameters);
+ mFocusMode = mode;
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Gets the current flash mode setting.
+ *
+ * @return current flash mode. null if flash mode setting is not
+ * supported or the camera is not yet created.
+ * @see Camera.Parameters#FLASH_MODE_OFF
+ * @see Camera.Parameters#FLASH_MODE_AUTO
+ * @see Camera.Parameters#FLASH_MODE_ON
+ * @see Camera.Parameters#FLASH_MODE_RED_EYE
+ * @see Camera.Parameters#FLASH_MODE_TORCH
+ */
+ @Nullable
+ @FlashMode
+ public String getFlashMode() {
+ return mFlashMode;
+ }
+
+ /**
+ * Sets the flash mode.
+ *
+ * @param mode flash mode.
+ * @return {@code true} if the flash mode is set, {@code false} otherwise
+ * @see #getFlashMode()
+ */
+ public boolean setFlashMode(@FlashMode String mode) {
+ synchronized (mCameraLock) {
+ if (mCamera != null && mode != null) {
+ Camera.Parameters parameters = mCamera.getParameters();
+ if (parameters.getSupportedFlashModes().contains(mode)) {
+ parameters.setFlashMode(mode);
+ mCamera.setParameters(parameters);
+ mFlashMode = mode;
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Starts camera auto-focus and registers a callback function to run when
+ * the camera is focused. This method is only valid when preview is active
+ * (between {@link #start()} or {@link #start(SurfaceHolder)} and before {@link #stop()} or {@link #release()}).
+ *
+ *
Callers should check
+ * {@link #getFocusMode()} to determine if
+ * this method should be called. If the camera does not support auto-focus,
+ * it is a no-op and {@link AutoFocusCallback#onAutoFocus(boolean)}
+ * callback will be called immediately.
+ *
+ *
If the current flash mode is not
+ * {@link Camera.Parameters#FLASH_MODE_OFF}, flash may be
+ * fired during auto-focus, depending on the driver and camera hardware.
+ *
+ * @param cb the callback to run
+ * @see #cancelAutoFocus()
+ */
+ public void autoFocus(@Nullable AutoFocusCallback cb) {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ CameraAutoFocusCallback autoFocusCallback = null;
+ if (cb != null) {
+ autoFocusCallback = new CameraAutoFocusCallback();
+ autoFocusCallback.mDelegate = cb;
+ }
+ mCamera.autoFocus(autoFocusCallback);
+ }
+ }
+ }
+
+ /**
+ * Cancels any auto-focus function in progress.
+ * Whether or not auto-focus is currently in progress,
+ * this function will return the focus position to the default.
+ * If the camera does not support auto-focus, this is a no-op.
+ *
+ * @see #autoFocus(AutoFocusCallback)
+ */
+ public void cancelAutoFocus() {
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ mCamera.cancelAutoFocus();
+ }
+ }
+ }
+
+ /**
+ * Sets camera auto-focus move callback.
+ *
+ * @param cb the callback to run
+ * @return {@code true} if the operation is supported (i.e. from Jelly Bean), {@code false} otherwise
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public boolean setAutoFocusMoveCallback(@Nullable AutoFocusMoveCallback cb) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ return false;
+ }
+
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ CameraAutoFocusMoveCallback autoFocusMoveCallback = null;
+ if (cb != null) {
+ autoFocusMoveCallback = new CameraAutoFocusMoveCallback();
+ autoFocusMoveCallback.mDelegate = cb;
+ }
+ mCamera.setAutoFocusMoveCallback(autoFocusMoveCallback);
+ }
+ }
+
+ return true;
+ }
+
+ //==============================================================================================
+ // Private
+ //==============================================================================================
+
+ /**
+ * Only allow creation via the builder class.
+ */
+ private CameraSource() {
+ }
+
+ /**
+ * Wraps the camera1 shutter callback so that the deprecated API isn't exposed.
+ */
+ private class PictureStartCallback implements Camera.ShutterCallback {
+ private ShutterCallback mDelegate;
+
+ @Override
+ public void onShutter() {
+ if (mDelegate != null) {
+ mDelegate.onShutter();
+ }
+ }
+ }
+
+ /**
+ * Wraps the final callback in the camera sequence, so that we can automatically turn the camera
+ * preview back on after the picture has been taken.
+ */
+ private class PictureDoneCallback implements Camera.PictureCallback {
+ private PictureCallback mDelegate;
+
+ @Override
+ public void onPictureTaken(byte[] data, Camera camera) {
+ if (mDelegate != null) {
+ mDelegate.onPictureTaken(data);
+ }
+ synchronized (mCameraLock) {
+ if (mCamera != null) {
+ mCamera.startPreview();
+ }
+ }
+ }
+ }
+
+ /**
+ * Wraps the camera1 auto focus callback so that the deprecated API isn't exposed.
+ */
+ private class CameraAutoFocusCallback implements Camera.AutoFocusCallback {
+ private AutoFocusCallback mDelegate;
+
+ @Override
+ public void onAutoFocus(boolean success, Camera camera) {
+ if (mDelegate != null) {
+ mDelegate.onAutoFocus(success);
+ }
+ }
+ }
+
+ /**
+ * Wraps the camera1 auto focus move callback so that the deprecated API isn't exposed.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private class CameraAutoFocusMoveCallback implements Camera.AutoFocusMoveCallback {
+ private AutoFocusMoveCallback mDelegate;
+
+ @Override
+ public void onAutoFocusMoving(boolean start, Camera camera) {
+ if (mDelegate != null) {
+ mDelegate.onAutoFocusMoving(start);
+ }
+ }
+ }
+
+ /**
+ * Opens the camera and applies the user settings.
+ *
+ * @throws RuntimeException if the method fails
+ */
+ @SuppressLint("InlinedApi")
+ private Camera createCamera() {
+ int requestedCameraId = getIdForRequestedCamera(mFacing);
+ if (requestedCameraId == -1) {
+ throw new RuntimeException("Could not find requested camera.");
+ }
+ Camera camera = Camera.open(requestedCameraId);
+
+ SizePair sizePair = selectSizePair(camera, mRequestedPreviewWidth, mRequestedPreviewHeight);
+ if (sizePair == null) {
+ throw new RuntimeException("Could not find suitable preview size.");
+ }
+ Size pictureSize = sizePair.pictureSize();
+ mPreviewSize = sizePair.previewSize();
+
+ int[] previewFpsRange = selectPreviewFpsRange(camera, mRequestedFps);
+ if (previewFpsRange == null) {
+ throw new RuntimeException("Could not find suitable preview frames per second range.");
+ }
+
+ Camera.Parameters parameters = camera.getParameters();
+
+ if (pictureSize != null) {
+ parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
+ }
+
+ parameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
+ parameters.setPreviewFpsRange(
+ previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
+ previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
+ parameters.setPreviewFormat(ImageFormat.NV21);
+
+ setRotation(camera, parameters, requestedCameraId);
+
+ if (mFocusMode != null) {
+ if (parameters.getSupportedFocusModes().contains(
+ mFocusMode)) {
+ parameters.setFocusMode(mFocusMode);
+ } else {
+ Log.i(TAG, "Camera focus mode: " + mFocusMode + " is not supported on this device.");
+ }
+ }
+
+ // setting mFocusMode to the one set in the params
+ mFocusMode = parameters.getFocusMode();
+
+ if (mFlashMode != null) {
+ if (parameters.getSupportedFlashModes() != null) {
+ if (parameters.getSupportedFlashModes().contains(
+ mFlashMode)) {
+ parameters.setFlashMode(mFlashMode);
+ } else {
+ Log.i(TAG, "Camera flash mode: " + mFlashMode + " is not supported on this device.");
+ }
+ }
+ }
+
+ // setting mFlashMode to the one set in the params
+ mFlashMode = parameters.getFlashMode();
+
+ camera.setParameters(parameters);
+
+ // Four frame buffers are needed for working with the camera:
+ //
+ // one for the frame that is currently being executed upon in doing detection
+ // one for the next pending frame to process immediately upon completing detection
+ // two for the frames that the camera uses to populate future preview images
+ camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback());
+ camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
+ camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
+ camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
+ camera.addCallbackBuffer(createPreviewBuffer(mPreviewSize));
+
+ return camera;
+ }
+
+ /**
+ * Gets the id for the camera specified by the direction it is facing. Returns -1 if no such
+ * camera was found.
+ *
+ * @param facing the desired camera (front-facing or rear-facing)
+ */
+ private static int getIdForRequestedCamera(int facing) {
+ CameraInfo cameraInfo = new CameraInfo();
+ for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == facing) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Selects the most suitable preview and picture size, given the desired width and height.
+ *
+ * Even though we may only need the preview size, it's necessary to find both the preview
+ * size and the picture size of the camera together, because these need to have the same aspect
+ * ratio. On some hardware, if you would only set the preview size, you will get a distorted
+ * image.
+ *
+ * @param camera the camera to select a preview size from
+ * @param desiredWidth the desired width of the camera preview frames
+ * @param desiredHeight the desired height of the camera preview frames
+ * @return the selected preview and picture size pair
+ */
+ private static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) {
+ List validPreviewSizes = generateValidPreviewSizeList(camera);
+
+ // The method for selecting the best size is to minimize the sum of the differences between
+ // the desired values and the actual values for width and height. This is certainly not the
+ // only way to select the best size, but it provides a decent tradeoff between using the
+ // closest aspect ratio vs. using the closest pixel area.
+ SizePair selectedPair = null;
+ int minDiff = Integer.MAX_VALUE;
+ for (SizePair sizePair : validPreviewSizes) {
+ Size size = sizePair.previewSize();
+ int diff = Math.abs(size.getWidth() - desiredWidth) +
+ Math.abs(size.getHeight() - desiredHeight);
+ if (diff < minDiff) {
+ selectedPair = sizePair;
+ minDiff = diff;
+ }
+ }
+
+ return selectedPair;
+ }
+
+ /**
+ * Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted
+ * preview images on some devices, the picture size must be set to a size that is the same
+ * aspect ratio as the preview size or the preview may end up being distorted. If the picture
+ * size is null, then there is no picture size with the same aspect ratio as the preview size.
+ */
+ private static class SizePair {
+ private Size mPreview;
+ private Size mPicture;
+
+ public SizePair(android.hardware.Camera.Size previewSize,
+ android.hardware.Camera.Size pictureSize) {
+ mPreview = new Size(previewSize.width, previewSize.height);
+ if (pictureSize != null) {
+ mPicture = new Size(pictureSize.width, pictureSize.height);
+ }
+ }
+
+ public Size previewSize() {
+ return mPreview;
+ }
+
+ @SuppressWarnings("unused")
+ public Size pictureSize() {
+ return mPicture;
+ }
+ }
+
+ /**
+ * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is
+ * not a corresponding picture size of the same aspect ratio. If there is a corresponding
+ * picture size of the same aspect ratio, the picture size is paired up with the preview size.
+ *
+ * This is necessary because even if we don't use still pictures, the still picture size must be
+ * set to a size that is the same aspect ratio as the preview size we choose. Otherwise, the
+ * preview images may be distorted on some devices.
+ */
+ private static List generateValidPreviewSizeList(Camera camera) {
+ Camera.Parameters parameters = camera.getParameters();
+ List supportedPreviewSizes =
+ parameters.getSupportedPreviewSizes();
+ List supportedPictureSizes =
+ parameters.getSupportedPictureSizes();
+ List validPreviewSizes = new ArrayList<>();
+ for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) {
+ float previewAspectRatio = (float) previewSize.width / (float) previewSize.height;
+
+ // By looping through the picture sizes in order, we favor the higher resolutions.
+ // We choose the highest resolution in order to support taking the full resolution
+ // picture later.
+ for (android.hardware.Camera.Size pictureSize : supportedPictureSizes) {
+ float pictureAspectRatio = (float) pictureSize.width / (float) pictureSize.height;
+ if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) {
+ validPreviewSizes.add(new SizePair(previewSize, pictureSize));
+ break;
+ }
+ }
+ }
+
+ // If there are no picture sizes with the same aspect ratio as any preview sizes, allow all
+ // of the preview sizes and hope that the camera can handle it. Probably unlikely, but we
+ // still account for it.
+ if (validPreviewSizes.size() == 0) {
+ Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size");
+ for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) {
+ // The null picture size will let us know that we shouldn't set a picture size.
+ validPreviewSizes.add(new SizePair(previewSize, null));
+ }
+ }
+
+ return validPreviewSizes;
+ }
+
+ /**
+ * Selects the most suitable preview frames per second range, given the desired frames per
+ * second.
+ *
+ * @param camera the camera to select a frames per second range from
+ * @param desiredPreviewFps the desired frames per second for the camera preview frames
+ * @return the selected preview frames per second range
+ */
+ private int[] selectPreviewFpsRange(Camera camera, float desiredPreviewFps) {
+ // The camera API uses integers scaled by a factor of 1000 instead of floating-point frame
+ // rates.
+ int desiredPreviewFpsScaled = (int) (desiredPreviewFps * 1000.0f);
+
+ // The method for selecting the best range is to minimize the sum of the differences between
+ // the desired value and the upper and lower bounds of the range. This may select a range
+ // that the desired value is outside of, but this is often preferred. For example, if the
+ // desired frame rate is 29.97, the range (30, 30) is probably more desirable than the
+ // range (15, 30).
+ int[] selectedFpsRange = null;
+ int minDiff = Integer.MAX_VALUE;
+ List previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange();
+ for (int[] range : previewFpsRangeList) {
+ int deltaMin = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
+ int deltaMax = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
+ int diff = Math.abs(deltaMin) + Math.abs(deltaMax);
+ if (diff < minDiff) {
+ selectedFpsRange = range;
+ minDiff = diff;
+ }
+ }
+ return selectedFpsRange;
+ }
+
+ /**
+ * Calculates the correct rotation for the given camera id and sets the rotation in the
+ * parameters. It also sets the camera's display orientation and rotation.
+ *
+ * @param parameters the camera parameters for which to set the rotation
+ * @param cameraId the camera id to set rotation based on
+ */
+ private void setRotation(Camera camera, Camera.Parameters parameters, int cameraId) {
+ WindowManager windowManager =
+ (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ int degrees = 0;
+ int rotation = windowManager.getDefaultDisplay().getRotation();
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ default:
+ Log.e(TAG, "Bad rotation value: " + rotation);
+ }
+
+ CameraInfo cameraInfo = new CameraInfo();
+ Camera.getCameraInfo(cameraId, cameraInfo);
+
+ int angle;
+ int displayAngle;
+ if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ angle = (cameraInfo.orientation + degrees) % 360;
+ displayAngle = (360 - angle) % 360; // compensate for it being mirrored
+ } else { // back-facing
+ angle = (cameraInfo.orientation - degrees + 360) % 360;
+ displayAngle = angle;
+ }
+
+ // This corresponds to the rotation constants in {@link Frame}.
+ mRotation = angle / 90;
+
+ camera.setDisplayOrientation(displayAngle);
+ parameters.setRotation(angle);
+ }
+
+ /**
+ * Creates one buffer for the camera preview callback. The size of the buffer is based off of
+ * the camera preview size and the format of the camera image.
+ *
+ * @return a new preview buffer of the appropriate size for the current camera settings
+ */
+ private byte[] createPreviewBuffer(Size previewSize) {
+ int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21);
+ long sizeInBits = previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel;
+ int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1;
+
+ //
+ // NOTICE: This code only works when using play services v. 8.1 or higher.
+ //
+
+ // Creating the byte array this way and wrapping it, as opposed to using .allocate(),
+ // should guarantee that there will be an array to work with.
+ byte[] byteArray = new byte[bufferSize];
+ ByteBuffer buffer = ByteBuffer.wrap(byteArray);
+ if (!buffer.hasArray() || (buffer.array() != byteArray)) {
+ // I don't think that this will ever happen. But if it does, then we wouldn't be
+ // passing the preview content to the underlying detector later.
+ throw new IllegalStateException("Failed to create valid buffer for camera source.");
+ }
+
+ mBytesToByteBuffer.put(byteArray, buffer);
+ return byteArray;
+ }
+
+ //==============================================================================================
+ // Frame processing
+ //==============================================================================================
+
+ /**
+ * Called when the camera has a new preview frame.
+ */
+ private class CameraPreviewCallback implements Camera.PreviewCallback {
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ mFrameProcessor.setNextFrame(data, camera);
+ }
+ }
+
+ /**
+ * This runnable controls access to the underlying receiver, calling it to process frames when
+ * available from the camera. This is designed to run detection on frames as fast as possible
+ * (i.e., without unnecessary context switching or waiting on the next frame).
+ *
+ * While detection is running on a frame, new frames may be received from the camera. As these
+ * frames come in, the most recent frame is held onto as pending. As soon as detection and its
+ * associated processing are done for the previous frame, detection on the mostly recently
+ * received frame will immediately start on the same thread.
+ */
+ private class FrameProcessingRunnable implements Runnable {
+ private Detector> mDetector;
+ private long mStartTimeMillis = SystemClock.elapsedRealtime();
+
+ // This lock guards all of the member variables below.
+ private final Object mLock = new Object();
+ private boolean mActive = true;
+
+ // These pending variables hold the state associated with the new frame awaiting processing.
+ private long mPendingTimeMillis;
+ private int mPendingFrameId = 0;
+ private ByteBuffer mPendingFrameData;
+
+ FrameProcessingRunnable(Detector> detector) {
+ mDetector = detector;
+ }
+
+ /**
+ * Releases the underlying receiver. This is only safe to do after the associated thread
+ * has completed, which is managed in camera source's release method above.
+ */
+ @SuppressLint("Assert")
+ void release() {
+ assert (mProcessingThread.getState() == State.TERMINATED);
+ mDetector.release();
+ mDetector = null;
+ }
+
+ /**
+ * Marks the runnable as active/not active. Signals any blocked threads to continue.
+ */
+ void setActive(boolean active) {
+ synchronized (mLock) {
+ mActive = active;
+ mLock.notifyAll();
+ }
+ }
+
+ /**
+ * Sets the frame data received from the camera. This adds the previous unused frame buffer
+ * (if present) back to the camera, and keeps a pending reference to the frame data for
+ * future use.
+ */
+ void setNextFrame(byte[] data, Camera camera) {
+ synchronized (mLock) {
+ if (mPendingFrameData != null) {
+ camera.addCallbackBuffer(mPendingFrameData.array());
+ mPendingFrameData = null;
+ }
+
+ if (!mBytesToByteBuffer.containsKey(data)) {
+ Log.d(TAG,
+ "Skipping frame. Could not find ByteBuffer associated with the image " +
+ "data from the camera.");
+ return;
+ }
+
+ // Timestamp and frame ID are maintained here, which will give downstream code some
+ // idea of the timing of frames received and when frames were dropped along the way.
+ mPendingTimeMillis = SystemClock.elapsedRealtime() - mStartTimeMillis;
+ mPendingFrameId++;
+ mPendingFrameData = mBytesToByteBuffer.get(data);
+
+ // Notify the processor thread if it is waiting on the next frame (see below).
+ mLock.notifyAll();
+ }
+ }
+
+ /**
+ * As long as the processing thread is active, this executes detection on frames
+ * continuously. The next pending frame is either immediately available or hasn't been
+ * received yet. Once it is available, we transfer the frame info to local variables and
+ * run detection on that frame. It immediately loops back for the next frame without
+ * pausing.
+ *
+ * If detection takes longer than the time in between new frames from the camera, this will
+ * mean that this loop will run without ever waiting on a frame, avoiding any context
+ * switching or frame acquisition time latency.
+ *
+ * If you find that this is using more CPU than you'd like, you should probably decrease the
+ * FPS setting above to allow for some idle time in between frames.
+ */
+ @Override
+ public void run() {
+ Frame outputFrame;
+ ByteBuffer data;
+
+ while (true) {
+ synchronized (mLock) {
+ while (mActive && (mPendingFrameData == null)) {
+ try {
+ // Wait for the next frame to be received from the camera, since we
+ // don't have it yet.
+ mLock.wait();
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Frame processing loop terminated.", e);
+ return;
+ }
+ }
+
+ if (!mActive) {
+ // Exit the loop once this camera source is stopped or released. We check
+ // this here, immediately after the wait() above, to handle the case where
+ // setActive(false) had been called, triggering the termination of this
+ // loop.
+ return;
+ }
+
+ outputFrame = new Frame.Builder()
+ .setImageData(mPendingFrameData, mPreviewSize.getWidth(),
+ mPreviewSize.getHeight(), ImageFormat.NV21)
+ .setId(mPendingFrameId)
+ .setTimestampMillis(mPendingTimeMillis)
+ .setRotation(mRotation)
+ .build();
+
+ // Hold onto the frame data locally, so that we can use this for detection
+ // below. We need to clear mPendingFrameData to ensure that this buffer isn't
+ // recycled back to the camera before we are done using that data.
+ data = mPendingFrameData;
+ mPendingFrameData = null;
+ }
+
+ // The code below needs to run outside of synchronization, because this will allow
+ // the camera to add pending frame(s) while we are running detection on the current
+ // frame.
+
+ try {
+ mDetector.receiveFrame(outputFrame);
+ } catch (Throwable t) {
+ Log.e(TAG, "Exception thrown from receiver.", t);
+ } finally {
+ mCamera.addCallbackBuffer(data.array());
+ }
+ }
+ }
+ }
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/CameraSourcePreview.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/CameraSourcePreview.java
new file mode 100644
index 0000000..070fbf1
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/CameraSourcePreview.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ca.dungeons.sensordump;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.support.annotation.RequiresPermission;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+
+import com.google.android.gms.common.images.Size;
+
+import java.io.IOException;
+
+public class CameraSourcePreview extends ViewGroup {
+ private static final String TAG = "CameraSourcePreview";
+
+ private Context mContext;
+ private SurfaceView mSurfaceView;
+ private boolean mStartRequested;
+ private boolean mSurfaceAvailable;
+ private CameraSource mCameraSource;
+
+ private GraphicOverlay mOverlay;
+
+ public CameraSourcePreview(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ mStartRequested = false;
+ mSurfaceAvailable = false;
+
+ mSurfaceView = new SurfaceView(context);
+ mSurfaceView.getHolder().addCallback(new SurfaceCallback());
+ addView(mSurfaceView);
+ }
+
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public void start(CameraSource cameraSource) throws IOException, SecurityException {
+ if (cameraSource == null) {
+ stop();
+ }
+
+ mCameraSource = cameraSource;
+
+ if (mCameraSource != null) {
+ mStartRequested = true;
+ startIfReady();
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public void start(CameraSource cameraSource, GraphicOverlay overlay) throws IOException, SecurityException {
+ mOverlay = overlay;
+ start(cameraSource);
+ }
+
+ public void stop() {
+ if (mCameraSource != null) {
+ mCameraSource.stop();
+ }
+ }
+
+ public void release() {
+ if (mCameraSource != null) {
+ mCameraSource.release();
+ mCameraSource = null;
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.CAMERA)
+ private void startIfReady() throws IOException, SecurityException {
+ if (mStartRequested && mSurfaceAvailable) {
+ mCameraSource.start(mSurfaceView.getHolder());
+ if (mOverlay != null) {
+ Size size = mCameraSource.getPreviewSize();
+ int min = Math.min(size.getWidth(), size.getHeight());
+ int max = Math.max(size.getWidth(), size.getHeight());
+ if (isPortraitMode()) {
+ // Swap width and height sizes when in portrait, since it will be rotated by
+ // 90 degrees
+ mOverlay.setCameraInfo(min, max, mCameraSource.getCameraFacing());
+ } else {
+ mOverlay.setCameraInfo(max, min, mCameraSource.getCameraFacing());
+ }
+ mOverlay.clear();
+ }
+ mStartRequested = false;
+ }
+ }
+
+ private class SurfaceCallback implements SurfaceHolder.Callback {
+ @Override
+ public void surfaceCreated(SurfaceHolder surface) {
+ mSurfaceAvailable = true;
+ try {
+ startIfReady();
+ } catch (SecurityException se) {
+ Log.e(TAG,"Do not have permission to start the camera", se);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not start camera source.", e);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder surface) {
+ mSurfaceAvailable = false;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int width = 320;
+ int height = 240;
+ if (mCameraSource != null) {
+ Size size = mCameraSource.getPreviewSize();
+ if (size != null) {
+ width = size.getWidth();
+ height = size.getHeight();
+ }
+ }
+
+ // Swap width and height sizes when in portrait, since it will be rotated 90 degrees
+ if (isPortraitMode()) {
+ int tmp = width;
+ //noinspection SuspiciousNameCombination
+ width = height;
+ height = tmp;
+ }
+
+ final int layoutWidth = right - left;
+ final int layoutHeight = bottom - top;
+
+ // Computes height and width for potentially doing fit width.
+ int childWidth = layoutWidth;
+ int childHeight = (int)(((float) layoutWidth / (float) width) * height);
+
+ // If height is too tall using fit width, does fit height instead.
+ if (childHeight > layoutHeight) {
+ childHeight = layoutHeight;
+ childWidth = (int)(((float) layoutHeight / (float) height) * width);
+ }
+
+ for (int i = 0; i < getChildCount(); ++i) {
+ getChildAt(i).layout(0, 0, childWidth, childHeight);
+ }
+
+ try {
+ startIfReady();
+ } catch (SecurityException se) {
+ Log.e(TAG,"Do not have permission to start the camera", se);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not start camera source.", e);
+ }
+ }
+
+ private boolean isPortraitMode() {
+ int orientation = mContext.getResources().getConfiguration().orientation;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ return false;
+ }
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ return true;
+ }
+
+ Log.d(TAG, "isPortraitMode returning false by default");
+ return false;
+ }
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/DatabaseHelper.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/DatabaseHelper.java
new file mode 100644
index 0000000..caa5c81
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/DatabaseHelper.java
@@ -0,0 +1,106 @@
+package ca.dungeons.sensordump;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.content.Context;
+import android.content.ContentValues;
+import android.util.Log;
+import org.json.JSONObject;
+
+
+ /**
+ * A class to buffer generated data to a dataBase for later upload.
+ * @author Gurtok.
+ * @version First version of ESD dataBase helper.
+ */
+class DatabaseHelper extends SQLiteOpenHelper{
+
+ /** Main database name */
+ private static final String DATABASE_NAME = "dbStorage";
+
+ /** Database version. */
+ private static final int DATABASE_VERSION = 1;
+
+ /** Table name for database. */
+ private static final String TABLE_NAME = "StorageTable";
+
+ /** Json data column name. */
+ private static final String dataColumn = "JSON";
+
+ /** Since we only have one database, we reference it on creation. */
+ private final SQLiteDatabase writableDatabase = this.getWritableDatabase();
+
+ /** Used to keep track of the database row we are working on. */
+ private int deleteRowId;
+
+ /** Default constructor.
+ * Creates a new dataBase if required.
+ * @param context Calling method context.
+ */
+ DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ String query = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (ID INTEGER PRIMARY KEY, JSON TEXT);";
+ writableDatabase.execSQL( query );
+ }
+
+ /** Get number of database entries.*/
+ long databaseEntries(){
+ return DatabaseUtils.queryNumEntries( writableDatabase, DatabaseHelper.TABLE_NAME, null );
+ }
+
+ /**
+ * @param db Existing dataBase.
+ * @param oldVersion Old version number ID.
+ * @param newVersion New version number ID.
+ */
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+ }
+
+ /** Required over-ride method. Not currently used.*/
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ }
+
+ /**
+ * Pack the json object into a content values object for shipping.
+ * Insert completed json object into dataBase.
+ * Key will autoIncrement.
+ * Will also start a background thread to upload the database to Kibana.
+ * @param jsonObject Passed object to be inserted.
+ */
+ void JsonToDatabase(JSONObject jsonObject){
+ ContentValues values = new ContentValues();
+ values.put(dataColumn, jsonObject.toString() );
+ long checkDB = writableDatabase.insert( TABLE_NAME, null, values);
+
+ if(checkDB == -1){
+ Log.e("Failed insert","Failed insert database.");
+
+ }
+
+ }
+
+ /** Delete top row from the database. */
+ void deleteJson() {
+ writableDatabase.execSQL( "DELETE FROM " + TABLE_NAME + " WHERE ID = " + deleteRowId );
+ }
+
+ /** Query the database for the next row. Return null if database is empty. */
+ String getNextCursor(){
+
+ if( databaseEntries() >= 1 ){
+ Cursor outCursor = writableDatabase.rawQuery( "SELECT * FROM " + DatabaseHelper.TABLE_NAME + " ORDER BY ID ASC LIMIT 1", new String[]{} );
+ outCursor.moveToFirst();
+ deleteRowId = outCursor.getInt( 0 );
+ String deleteRowString = outCursor.getString(1);
+ outCursor.close();
+ return deleteRowString;
+ }
+ return null;
+ }
+
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/ElasticSearchIndexer.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/ElasticSearchIndexer.java
index dc333e4..6ff1be0 100644
--- a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/ElasticSearchIndexer.java
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/ElasticSearchIndexer.java
@@ -1,257 +1,246 @@
package ca.dungeons.sensordump;
-import android.content.SharedPreferences;
-import android.util.Log;
-import org.json.JSONException;
-import org.json.JSONObject;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
+import java.io.DataOutputStream;
import java.net.Authenticator;
-import java.net.HttpURLConnection;
import java.net.PasswordAuthentication;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
import java.net.URL;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-
-class ElasticSearchIndexer {
-
- private static int MAX_FAILED_DOCS = 1000;
- private static int LAST_RESPONSE_CODE = 299;
-
- long failedIndex = 0;
- long indexRequests = 0;
- long indexSuccess = 0;
- private String esHost;
- private String esPort;
- private String esIndex;
- private String esType;
- private String esUsername;
- private String esPassword;
- private boolean esSSL;
-
- // We store all the failed index operations here, so we can replay them
- // at a later time. This is to handle occasional disconnects in areas where
- // we may not have data or connection to the carrier network.
- private List failedJSONDocs = new ArrayList<>();
- private boolean isLastIndexSuccessful = false;
-
- // Control variable to prevent sensors from being written before mapping created
- // Multi-threading is fun :(
- private boolean isCreatingMapping = true;
-
- // Another control variable, because threading is hate.
- private boolean isRetryingFailedIndexes = false;
-
-
- ElasticSearchIndexer() {
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import java.io.IOException;
+
+import org.json.JSONObject;
+import org.json.JSONException;
+
+
+/**
+* Elastic Search Indexer.
+* Use this thread to upload data to the elastic server.
+*/
+class ElasticSearchIndexer extends Thread{
+
+ /** Used to identify which class is writing to logCat. */
+ private final String logTag = "eSearchIndexer";
+
+ /** The context of the service manager. */
+ private final Context passedContext;
+
+ /** Elastic username. */
+ String esUsername = "";
+
+ /** Elastic password. */
+ String esPassword = "";
+
+ /** Used to establish outside connection. */
+ private HttpURLConnection httpCon;
+
+ /** Connection fail count. When this hits 10, cancel the upload thread. */
+ private int connectFailCount = 0;
+
+ /** The URL we use to post data to the server. */
+ URL postUrl;
+
+ /** The URL we use to create an index and PUT a mapping schema on it. */
+ URL mapUrl;
+
+ /** Variable to keep track if this instance of the indexer has submitted a map. */
+ private boolean alreadySentMapping;
+
+ /** A variable to hold the JSON string to be uploaded. */
+ String uploadString = "";
+
+ /** Base constructor. */
+ ElasticSearchIndexer( Context context ){
+ passedContext = context;
}
- void updateURL(SharedPreferences sharedPrefs) {
- // Extract config information to build connection strings
- esHost = sharedPrefs.getString("host", "localhost");
- esPort = sharedPrefs.getString("port", "9200");
- esIndex = sharedPrefs.getString("index", "sensor_dump");
- esType = sharedPrefs.getString("type", "phone_data");
- esSSL = sharedPrefs.getBoolean("ssl", false);
- esUsername = sharedPrefs.getString("user", "");
- esPassword = sharedPrefs.getString("pass", "");
-
- // Tag the current date stamp on the index name if set in preferences
- // Thanks GlenRSmith for this idea
- if (sharedPrefs.getBoolean("index_date", false)) {
- Date logDate = new Date(System.currentTimeMillis());
- SimpleDateFormat logDateFormat = new SimpleDateFormat("yyyyMMdd");
- String dateString = logDateFormat.format(logDate);
- esIndex = esIndex + "-" + dateString;
+ /** This run method is executed upon each index start. */
+ @Override
+ public void run() {
+ if( !alreadySentMapping ){
+ createMapping();
}
+ if( !uploadString.equals("") && alreadySentMapping ){
+ index( uploadString );
+ }
}
- // Stop/start should reset counters
- public void resetCounters() {
- failedIndex = 0;
- indexRequests = 0;
- indexSuccess = 0;
+ /** Send messages to Upload thread and ESD service thread to indicate result of index.*/
+ private void indexSuccess( boolean result ){
+
+ Intent messageIntent;
+
+ messageIntent = new Intent( Uploads_Receiver.INDEX_SUCCESS );
+ messageIntent.putExtra("INDEX_SUCCESS", result );
+ passedContext.sendBroadcast( messageIntent );
+
+ messageIntent = new Intent( EsdServiceReceiver.INDEX_SUCCESS );
+ messageIntent.putExtra("INDEX_SUCCESS", result );
+ passedContext.sendBroadcast( messageIntent );
}
- private void callElasticAPI(final String verb, final String url,
- final String jsonData, final boolean isBulk) {
- indexRequests++;
- // Send authentication if required
- if (esUsername.length() > 0 && esPassword.length() > 0) {
- Authenticator.setDefault(new Authenticator() {
- protected PasswordAuthentication getPasswordAuthentication() {
- return new PasswordAuthentication(esUsername, esPassword.toCharArray());
+ /** Create a map and send to elastic for sensor index. */
+ private void createMapping() {
+ Log.e(logTag + " newMap", "Mapping uploading." );
+ // Connect to elastic using PUT to make elastic understand this is a mapping.
+ if( connect("PUT") ) {
+ try {
+ DataOutputStream dataOutputStream = new DataOutputStream(httpCon.getOutputStream());
+
+ // Lowest json level, contains explicit typing of sensor data.
+ JSONObject mappingTypes = new JSONObject();
+ // Type "start_location" && "location" using pre-defined typeGeoPoint. ^^
+ mappingTypes.put("start_location", new JSONObject().put("type", "geo_point" ));
+ mappingTypes.put("location", new JSONObject().put("type", "geo_point" ));
+ // Put the two newly typed fields under properties.
+ JSONObject properties = new JSONObject().put("properties", mappingTypes);
+
+ // Mappings should be nested under index_type.
+ JSONObject esTypeObj = new JSONObject().put( "esd", properties);
+ // File this new properties json under _mappings.
+ JSONObject mappings = new JSONObject().put("mappings", esTypeObj);
+
+ // Write out to elastic using the passed outputStream that is connected.
+ dataOutputStream.writeBytes( mappings.toString() );
+
+ if ( checkResponseCode() ) {
+ alreadySentMapping = true;
+ } else {
+ // Send message to upload thread about the failure to upload via intent.
+ Log.e(logTag + " newMap", "Failed response code check on MAPPING. " + mappings.toString());
}
- });
+
+ } catch (JSONException j) {
+ Log.e(logTag + " newMap", "JSON error: " + j.toString());
+ } catch (IOException IoEx) {
+ Log.e(logTag + " newMap", "Failed to write to outputStreamWriter.");
+ }
+
+ httpCon.disconnect();
+ }else{
+ Log.e(logTag, "Connection is bad." );
}
+ Log.e(logTag, "Finished mapping." );
+ }
- // Spin up a thread for http connection
- Runnable r = new Runnable() {
- public void run() {
-
- HttpURLConnection httpCon;
- OutputStreamWriter osw;
- URL u;
-
- try {
- u = new URL(url);
- httpCon = (HttpURLConnection) u.openConnection();
- httpCon.setConnectTimeout(2000);
- httpCon.setReadTimeout(2000);
- httpCon.setDoOutput(true);
- httpCon.setRequestMethod(verb);
- osw = new OutputStreamWriter(httpCon.getOutputStream());
- osw.write(jsonData);
- osw.close();
- httpCon.getInputStream();
-
- // Something bad happened. I expect only the finest of 200's
- int responseCode = httpCon.getResponseCode();
- if (responseCode > LAST_RESPONSE_CODE) {
- if (!isCreatingMapping) {
- failedIndex++;
- isLastIndexSuccessful = false;
- }
- } else {
- isLastIndexSuccessful = true;
- indexSuccess++;
- }
+ /** Send JSON data to elastic using POST. */
+ private void index( String uploadString ) {
+ //Log.e(logTag+" index", "Index STARTED: " + uploadString );
+
+ // Boolean return to check if we successfully connected to the elastic host.
+ if( connect("POST") ){
+ // POST our documents to elastic.
+ try {
+ DataOutputStream dataOutputStream = new DataOutputStream( httpCon.getOutputStream() );
+ dataOutputStream.writeBytes( uploadString );
+ // Check status of post operation.
+ if( checkResponseCode() ){
+ indexSuccess( true );
+ }else{
+ Log.e(logTag+" esIndex.", "Uploaded string FAILURE!" );
+ indexSuccess( false );
+ }
+ }catch( IOException IOex ){
+ // Error writing to httpConnection.
+ Log.e( logTag+" esIndex.", IOex.getMessage() );
+ }
+ httpCon.disconnect();
+ }
+ }
- httpCon.disconnect();
+ /** Open a connection with the server. */
+ private boolean connect(String verb){
+ //Log.e( logTag+" connect.", "Connecting." );
+ if( connectFailCount == 0 || connectFailCount % 10 != 0 ){
- } catch (Exception e) {
-
- // Probably a connection error. Maybe. Lets just buffer up the json
- // docs so we can try them again later
- if (e instanceof IOException) {
- if (!isCreatingMapping && !isBulk) {
- isLastIndexSuccessful = false;
- // Store up to MAX_FAILED_DOCS worth of data before dumping it
- if(failedJSONDocs.size() < MAX_FAILED_DOCS) {
- failedJSONDocs.add(jsonData);
- }
- }
+ // Send authentication if required
+ if (esUsername.length() > 0 && esPassword.length() > 0) {
+ Authenticator.setDefault(new Authenticator() {
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(esUsername, esPassword.toCharArray());
}
+ });
+ }
- // Only show errors for index requests, not the mapping request
- if (isCreatingMapping) {
- isCreatingMapping = false;
- } else {
- Log.v("Index Request", "" + indexRequests);
- Log.v("Fail Reason", e.toString());
- Log.v("Fail URL", url);
- Log.v("Fail Data", jsonData);
- failedIndex++;
- }
- }
- // We are no longer creating the mapping. Time for sensor readings!
- if (isCreatingMapping) {
- isCreatingMapping = false;
- }
+ // Establish connection.
+ try {
- // Bulk success!
- if (isBulk) {
- // Clear failed log and update our stats
- failedIndex -= failedJSONDocs.size();
- indexSuccess += failedJSONDocs.size();
- failedJSONDocs.clear();
- isRetryingFailedIndexes = false;
+ if( verb.equals("PUT") ) {
+ httpCon = (HttpURLConnection) mapUrl.openConnection();
+ }else{
+ httpCon = (HttpURLConnection) postUrl.openConnection();
}
- }
- };
- // Only allow posts if we're not creating mapping
- if (isCreatingMapping) {
- if (verb.equals("PUT")) {
- Thread t = new Thread(r);
- t.start();
+ httpCon.setConnectTimeout(2000);
+ httpCon.setReadTimeout(2000);
+ httpCon.setDoOutput(true);
+ httpCon.setDoInput(true);
+ httpCon.setRequestMethod( verb );
+ httpCon.connect();
+
+ // Reset the failure count.
+ connectFailCount = 0;
+ return true;
+ }catch(MalformedURLException urlEx){
+ Log.e( logTag+" connect.", "Error building URL.");
+ connectFailCount++;
+ }catch (IOException IOex) {
+ Log.e( logTag+" connect.", "Failed to connect to elastic. " + IOex.getMessage() + " " + IOex.getCause());
+ connectFailCount++;
}
- } else {
- // We're not creating a mapping, just go nuts
- Thread t = new Thread(r);
- t.start();
+ }else{
+ Log.e(logTag, "Failure to connect. Aborting!" );
}
+ // If it got this far, it failed.
+ return false;
}
- // Build the URL based on the config data
- private String buildURL() {
- if (esSSL) {
- return "https://" + esHost + ":" + esPort + "/" + esIndex + "/";
- } else {
- return "http://" + esHost + ":" + esPort + "/" + esIndex + "/";
- }
- }
+ /** Helper class to determine if an individual indexing operation was successful.
+ * "I expect only the finest of 200s" - Ademara*/
+ private boolean checkResponseCode(){
- // Send mapping to elastic for sensor index using PUT
- private void createMapping() {
+ String responseMessage = "ResponseCode placeholder.";
+ int responseCode = 0;
- JSONObject mappings = new JSONObject();
- try {
- JSONObject typeGeoPoint = new JSONObject().put("type", "geo_point");
- JSONObject mappingTypes = new JSONObject().put("start_location", typeGeoPoint);
- mappingTypes.put("location", typeGeoPoint);
- JSONObject properties = new JSONObject().put("properties",mappingTypes);
- JSONObject indexType = new JSONObject().put(esType, properties);
- mappings = new JSONObject().put("mappings", indexType);
- } catch (JSONException j) {
- Log.v("Json Exception", j.toString());
- }
-
- Log.v("Mapping", mappings.toString());
+ try{
- callElasticAPI("PUT", buildURL(), mappings.toString() , false);
- }
+ responseMessage = httpCon.getResponseMessage();
+ responseCode = httpCon.getResponseCode();
- // Spam those failed docs!
- // Maybe this should be a bulk operation... one day
- private void indexFailedDocuments() {
- String url;
- StringBuilder bulkDataList = new StringBuilder();
-
- // Bulk index url
- if (esSSL) {
- url = "https://" + esHost + ":" + esPort + "/_bulk";
- } else {
- url = "http://" + esHost + ":" + esPort + "/_bulk";
- }
+ if (200 <= responseCode && responseCode <= 299 || responseCode == 400 ) {
+ //Log.e( logTag, "Success with response code: " + responseMessage + responseCode );
- for (String failedJsonDoc : failedJSONDocs) {
- bulkDataList.append("{\"index\":{\"_index\":\"").append(esIndex)
- .append("\",\"_type\":\"").append(esType).append("\"}}\n");
- bulkDataList.append(failedJsonDoc).append("\n");
- }
+ if( responseCode == 400 ){
+ Log.e( logTag, "Index already exists. Skipping map." );
+ }
- String bulkData = bulkDataList.toString();
+ httpCon.disconnect();
+ return true;
+ }else{
+ throw new IOException( "" );
+ }
- Log.v("Bulk Data", bulkData);
- callElasticAPI("POST", url, bulkData, true);
- }
+ }catch( IOException ioEx ){
- // Send JSON data to elastic using POST
- void index(JSONObject joIndex) {
+ // Something bad happened. I expect only the finest of 200's
+ Log.e( logTag+" response", String.format("%s%s\n%s%s\n%s",
+ "Bad response code: ", responseCode,
+ "Response Message: ", responseMessage,
+ httpCon.getURL() + " request type: " + httpCon.getRequestMethod() )// End string.
+ );
- // Create the mapping on first request
- if (isCreatingMapping && indexRequests == 0) {
- createMapping();
- }
+ }
+
+ httpCon.disconnect();
+ return false;
+ }
- String jsonData = joIndex.toString();
- String url = buildURL() + esType + "/";
- // If we have some data, it's good to post
- if (jsonData != null) {
- callElasticAPI("POST", url, jsonData, false);
- }
- // Try it again!
- if (isLastIndexSuccessful && failedJSONDocs.size() > 0 && !isRetryingFailedIndexes) {
- isRetryingFailedIndexes = true;
- indexFailedDocuments();
- }
- }
}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/EsdServiceManager.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/EsdServiceManager.java
new file mode 100644
index 0000000..a627238
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/EsdServiceManager.java
@@ -0,0 +1,265 @@
+package ca.dungeons.sensordump;
+
+import android.app.Service;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class EsdServiceManager extends Service {
+
+ /** String to identify this class in LogCat. */
+ private static final String logTag = "EsdServiceManager";
+
+ /** Android connection manager. Use to find out if we are connected before doing any networking. */
+ private ConnectivityManager connectionManager;
+
+ /** Uploads controls the data flow between the local database and Elastic server. */
+ private Uploads_Receiver uploadsReceiver;
+
+ /** Main activity preferences. Holds URL and name data. */
+ private SharedPreferences sharedPrefs;
+
+ /** */
+ private EsdServiceReceiver esdMessageReceiver;
+
+ /** True if we are currently reading sensor data. */
+ boolean logging = false;
+
+ /** Toggle, if we should be recording AUDIO sensor data. */
+ boolean audioLogging = false;
+
+ /** Toggle, if we should be recording GPS data.*/
+ boolean gpsLogging = false;
+
+ /** The rate in milliseconds we record sensor data. */
+ int sensorRefreshRate = 250;
+
+ /** Toggle, if this service is currently running. Used by the main activity. */
+ private boolean serviceActive = false;
+
+ /** Time of the last sensor recording. Used to shut down unused resources. */
+ private long lastSuccessfulSensorResult;
+
+ /** Number of sensor readings this session. */
+ public int sensorReadings = 0;
+
+ /** Number of audio readings this session. */
+ public int audioReadings = 0;
+
+ /** Number of gps locations recorded this session */
+ public int gpsReadings = 0;
+
+ /** Number of documents indexed to Elastic this session. */
+ public int documentsIndexed = 0;
+
+ /** Number of data uploaded failures this session. */
+ public int uploadErrors = 0;
+
+ /** This thread pool is the working pool. Use this to execute the sensor runnable and Uploads. */
+ private final ExecutorService workingThreadPool = Executors.newFixedThreadPool( 4 );
+
+ /** This thread pool handles the timer in which we control this service.
+ * Timer that controls if/when we should be uploading data to the server.
+ */
+ private final ScheduledExecutorService timerPool = Executors.newScheduledThreadPool( 2 );
+
+ /** This is the runnable we will use to check network connectivity once every 30 min. */
+ private final Runnable uploadRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if( !uploadsReceiver.isWorking() && connectionManager.getActiveNetworkInfo().isConnected() ){
+ //Log.e(logTag, "Submitting upload thread.");
+ Intent uploadStartIntent = new Intent( Uploads_Receiver.START_UPLOAD_THREAD );
+ sendBroadcast( uploadStartIntent );
+ }else if( uploadsReceiver.isWorking() ){
+ Log.e(logTag, "Uploading already in progress." );
+ }else{
+ Log.e(logTag, "Failed to submit uploads runnable to thread pool!" );
+ }
+
+ }
+ };
+
+ /**
+ * Service Timeout timer runnable.
+ * If we go more than an a half hour without recording any sensor data, shut down this thread.
+ */
+ private final Runnable serviceTimeoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ // Last sensor result plus 1/2 hour in milliseconds is greater than the current time.
+ boolean timeCheck = lastSuccessfulSensorResult + ( 1000*60*30 ) > System.currentTimeMillis();
+
+ if( !logging && !uploadsReceiver.isWorking() && !timeCheck ){
+ Log.e( logTag, "Shutting down service. Not logging!" );
+ EsdServiceManager.super.stopSelf();
+ }
+ }
+ };
+
+ /**
+ * Default constructor:
+ * Instantiate the class broadcast receiver and messageFilters.
+ * Register receiver to make sure we can communicate with the other threads.
+ */
+ @Override
+ public void onCreate () {
+ sharedPrefs = PreferenceManager.getDefaultSharedPreferences( this.getBaseContext() );
+
+ esdMessageReceiver = new EsdServiceReceiver( this );
+ registerReceiver( esdMessageReceiver, esdMessageReceiver.messageFilter );
+ }
+
+ /**
+ * Runs when the mainActivity executes this service.
+ * @param intent - Not used.
+ * @param flags - Not used.
+ * @param startId - Name of mainActivity.
+ * @return START_STICKY will make sure the OS restarts this process if it has to trim memory.
+ */
+ @Override
+ public int onStartCommand (Intent intent, int flags, int startId){
+ //Log.e(logTag, "ESD -- On Start Command." );
+ if( !serviceActive ){
+
+ updateUiData();
+ lastSuccessfulSensorResult = System.currentTimeMillis();
+ connectionManager = (ConnectivityManager) getSystemService( CONNECTIVITY_SERVICE );
+
+ /* Use SensorRunnable class to start the logging process. */
+ SensorRunnable sensorRunnable = new SensorRunnable( this, sharedPrefs );
+ workingThreadPool.submit( sensorRunnable );
+
+ /* Create an instance of Uploads, and submit to the thread pool to begin execution. */
+ uploadsReceiver = new Uploads_Receiver( this, sharedPrefs, workingThreadPool );
+ uploadsReceiver.registerMessageReceiver();
+
+ /* Schedule periodic checks for internet connectivity. */
+ setupUploads();
+ /* Schedule periodic checks for service shutdown due to inactivity. */
+ setupManagerTimeout();
+
+ /* Send a message to the main thread to indicate the manager service has been initialized. */
+ serviceActive = true;
+
+ Log.i(logTag, "Started service manager.");
+ }
+ // If the service is shut down, do not restart it automatically.
+ return Service.START_NOT_STICKY;
+ }
+
+ /** This method uses the passed UI handler to relay messages if/when the activity is running. */
+ synchronized void updateUiData(){
+
+ if( getApplicationContext() != null ){
+
+ Intent outIntent = new Intent( MainActivity.UI_SENSOR_COUNT);
+
+ outIntent.putExtra( "sensorReadings", sensorReadings );
+ outIntent.putExtra( "gpsReadings", gpsReadings );
+ outIntent.putExtra( "audioReadings", audioReadings );
+
+ outIntent.putExtra( "documentsIndexed", documentsIndexed );
+ outIntent.putExtra( "uploadErrors", uploadErrors );
+
+ sendBroadcast( outIntent );
+ }
+ }
+
+
+ /** Timer used to periodically check if the upload runnable needs to be executed. */
+ private void setupUploads() {
+ timerPool.scheduleAtFixedRate( uploadRunnable, 10, 30, TimeUnit.SECONDS );
+ } // Delay the task 10 seconds out and then repeat every 30 seconds.
+
+ /** Timer used to periodically check if this service is being used (recording data). */
+ private void setupManagerTimeout(){
+ timerPool.scheduleAtFixedRate( serviceTimeoutRunnable, 30, 30, TimeUnit.MINUTES );
+ } // Delay the task 60 min out. Then repeat once every 60 min.
+
+ /**
+ * Start logging method:
+ * Send toggle requests to the sensor thread receiver.
+ * 1. SENSOR toggle.
+ * 2. GPS toggle.
+ * 3. AUDIO toggle.
+ */
+ public void startLogging() {
+ logging = true;
+
+ Intent messageIntent = new Intent( SensorRunnable.SENSOR_POWER );
+ messageIntent.putExtra( "sensorPower", logging );
+ sendBroadcast( messageIntent );
+
+ if( gpsLogging ){
+ messageIntent = new Intent( SensorRunnable.GPS_POWER );
+ messageIntent.putExtra("gpsPower", gpsLogging );
+ sendBroadcast( messageIntent );
+ }
+
+ if( audioLogging ){
+ messageIntent = new Intent( SensorRunnable.AUDIO_POWER );
+ messageIntent.putExtra("audioPower", audioLogging );
+ sendBroadcast( messageIntent );
+ }
+
+ }
+
+ /**
+ * Stop logging method:
+ * 1. Unregister listeners for both sensors and battery.
+ * 2. Turn gps recording off.
+ * 3. Update main thread to initialize UI changes.
+ */
+ public void stopLogging() {
+ logging = false;
+ Intent messageIntent = new Intent( SensorRunnable.SENSOR_POWER );
+ messageIntent.putExtra( "sensorPower", logging );
+ sendBroadcast( messageIntent );
+ }
+
+ /**
+ * This runs when the service either shuts itself down or the OS trims memory.
+ * StopLogging() stops all sensor logging.
+ * Unregister the Upload broadcast receiver.
+ * Sends a message to the UI and UPLOAD receivers that we have shut down.
+ */
+ @Override
+ public void onDestroy () {
+
+ stopLogging();
+ this.unregisterReceiver( esdMessageReceiver );
+ uploadsReceiver.unRegisterUploadReceiver();
+
+ Intent messageIntent = new Intent( MainActivity.UI_ACTION_RECEIVER );
+ messageIntent.putExtra( "serviceManagerRunning", false );
+ sendBroadcast( messageIntent );
+
+ messageIntent = new Intent( Uploads_Receiver.STOP_UPLOAD_THREAD );
+ sendBroadcast( messageIntent );
+
+ super.onDestroy();
+ }
+
+ /** Not used as of yet. */
+ @Nullable
+ @Override
+ public IBinder onBind (Intent intent ){
+ return new Binder();
+ }
+
+
+}
+
+
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/EsdServiceReceiver.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/EsdServiceReceiver.java
new file mode 100644
index 0000000..4de4f54
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/EsdServiceReceiver.java
@@ -0,0 +1,158 @@
+package ca.dungeons.sensordump;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+
+ /**
+ * Created by Gurtok on 9/19/2017.
+ * Broadcast receiver for the EsdServiceManager.
+ */
+public class EsdServiceReceiver extends BroadcastReceiver {
+
+ /** ID this class in LogCat. */
+ private static final String logTag = "EsdServiceReceiver";
+
+ /** Instance of ESD service manager. */
+ private final EsdServiceManager esdServiceManager;
+
+ /** Filter for the broadcast receiver. */
+ final IntentFilter messageFilter = new IntentFilter();
+
+/* Sensor toggles. */
+ /** Intent action address: Boolean - If we are recording PHONE sensor data. */
+ public final static String SENSOR_MESSAGE = "esd.intent.action.message.SENSOR";
+
+ /** Intent action address: Boolean - If we are recording GPS sensor data. */
+ public final static String GPS_MESSAGE = "esd.intent.action.message.GPS";
+
+ /** Intent action address: Boolean - If we are recording AUDIO sensor data. */
+ public final static String AUDIO_MESSAGE = "esd.intent.action.message.AUDIO";
+
+/* Interval rate change from main UI. */
+ /** Intent action address: integer - Rate change from user. */
+ public final static String INTERVAL = "esd.intent.action.message.INTERVAL";
+
+/* Update UI. */
+ /** Intent action address: String - Call for the service to update the UI thread data records. */
+ private final static String UPDATE_UI_display = "esd.intent.action.message.UPDATE_UI_display";
+
+/* Update counts. */
+ /** Intent action address: Boolean - Indicate if the current index attempt was successful. */
+ public final static String INDEX_SUCCESS = "esd.intent.action.message.Uploads.INDEX_SUCCESS";
+
+/* Sensor readings. */
+ /** Intent action address: Boolean - Used by SensorListener to update EsdServiceManagers data. */
+ public final static String SENSOR_SUCCESS = "esd.intent.action.message.SensorListener.SENSOR_SUCCESS";
+
+ /** Default constructor:
+ * This class is instantiated by the service manager, thus it passes itself to this class. */
+ public EsdServiceReceiver( EsdServiceManager passedManagerObj ) {
+ esdServiceManager = passedManagerObj;
+ addFilters();
+ }
+
+ /** Assembles the message filter for this receiver. */
+ private void addFilters(){
+ messageFilter.addAction( SENSOR_MESSAGE );
+ messageFilter.addAction( GPS_MESSAGE );
+ messageFilter.addAction( AUDIO_MESSAGE );
+ messageFilter.addAction( INTERVAL );
+
+ messageFilter.addAction(UPDATE_UI_display);
+
+ messageFilter.addAction( INDEX_SUCCESS );
+ messageFilter.addAction( SENSOR_SUCCESS );
+ }
+
+ /** Main point of contact for the service manager.
+ * All information and requests are handled here.
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Intent messageIntent = new Intent();
+
+ switch( intent.getAction() ){
+
+ case (SENSOR_MESSAGE):
+ if (intent.getBooleanExtra("sensorPower", true)) {
+ esdServiceManager.startLogging();
+ } else {
+ esdServiceManager.stopLogging();
+ }
+ break;
+
+ case GPS_MESSAGE:
+ esdServiceManager.gpsLogging = intent.getBooleanExtra("gpsPower", false);
+ messageIntent.setAction( SensorRunnable.GPS_POWER );
+ messageIntent.putExtra("gpsPower", esdServiceManager.gpsLogging);
+ esdServiceManager.sendBroadcast(messageIntent);
+ break;
+
+ case AUDIO_MESSAGE:
+ esdServiceManager.audioLogging = intent.getBooleanExtra("audioPower", false);
+ messageIntent = new Intent(SensorRunnable.AUDIO_POWER);
+ messageIntent.putExtra("audioPower", esdServiceManager.audioLogging);
+ esdServiceManager.sendBroadcast(messageIntent);
+ break;
+
+ case INTERVAL:
+ esdServiceManager.sensorRefreshRate = intent.getIntExtra("sensorInterval", 250);
+ messageIntent = new Intent(SensorRunnable.INTERVAL);
+ messageIntent.putExtra("sensorInterval", esdServiceManager.sensorRefreshRate);
+ esdServiceManager.sendBroadcast(messageIntent);
+ break;
+
+ case UPDATE_UI_display:
+ esdServiceManager.updateUiData();
+ break;
+
+ case INDEX_SUCCESS:
+ if( intent.getBooleanExtra(INDEX_SUCCESS, true) ){
+ esdServiceManager.documentsIndexed++;
+ }else{
+ esdServiceManager.uploadErrors++;
+ }
+ esdServiceManager.updateUiData();
+ break;
+
+ case SENSOR_SUCCESS:
+ if( esdServiceManager.logging && intent.getBooleanExtra( "sensorReading", false) )
+ esdServiceManager.sensorReadings++;
+
+ if ( esdServiceManager.gpsLogging && intent.getBooleanExtra( "gpsReading", false) )
+ esdServiceManager.gpsReadings++;
+
+ if ( esdServiceManager.audioLogging && intent.getBooleanExtra( "audioReading", false) )
+ esdServiceManager.audioReadings++;
+ esdServiceManager.updateUiData();
+ break;
+
+ default:
+ Log.e(logTag , "Received bad information from ACTION intent." );
+ break;
+ }
+ }
+
+
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/Fragment_Preference.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/Fragment_Preference.java
new file mode 100644
index 0000000..4500123
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/Fragment_Preference.java
@@ -0,0 +1,70 @@
+package ca.dungeons.sensordump;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.google.android.gms.common.api.CommonStatusCodes;
+
+public class Fragment_Preference extends PreferenceFragment {
+
+ private static final String TAG = "Preference_Frag";
+
+ static final int QR_REQUEST_CODE = 1232131213;
+ private SharedPreferences sharedPreferences;
+
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences( this.getContext() );
+ addPreferencesFromResource(R.xml.preferences);
+ setupQRButton();
+ }
+
+ private void setupQRButton(){
+ Preference qrPreference = this.getPreferenceManager().findPreference( "qr_code" );
+ qrPreference.setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Intent qr_Intent = new Intent( getContext(), BarcodeMainActivity.class );
+ startActivityForResult( qr_Intent, QR_REQUEST_CODE );
+ return false;
+ }
+ });
+
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+
+ Log.e( TAG, "Received results from QR reader." );
+ if( requestCode == QR_REQUEST_CODE ){
+
+ if( resultCode == CommonStatusCodes.SUCCESS ){
+ Log.e( TAG, "Received SUCCESS CODE" );
+ if( data != null ){
+ Log.e( TAG, "Intent is NOT NULL" );
+ String hostString = data.getStringExtra("hostString" );
+ if( ! hostString.equals("") ){
+ sharedPreferences.edit().putString( "host", hostString ).apply();
+ onCreate( this.getArguments() );
+ }
+ }else{
+ Log.e( TAG, "Supplied intent is null !!" );
+ }
+
+
+
+ }
+
+ }
+ }
+
+
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/GPSLogger.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/GPSLogger.java
index 08010a2..246e03c 100644
--- a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/GPSLogger.java
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/GPSLogger.java
@@ -3,38 +3,61 @@
import android.location.Location;
import android.location.LocationListener;
import android.os.Bundle;
+import android.util.Log;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+ /**
+ * GPS Logger - Location Listener implementation.
+ */
class GPSLogger implements LocationListener {
- boolean gpsHasData = false;
- double gpsLat;
- double gpsLong;
- double gpsAlt;
- float gpsAccuracy;
- float gpsBearing;
- String gpsProvider;
- float gpsSpeed;
- float gpsSpeedKMH;
- float gpsSpeedMPH;
- int gpsUpdates = 0;
- double gpsLatStart;
- double gpsLongStart;
- float gpsAcceleration;
- float gpsAccelerationKMH;
- float gpsAccelerationMPH;
- double gpsDistanceMetres;
- double gpsDistanceFeet;
- double gpsTotalDistance;
- double gpsTotalDistanceKM;
- double gpsTotalDistanceMiles;
+ /** */
+ private String gpsProvider;
+
+ /** */
+ private int gpsUpdates = 0;
+
+ /** */
+ private double gpsLat, gpsLong, gpsAlt;
+
+ /** */
+ private double gpsLatStart, gpsLongStart;
+
+ /** */
+ private double gpsDistanceMetres, gpsDistanceFeet, gpsTotalDistance;
+ /** */
+ private double gpsTotalDistanceKM, gpsTotalDistanceMiles;
+
+ /** */
+ private float gpsAccuracy, gpsBearing;
+
+ /** */
+ private float gpsSpeed, gpsSpeedKMH, gpsSpeedMPH;
+
+ /** */
+ private float gpsAcceleration, gpsAccelerationKMH, gpsAccelerationMPH;
+
+ /** */
private float lastSpeed;
- private double lastLat;
- private double lastLong;
+ /** */
+ private double lastLat, lastLong;
+
+ /** */
+ boolean gpsHasData = false;
+
+ /**
+ * Method to record gps.
+ * @param location Current location.
+ */
@Override
public void onLocationChanged(Location location) {
+ //Log.e(logTag, "GPS logger, onChangeLocation!" );
+
gpsLat = location.getLatitude();
gpsLong = location.getLongitude();
gpsAlt = location.getAltitude();
@@ -44,7 +67,7 @@ public void onLocationChanged(Location location) {
gpsSpeed = location.getSpeed();
// Store the lat/long for the first reading we got
- if (gpsUpdates == 0) {
+ if( gpsUpdates == 0 ){
gpsLatStart = gpsLat;
gpsLongStart = gpsLong;
lastSpeed = gpsSpeed;
@@ -52,8 +75,6 @@ public void onLocationChanged(Location location) {
lastLong = gpsLong;
}
- this.gpsUpdates += 1;
-
// Metre per second is not ideal. Adding km/hr and mph as well
gpsSpeedKMH = gpsSpeed * (float) 3.6;
gpsSpeedMPH = gpsSpeed * (float) 2.23694;
@@ -85,24 +106,58 @@ public void onLocationChanged(Location location) {
// We're live!
gpsHasData = true;
+ gpsUpdates++;
}
- void resetGPS() {
- gpsUpdates = 0;
- }
+ /** Required over ride. Not used. */
@Override
- public void onStatusChanged(String provider, int status, Bundle extras) {
-
- }
+ public void onStatusChanged(String provider, int status, Bundle extras){}
+ /** Required over ride. Not used. */
@Override
- public void onProviderEnabled(String provider) {
+ public void onProviderEnabled(String provider){}
+ /** Required over ride. Not used. */
+ @Override
+ public void onProviderDisabled(String provider){}
+
+ /**
+ * Take the passed json object, add the collected gps data.
+ * @param passedJson A reference to the SensorRunnable json file that will be uploaded.
+ * @return The json that now included the gps data.
+ */
+ JSONObject getGpsData( JSONObject passedJson ){
+
+ if( passedJson != null ){
+ try{
+ // Function to update the joSensorData list.
+ passedJson.put("location", "" + gpsLat + "," + gpsLong);
+ passedJson.put("start_location", "" + gpsLatStart + "," + gpsLongStart);
+ passedJson.put("altitude", gpsAlt);
+ passedJson.put("accuracy", gpsAccuracy);
+ passedJson.put("bearing", gpsBearing);
+ passedJson.put("gps_provider", gpsProvider);
+ passedJson.put("speed", gpsSpeed);
+ passedJson.put("speed_kmh", gpsSpeedKMH);
+ passedJson.put("speed_mph", gpsSpeedMPH);
+ passedJson.put("gps_updates", gpsUpdates);
+ passedJson.put("acceleration", gpsAcceleration);
+ passedJson.put("acceleration_kmh", gpsAccelerationKMH);
+ passedJson.put("acceleration_mph", gpsAccelerationMPH);
+ passedJson.put("distance_metres", gpsDistanceMetres);
+ passedJson.put("distance_feet", gpsDistanceFeet);
+ passedJson.put("total_distance_metres", gpsTotalDistance);
+ passedJson.put("total_distance_km", gpsTotalDistanceKM);
+ passedJson.put("total_distance_miles", gpsTotalDistanceMiles);
+ }catch(JSONException JsonEx ){
+ Log.e( "GPSLogger", "Error creating Json. " );
+ return passedJson;
+ }
+ }
+ //Log.e(logTag, "Getting gps data!!");
+ return passedJson;
}
- @Override
- public void onProviderDisabled(String provider) {
- }
}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/GraphicOverlay.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/GraphicOverlay.java
new file mode 100644
index 0000000..7ee8db5
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/GraphicOverlay.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ca.dungeons.sensordump;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.google.android.gms.vision.CameraSource;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Vector;
+
+/**
+ * A view which renders a series of custom graphics to be overlayed on top of an associated preview
+ * (i.e., the camera preview). The creator can add graphics objects, update the objects, and remove
+ * them, triggering the appropriate drawing and invalidation within the view.
+ *
+ * Supports scaling and mirroring of the graphics relative the camera's preview properties. The
+ * idea is that detection items are expressed in terms of a preview size, but need to be scaled up
+ * to the full view size, and also mirrored in the case of the front-facing camera.
+ *
+ * Associated {@link Graphic} items should use the following methods to convert to view coordinates
+ * for the graphics that are drawn:
+ *
+ *
{@link Graphic#scaleX(float)} and {@link Graphic#scaleY(float)} adjust the size of the
+ * supplied value from the preview scale to the view scale.
+ *
{@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the coordinate
+ * from the preview's coordinate system to the view coordinate system.
+ *
+ */
+public class GraphicOverlay extends View {
+ private final Object mLock = new Object();
+ private int mPreviewWidth;
+ private float mWidthScaleFactor = 1.0f;
+ private int mPreviewHeight;
+ private float mHeightScaleFactor = 1.0f;
+ private int mFacing = CameraSource.CAMERA_FACING_BACK;
+ private Set mGraphics = new HashSet<>();
+
+ /**
+ * Base class for a custom graphics object to be rendered within the graphic overlay. Subclass
+ * this and implement the {@link Graphic#draw(Canvas)} method to define the
+ * graphics element. Add instances to the overlay using {@link GraphicOverlay#add(Graphic)}.
+ */
+ public static abstract class Graphic {
+ private GraphicOverlay mOverlay;
+
+ public Graphic(GraphicOverlay overlay) {
+ mOverlay = overlay;
+ }
+
+ /**
+ * Draw the graphic on the supplied canvas. Drawing should use the following methods to
+ * convert to view coordinates for the graphics that are drawn:
+ *
+ *
{@link Graphic#scaleX(float)} and {@link Graphic#scaleY(float)} adjust the size of
+ * the supplied value from the preview scale to the view scale.
+ *
{@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the
+ * coordinate from the preview's coordinate system to the view coordinate system.
+ *
+ *
+ * @param canvas drawing canvas
+ */
+ public abstract void draw(Canvas canvas);
+
+ /**
+ * Adjusts a horizontal value of the supplied value from the preview scale to the view
+ * scale.
+ */
+ public float scaleX(float horizontal) {
+ return horizontal * mOverlay.mWidthScaleFactor;
+ }
+
+ /**
+ * Adjusts a vertical value of the supplied value from the preview scale to the view scale.
+ */
+ public float scaleY(float vertical) {
+ return vertical * mOverlay.mHeightScaleFactor;
+ }
+
+ /**
+ * Adjusts the x coordinate from the preview's coordinate system to the view coordinate
+ * system.
+ */
+ public float translateX(float x) {
+ if (mOverlay.mFacing == CameraSource.CAMERA_FACING_FRONT) {
+ return mOverlay.getWidth() - scaleX(x);
+ } else {
+ return scaleX(x);
+ }
+ }
+
+ /**
+ * Adjusts the y coordinate from the preview's coordinate system to the view coordinate
+ * system.
+ */
+ public float translateY(float y) {
+ return scaleY(y);
+ }
+
+ public void postInvalidate() {
+ mOverlay.postInvalidate();
+ }
+ }
+
+ public GraphicOverlay(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Removes all graphics from the overlay.
+ */
+ public void clear() {
+ synchronized (mLock) {
+ mGraphics.clear();
+ }
+ postInvalidate();
+ }
+
+ /**
+ * Adds a graphic to the overlay.
+ */
+ public void add(T graphic) {
+ synchronized (mLock) {
+ mGraphics.add(graphic);
+ }
+ postInvalidate();
+ }
+
+ /**
+ * Removes a graphic from the overlay.
+ */
+ public void remove(T graphic) {
+ synchronized (mLock) {
+ mGraphics.remove(graphic);
+ }
+ postInvalidate();
+ }
+
+ /**
+ * Returns a copy (as a list) of the set of all active graphics.
+ * @return list of all active graphics.
+ */
+ public List getGraphics() {
+ synchronized (mLock) {
+ return new Vector(mGraphics);
+ }
+ }
+
+ /**
+ * Returns the horizontal scale factor.
+ */
+ public float getWidthScaleFactor() {
+ return mWidthScaleFactor;
+ }
+
+ /**
+ * Returns the vertical scale factor.
+ */
+ public float getHeightScaleFactor() {
+ return mHeightScaleFactor;
+ }
+
+ /**
+ * Sets the camera attributes for size and facing direction, which informs how to transform
+ * image coordinates later.
+ */
+ public void setCameraInfo(int previewWidth, int previewHeight, int facing) {
+ synchronized (mLock) {
+ mPreviewWidth = previewWidth;
+ mPreviewHeight = previewHeight;
+ mFacing = facing;
+ }
+ postInvalidate();
+ }
+
+ /**
+ * Draws the overlay with its associated graphic objects.
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ synchronized (mLock) {
+ if ((mPreviewWidth != 0) && (mPreviewHeight != 0)) {
+ mWidthScaleFactor = (float) canvas.getWidth() / (float) mPreviewWidth;
+ mHeightScaleFactor = (float) canvas.getHeight() / (float) mPreviewHeight;
+ }
+
+ for (Graphic graphic : mGraphics) {
+ graphic.draw(canvas);
+ }
+ }
+ }
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/MainActivity.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/MainActivity.java
index 4514dd7..c26d655 100644
--- a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/MainActivity.java
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/MainActivity.java
@@ -7,345 +7,366 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
-import android.content.pm.ActivityInfo;
+
import android.content.pm.PackageManager;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.location.LocationManager;
-import android.os.BatteryManager;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
-import android.util.Log;
import android.view.View;
-import android.view.WindowManager;
-import android.widget.Button;
+import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ToggleButton;
-import org.json.JSONObject;
+import android.os.Bundle;
+import android.util.Log;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
+ /**
+ * Elastic Sensor Dump.
+ * Enumerates the sensors from an android device.
+ * Record the sensor data and upload it to your elastic search server.
+ */
+public class MainActivity extends Activity{
-public class MainActivity extends Activity implements SensorEventListener {
+ /** */
+ private final String logTag = "MainActivity";
- private static int MIN_SENSOR_REFRESH = 50;
- private ElasticSearchIndexer esIndexer;
- private SensorManager mSensorManager;
- private LocationManager locationManager;
- // Config data
+ /** Global SharedPreferences object. */
private SharedPreferences sharedPrefs;
- private TextView tvProgress = null;
- private GPSLogger gpsLogger = new GPSLogger();
- // JSON structure for sensor and gps data
- private JSONObject joSensorData = new JSONObject();
+ /** Broadcast receiver to receive updates from the rest of the app. */
+ private BroadcastReceiver broadcastReceiver;
+
+ /** Persistent access to the apps database to avoid creating multiple db objects. */
+ private DatabaseHelper databaseHelper;
- private int[] usableSensors;
- private boolean logging = false;
+ /** If the UI has receivers registered. */
+ private boolean registeredReceivers;
- private long lastUpdate;
- private long startTime;
+ /** Action string address to facilitate communication for updating UI display. */
+ public static final String UI_SENSOR_COUNT = "esd.intent.action.message.UI_SENSOR_COUNT";
+ /** Action string address to indicate if the service manager is currently running. */
+ public static final String UI_ACTION_RECEIVER = "esd.intent.action.message.UI_ACTION_RECEIVER";
+
+ /** */
+ public static final String UI_UPLOAD_COUNT = "esd.intent.action.message.UI_ACTION_RECEIVER";
+
+ /** Control variable to lessen the impact of consistent database queries. Once per 5 updates. */
+ private int updateCounterForDatabaseQueries;
+
+ /** Do NOT record more than once every 50 milliseconds. Default value is 250ms. */
+ private final int MIN_SENSOR_REFRESH = 50;
+
+ /** Refresh time in milliseconds. Default = 250ms.*/
private int sensorRefreshTime = 250;
- double batteryLevel = 0;
-
- final BroadcastReceiver batteryReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- //context.unregisterReceiver(this);
- int tempLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
- int tempScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
- if(tempLevel > 0 && tempScale > 0) {
- batteryLevel = tempLevel;
+
+ /** Number of sensor readings this session */
+ private int sensorReadings, documentsIndexed, gpsReadings, uploadErrors, audioReadings, databasePopulation = 0;
+
+ /** Create our main activity broadcast receiver to receive data from app. */
+ private void createBroadcastReceiver(){
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction( UI_SENSOR_COUNT );
+ intentFilter.addAction( UI_UPLOAD_COUNT );
+
+ broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive( Context context, Intent intent ) {
+
+ switch( intent.getAction() ){
+
+ case UI_SENSOR_COUNT:
+ // Update sensor metrics. If the intent reading is null, use the last reading we received.
+ sensorReadings = intent.getIntExtra("sensorReadings", sensorReadings );
+ gpsReadings = intent.getIntExtra("gpsReadings", gpsReadings );
+ audioReadings = intent.getIntExtra( "audioReadings", audioReadings );
+ break;
+ case UI_UPLOAD_COUNT:
+ // Update upload metrics. If the intent reading is null, use the last reading we received.
+ documentsIndexed = intent.getIntExtra( "documentsIndexed", documentsIndexed );
+ uploadErrors = intent.getIntExtra( "uploadErrors", uploadErrors );
+ updateScreen();
+ break;
+ default:
+ break;
+ }
+
}
- }
- };
+ };
+ registerReceiver( broadcastReceiver, intentFilter );
+ registeredReceivers = true;
+ }
- boolean gpsChoosen = false;
- boolean gpsBool = false;
+ /**
+ * Build main activity buttons.
+ * @param savedInstanceState A generic object.
+ */
@Override
protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- // Prevent screen from going into landscape
- setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
-
- // Callback for settings screen
- final Intent settingsIntent = new Intent(this, SettingsActivity.class);
- sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
-
- // check for gps access, also set up some button controls
- CheckGPS();
- // Click a button, get some sensor data
- final Button btnStart = (Button) findViewById(R.id.btnStart);
- btnStart.setOnClickListener(new View.OnClickListener() {
- public void onClick(View v) {
- if (!logging) {
- btnStart.setText(getString(R.string.buttonStop));
- startLogging();
- logging = true;
- } else {
- btnStart.setText(getString(R.string.buttonStart));
- stopLogging();
- logging = false;
+ super.onCreate( savedInstanceState);
+ setContentView( R.layout.activity_main);
+ sharedPrefs = this.getPreferences(MODE_PRIVATE);
+ buildButtonLogic();
+ createBroadcastReceiver();
+
+ Log.e(logTag, "Started Main Activity!" );
+ }
+
+ /** Method to start the service manager if we have not already. */
+ private void startServiceManager(){
+
+ Intent startIntent = new Intent( this, EsdServiceManager.class );
+ startService( startIntent );
+
+ }
+
+ /**
+ * Update preferences with new permissions.
+ * @param asked Preferences key.
+ * @param permission True if we have access.
+ */
+ private void BooleanToPrefs(String asked, boolean permission) {
+ sharedPrefs = getPreferences(MODE_PRIVATE);
+ SharedPreferences.Editor sharedPref_Editor = sharedPrefs.edit();
+ sharedPref_Editor.putBoolean(asked, permission);
+ sharedPref_Editor.apply();
+ }
+
+ /**
+ * Update the display with readings/written/errors.
+ * Need to update UI based on the passed data intent.
+ */
+ private void updateScreen() {
+ Log.i( logTag, "Updating screen." );
+ // Execute this on first executeIndexer, and then every third update from then on.
+ if( updateCounterForDatabaseQueries % 10 == 0 ) {
+ updateCounterForDatabaseQueries = 0;
+ getDatabasePopulation();
+ }
+
+ TextView sensorTV = (TextView) findViewById(R.id.sensor_tv);
+ TextView documentsTV = (TextView) findViewById(R.id.documents_tv);
+ TextView gpsTV = (TextView) findViewById(R.id.gps_TV);
+ TextView errorsTV = (TextView) findViewById(R.id.errors_TV);
+ TextView audioTV = (TextView) findViewById( R.id.audioCount );
+ TextView databaseTV = (TextView) findViewById( R.id.databaseCount );
+
+ sensorTV.setText( String.valueOf(sensorReadings) );
+ documentsTV.setText( String.valueOf( documentsIndexed ) );
+ gpsTV.setText( String.valueOf( gpsReadings ) );
+ errorsTV.setText( String.valueOf( uploadErrors ) );
+ audioTV.setText( String.valueOf( audioReadings ) );
+ databaseTV.setText( String.valueOf( databasePopulation ) );
+ updateCounterForDatabaseQueries++;
+ }
+
+ /** Get the current database population. */
+ private void getDatabasePopulation(){
+ Long databaseEntries;
+ databaseEntries = databaseHelper.databaseEntries();
+ //Log.e(logTag + "getDbPop", "Database population = " + databaseEntries );
+ databasePopulation = Integer.valueOf( databaseEntries.toString() );
+ }
+
+ /**
+ * Go through the sensor array and light them all up
+ * btnStart: Click a button, get some sensor data.
+ * ibSetup: Settings screen.
+ * seekBar: Adjust the collection rate of data.
+ * gpsToggle: Turn gps collection on/off.
+ * audioToggle: Turn audio recording on/off.
+ */
+ private void buildButtonLogic() {
+
+ final ToggleButton startButton = (ToggleButton) findViewById(R.id.toggleStart);
+ startButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged( CompoundButton buttonView, boolean isChecked ) {
+
+ if( isChecked ){
+ Log.e( logTag, "Start button ON !");
+ startButton.setBackgroundResource( R.drawable.main_button_shape_on);
+ }else{
+ Log.e( logTag, "Start button OFF !");
+ startButton.setBackgroundResource( R.drawable.main_button_shape_off);
}
+ // Broadcast to the service manager that we are toggling sensor logging.
+ Intent messageIntent = new Intent();
+ messageIntent.setAction( EsdServiceReceiver.SENSOR_MESSAGE );
+ messageIntent.putExtra( "sensorPower", isChecked );
+ sendBroadcast( messageIntent );
}
});
- // Click a button, get the settings screen
- final ImageButton ibSetup = (ImageButton) findViewById(R.id.ibSetup);
- ibSetup.setOnClickListener(new View.OnClickListener() {
+ final ImageButton settingsButton = (ImageButton) findViewById(R.id.settings);
+ settingsButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
- startActivity(settingsIntent);
+ startActivity( new Intent(getBaseContext(), SettingsActivity.class) );
+ }
+ });
+
+
+ final CheckBox gpsCheckBox = (CheckBox) findViewById(R.id.gpsCheckBox);
+
+ gpsCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ // If gps button is turned ON.
+ if( !gpsPermission() && isChecked ){
+ gpsCheckBox.toggle();
+ Toast.makeText( getApplicationContext(), "GPS access denied.", Toast.LENGTH_SHORT ).show();
+ BooleanToPrefs("gps_asked", false);
+ }else{
+ // Broadcast to the service manager that we are toggling gps logging.
+ Intent messageIntent = new Intent( EsdServiceReceiver.GPS_MESSAGE );
+ messageIntent.putExtra("gpsPower", isChecked );
+ sendBroadcast( messageIntent );
+ }
+
+ }
+ });
+
+ final CheckBox audioCheckBox = (CheckBox) findViewById(R.id.audioCheckBox );
+ audioCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+
+ // If audio button is turned ON.
+ if( !audioPermission() && isChecked ){
+ audioCheckBox.toggle();
+ Toast.makeText( getApplicationContext(), "Audio access denied.", Toast.LENGTH_SHORT ).show();
+ BooleanToPrefs("audio_Asked", false);
+ }else{
+ // Broadcast to the service manager that we are toggling audio logging.
+ Intent messageIntent = new Intent( EsdServiceReceiver.AUDIO_MESSAGE );
+ messageIntent.putExtra( "audioPower", isChecked );
+ sendBroadcast( messageIntent );
+ }
}
});
- // Slide a bar to adjust the refresh times
final SeekBar seekBar = (SeekBar) findViewById(R.id.seekBar);
final TextView tvSeekBarText = (TextView) findViewById(R.id.TickText);
- tvSeekBarText.setText(getString(R.string.Collection_Interval) + " " + seekBar.getProgress() + getString(R.string.milliseconds));
+ tvSeekBarText.setText(getString(R.string.Collection_Interval) + " " + seekBar.getProgress() * 10 + getString(R.string.milliseconds));
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
- if (fromUser) {
- if(progress < MIN_SENSOR_REFRESH) progress = MIN_SENSOR_REFRESH;
- tvSeekBarText.setText(getString(R.string.Collection_Interval) + " " + progress + getString(R.string.milliseconds));
- sensorRefreshTime = progress;
+ if ( progress * 10 < MIN_SENSOR_REFRESH ) {
+ seekBar.setProgress( 5 );
+ Toast.makeText( getApplicationContext(), "Minimum sensor refresh is 50 ms", Toast.LENGTH_SHORT).show();
+ }else{
+ sensorRefreshTime = progress * 10;
}
+
+ Intent messageIntent = new Intent( EsdServiceReceiver.INTERVAL );
+ messageIntent.putExtra( "sensorInterval", sensorRefreshTime );
+ sendBroadcast( messageIntent );
+
+ tvSeekBarText.setText(getString(R.string.Collection_Interval) + " " + sensorRefreshTime + getString(R.string.milliseconds));
}
@Override
- public void onStartTrackingTouch(SeekBar seekBar){ } //intentionally blank
+ public void onStartTrackingTouch(SeekBar seekBar) {} //intentionally blank
+
@Override
- public void onStopTrackingTouch(SeekBar seekBar) { } //intentionally blank
+ public void onStopTrackingTouch(SeekBar seekBar) {} //intentionally blank
});
- // Get a list of all available sensors on the device and store in array
- mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
- List deviceSensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
- usableSensors = new int[deviceSensors.size()];
- for (int i = 0; i < deviceSensors.size(); i++) {
- usableSensors[i] = deviceSensors.get(i).getType();
- }
+ }
- IntentFilter batteryFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
- registerReceiver(batteryReceiver, batteryFilter);
+ /** Prompt user for GPS access.
+ * Write this result to shared preferences.
+ * @return True if we asked for permission and it was granted.
+ */
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean gpsPermission() {
- }
+ boolean gpsPermissionCoarse = (ContextCompat.checkSelfPermission(this, Manifest.permission.
+ ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED);
- @Override
- public final void onAccuracyChanged(Sensor sensor, int accuracy) {
- // I don't really care about this yet.
- }
+ boolean gpsPermissionFine = (ContextCompat.checkSelfPermission(this, android.Manifest.permission.
+ ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED);
- @Override
- public final void onSensorChanged(SensorEvent event) {
-
- try {
- // Update timestamp in sensor data structure
- Date logDate = new Date(System.currentTimeMillis());
- SimpleDateFormat logDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
- String dateString = logDateFormat.format(logDate);
- joSensorData.put("@timestamp", dateString);
-
- // Store the logging start time with each document
- Date startDate = new Date(startTime);
- String startDateString = logDateFormat.format(startDate);
- joSensorData.put("start_time", startDateString);
-
- // Store the duration of the sensor log with each document
- long logDuration = (System.currentTimeMillis() - startTime) / 1000;
- joSensorData.put("log_duration_seconds", logDuration);
-
- // Dump gps data into document if it's ready
- if (gpsLogger.gpsHasData) {
- joSensorData.put("location", "" + gpsLogger.gpsLat + "," + gpsLogger.gpsLong);
- joSensorData.put("start_location", "" + gpsLogger.gpsLatStart + "," + gpsLogger.gpsLongStart);
- joSensorData.put("altitude", gpsLogger.gpsAlt);
- joSensorData.put("accuracy", gpsLogger.gpsAccuracy);
- joSensorData.put("bearing", gpsLogger.gpsBearing);
- joSensorData.put("gps_provider", gpsLogger.gpsProvider);
- joSensorData.put("speed", gpsLogger.gpsSpeed);
- joSensorData.put("speed_kmh", gpsLogger.gpsSpeedKMH);
- joSensorData.put("speed_mph", gpsLogger.gpsSpeedMPH);
- joSensorData.put("gps_updates", gpsLogger.gpsUpdates);
- joSensorData.put("acceleration", gpsLogger.gpsAcceleration);
- joSensorData.put("acceleration_kmh", gpsLogger.gpsAccelerationKMH);
- joSensorData.put("acceleration_mph", gpsLogger.gpsAccelerationMPH);
- joSensorData.put("distance_metres", gpsLogger.gpsDistanceMetres);
- joSensorData.put("distance_feet", gpsLogger.gpsDistanceFeet);
- joSensorData.put("total_distance_metres", gpsLogger.gpsTotalDistance);
- joSensorData.put("total_distance_km", gpsLogger.gpsTotalDistanceKM);
- joSensorData.put("total_distance_miles", gpsLogger.gpsTotalDistanceMiles);
- }
- // put battery status percentage into the Json.
- if (batteryLevel > 0) {
- joSensorData.put("battery_percentage", batteryLevel);
- }
- // Store sensor update into sensor data structure
- for (int i = 0; i < event.values.length; i++) {
- // We don't need the android.sensor. and motorola.sensor. stuff
- // Split it out and just get the sensor name
- String sensorName;
- String[] sensorHierarchyName = event.sensor.getStringType().split("\\.");
- if (sensorHierarchyName.length == 0) {
- sensorName = event.sensor.getStringType();
- } else {
- sensorName = sensorHierarchyName[sensorHierarchyName.length - 1] + i;
- }
- // Store the actual sensor data now unless it's returning NaN or something crazy big or small
- Float sensorValue = event.values[i];
- if (!sensorValue.isNaN() && sensorValue < Long.MAX_VALUE && sensorValue > Long.MIN_VALUE) {
- joSensorData.put(sensorName, sensorValue);
- }
- }
+ if( !gpsPermissionFine && !gpsPermissionCoarse ){
- // Make sure we only generate docs at an adjustable rate
- // We'll use 250ms for now
- if (System.currentTimeMillis() > lastUpdate + sensorRefreshTime) {
- updateScreen();
- lastUpdate = System.currentTimeMillis();
- esIndexer.index(joSensorData);
+ ActivityCompat.requestPermissions(this, new String[]{
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ }, 1);
- }
- } catch (Exception e) {
- Log.v("JSON Logging error", e.toString());
- }
- }
+ gpsPermissionCoarse = ( ContextCompat.checkSelfPermission(this, Manifest.permission.
+ ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED );
- // Go through the sensor array and light them all up
- private void startLogging() {
-
- // Prevent screen from sleeping if logging has started
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- //check for gps access, store in preferences
- logging = true;
- startTime = System.currentTimeMillis();
- lastUpdate = startTime;
- gpsLogger.resetGPS();
- esIndexer = new ElasticSearchIndexer();
- esIndexer.updateURL(sharedPrefs);
- // Bind all sensors to activity
- for (int usableSensor : usableSensors) {
- mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(usableSensor), SensorManager.SENSOR_DELAY_NORMAL);
- }
- }
+ gpsPermissionFine = ( ContextCompat.checkSelfPermission(this, android.Manifest.permission.
+ ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED );
- // Shut down the sensors by stopping listening to them
- private void stopLogging() {
-
- // Disable wakelock if logging has stopped
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- logging = false;
- unregisterReceiver(batteryReceiver);
- tvProgress = (TextView) findViewById(R.id.tvProgress);
- tvProgress.setText( getString(R.string.loggingStopped) );
- mSensorManager.unregisterListener(this);
-
- // Disable GPS if we allowed it.
- if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
- try {
- locationManager.removeUpdates(gpsLogger);
- gpsOFF();
- } catch (Exception e) {
- Log.v("GPS Error", "GPS could not unbind");
- }
}
+ BooleanToPrefs("gps_permission_FINE", gpsPermissionFine );
+ BooleanToPrefs("gps_permission_COURSE", gpsPermissionCoarse );
+ return ( gpsPermissionFine || gpsPermissionCoarse );
}
- // Update the display with readings/written/errors
- private void updateScreen() {
+ /** Prompt user for MICROPHONE access.
+ * Write this result to shared preferences.
+ * @return True if we asked for permission and it was granted.
+ */
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean audioPermission(){
+ boolean audioPermission = sharedPrefs.getBoolean( "audio_permission", false );
- String updateText = getString(R.string.Sensor_Readings) + esIndexer.indexRequests + "\n" +
- getString(R.string.Documents_Written) + esIndexer.indexSuccess + "\n" +
- getString(R.string.GPS_Updates) + gpsLogger.gpsUpdates + "\n" +
- getString(R.string.Errors) + esIndexer.failedIndex + "\n" +
- "Batter sensor" + batteryLevel;
- tvProgress = (TextView) findViewById(R.id.tvProgress);
- tvProgress.setText(updateText);
- }
+ if( !audioPermission ){
- // Catch the permissions request for GPS being successful, and light up the GPS for this session
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
- switch (requestCode) {
- case 1: {
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
- locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
- locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, gpsLogger);
- }
- }
- }
+ String[] permissions = {Manifest.permission.RECORD_AUDIO};
+ ActivityCompat.requestPermissions(this, permissions, 1);
+
+ audioPermission = ( ContextCompat.checkSelfPermission(this,
+ Manifest.permission.RECORD_AUDIO ) == PackageManager.PERMISSION_GRANTED );
+ BooleanToPrefs("audio_Permission", audioPermission );
}
- }
- // unbind GPS listener, should stop GPS thread in 2.0
- public void gpsOFF(){
- //unbind GPS listener if permission was granted
- if(ContextCompat.checkSelfPermission(this, android.Manifest.permission.
- ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED )
- locationManager.removeUpdates(gpsLogger);
- else if(ContextCompat.checkSelfPermission(this, android.Manifest.permission.
- ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED)
- {
- Log.v("GPS Denied", "GPS Denied");
- }//intentionally blank, if permission is denied initially, gps was not on
- else //anything else
- Log.v("GPS Error", "GPS could not unbind");
+ return audioPermission;
}
- // GPS on/off persistent on app restart
- public void CheckGPS()
- {
- CompoundButton GpsButton = (CompoundButton) findViewById(R.id.GPS_Toggle);
- // if the user has already been asked about GPS access, do not ask again
- // else ask and verify access before listening
- if(!gpsChoosen) {
- if(sharedPrefs.getBoolean("GPS_bool", true) ) {
- gpsChoosen = true;
- gpsBool = true;
- GpsButton.setChecked(true);
- } else {
- gpsChoosen = true;
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
- gpsBool = ( ContextCompat.checkSelfPermission(this, android.Manifest.permission.
- ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED );
- }
- }
- // Light up the GPS if we're allowed
- if ( gpsBool ) {
- locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
- locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, gpsLogger);
- }
- }
+ /** If our activity is paused, we need to indicate to the service manager via a static variable. */
@Override
protected void onPause() {
+ if( registeredReceivers ){
+ unregisterReceiver( broadcastReceiver );
+ registeredReceivers = false;
+ }
+
+ databaseHelper.close();
super.onPause();
}
+ /** When the activity starts or resumes, we start the upload process immediately.
+ * If we were logging, we need to start the logging process. ( OS memory trim only )
+ */
@Override
protected void onResume() {
super.onResume();
+ updateCounterForDatabaseQueries = 0;
+ startServiceManager();
+ if( !registeredReceivers ){
+ createBroadcastReceiver();
+ }
+ databaseHelper = new DatabaseHelper( this );
+ getDatabasePopulation();
+ updateScreen();
}
- @Override
- protected void onStop() {
- super.onStop();
- }
-
+ /** If the user exits the application. */
@Override
protected void onDestroy() {
+ if( registeredReceivers ){
+ unregisterReceiver( broadcastReceiver );
+ }
+
+ databaseHelper.close();
super.onDestroy();
}
}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SensorListener.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SensorListener.java
new file mode 100644
index 0000000..9acba00
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SensorListener.java
@@ -0,0 +1,354 @@
+package ca.dungeons.sensordump;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorManager;
+import android.location.LocationManager;
+import android.os.BatteryManager;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by Gurtok on 8/14/2017.
+ *
+ */
+class SensorListener implements android.hardware.SensorEventListener {
+
+
+ /** Use this to identify this classes log messages. */
+ private final String logTag = "SensorRunnable";
+
+ /** Main activity context. */
+ private final Context passedContext;
+
+ /** Applications' shared preferences. */
+ private final SharedPreferences sharedPrefs;
+
+ /** Gives access to the local database via a helper class.*/
+ private final DatabaseHelper dbHelper;
+ /** */
+ private final ExecutorService threadPool = Executors.newSingleThreadExecutor();
+
+// Date / Time variables.
+ /** A static reference to the custom date format. */
+ @SuppressWarnings("SpellCheckingInspection")
+ private final SimpleDateFormat logDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZ", Locale.US);
+
+ /** Timers, the schema is defined else where. */
+ private final long startTime;
+ private long lastUpdate;
+
+// Sensor variables.
+ /** If we are currently logging PHONE sensor data. */
+ private boolean sensorLogging = false;
+
+ /** Instance of sensorMessageHandler Manager. */
+ private SensorManager mSensorManager;
+
+ /** Each loop, data wrapper to upload to Elastic. */
+ private JSONObject joSensorData = new JSONObject();
+
+ /** Array to hold sensorMessageHandler references. */
+ private List usableSensorList;
+
+ /** Refresh time in milliseconds. Default = 250ms.*/
+ private int sensorRefreshTime = 250;
+
+ /** If listeners are active. */
+ private boolean sensorsRegistered = false;
+
+ /** Listener for battery updates. */
+ private BroadcastReceiver batteryReceiver;
+
+ /** Battery level in percentages. */
+ private double batteryLevel = 0;
+
+// GPS variables.
+ /** Used to get access to GPS. */
+ private final LocationManager locationManager;
+
+ /** Helper class to organize gps data. */
+ private GPSLogger gpsLogger = new GPSLogger();
+
+ /** Control for telling if we have already registered the gps listeners. */
+ private boolean gpsRegistered;
+
+// AUDIO variables.
+ /** Helper class for obtaining audio data. */
+ private AudioRunnable audioRunnable;
+
+ /** Control variable to make sure we only create one audio logger. */
+ private boolean audioRegistered;
+
+// Communications.
+ /** Number of sensorMessageHandler readings this session, default 0. */
+ private boolean sensorReading = false;
+
+ /** Number of gps readings this session, default 0. */
+ private boolean gpsReading = false;
+
+ /** Number of audio events. */
+ private boolean audioReading = false;
+
+ /** Default Constructor. */
+ SensorListener( Context context, SharedPreferences sharedPreferences ){
+ sharedPrefs = sharedPreferences;
+ passedContext = context;
+
+ gpsLogger = new GPSLogger();
+ audioRunnable = new AudioRunnable();
+ dbHelper = new DatabaseHelper( passedContext );
+ startTime = lastUpdate = System.currentTimeMillis();
+ locationManager = (LocationManager) passedContext.getSystemService( Context.LOCATION_SERVICE );
+ parseSensorArray();
+
+ }
+
+
+ /** Our main connection to the UI thread for communication. */
+ private void onProgressUpdate() {
+ Intent messageIntent = new Intent( EsdServiceReceiver.SENSOR_SUCCESS );
+ messageIntent.putExtra( "sensorReading", sensorReading );
+
+ if( gpsRegistered ){
+ messageIntent.putExtra( "gpsReading", gpsReading );
+ }
+
+ if( audioRegistered ){
+ messageIntent.putExtra( "audioReading", audioReading );
+ }
+
+ passedContext.sendBroadcast( messageIntent );
+ }
+
+ /**
+ * This is the main recording loop. One reading per sensorMessageHandler per loop.
+ * Update timestamp in sensorMessageHandler data structure.
+ * Store the logging start time with each document.
+ * Store the duration of the sensorMessageHandler log with each document.
+ * Dump gps data into document if it's ready.
+ * Put battery status percentage into the Json.
+ *
+ * @param event A reference to the event object.
+ */
+ @Override
+ public final void onSensorChanged(SensorEvent event) {
+
+ if( System.currentTimeMillis() > lastUpdate + sensorRefreshTime ) {
+ // ^^ Make sure we generate docs at an adjustable rate.
+ // 250ms is the default setting.
+
+ // Reset our flags to update the service manager about the type of sensor readings.
+ sensorReading = gpsReading = audioReading = false;
+
+ // Check if we should be shutting down sensor recording.
+ if( !sensorLogging ){
+ return;
+ }
+
+ String sensorName;
+ String[] sensorHierarchyName;
+ try {
+ joSensorData.put( "@timestamp", logDateFormat.format( new Date( System.currentTimeMillis() )) );
+ joSensorData.put( "start_time", logDateFormat.format( new Date( startTime )) );
+ joSensorData.put( "log_duration_seconds", ( System.currentTimeMillis() - startTime ) / 1000 );
+
+ //Log.e(logTag, "gpsRegistered: " + gpsRegistered + " gps has data? " + gpsLogger.gpsHasData );
+ if( gpsRegistered && gpsLogger.gpsHasData ){
+ joSensorData = gpsLogger.getGpsData( joSensorData );
+ gpsReading = true;
+ }
+
+ //Log.e(logTag, "audioRegistered: " + audioRegistered + " gps has data? " + gpsLogger.gpsHasData );
+ if( audioRegistered && audioRunnable.hasData ){
+ joSensorData = audioRunnable.getAudioData( joSensorData );
+ audioReading = true;
+ }
+
+ if( batteryLevel > 0 ){
+ joSensorData.put("battery_percentage", batteryLevel);
+ }
+
+ for( Float cursor: event.values ){
+ if( !cursor.isNaN() && cursor < Long.MAX_VALUE && cursor > Long.MIN_VALUE ){
+ sensorHierarchyName = event.sensor.getStringType().split("\\.");
+ sensorName = ( sensorHierarchyName.length == 0 ? event.sensor.getStringType() : sensorHierarchyName[sensorHierarchyName.length - 1] );
+ joSensorData.put(sensorName, cursor );
+ }
+ }
+
+ dbHelper.JsonToDatabase( joSensorData );
+ sensorReading = true;
+ onProgressUpdate();
+ lastUpdate = System.currentTimeMillis();
+ //Log.e( logTag, "Sensor EVENT!" );
+ } catch (JSONException JsonEx) {
+ Log.e( logTag, JsonEx.getMessage() + " || " + JsonEx.getCause());
+ }
+ }
+ }
+
+// Phone Sensors
+ /** Use this method to control if we should be recording sensor data or not. */
+ void setSensorLogging( boolean power ){
+ sensorLogging = power;
+ if( power && !sensorsRegistered ){
+ registerSensorListeners();
+ }
+ if( !power && sensorsRegistered ){
+ unregisterSensorListeners();
+ }
+ }
+
+ /**
+ * A control method for collection intervals.
+ */
+ void setSensorRefreshTime(int updatedRefresh) {
+ sensorRefreshTime = updatedRefresh;
+ }
+
+ /** Method to register listeners upon logging. */
+ private void registerSensorListeners(){
+
+ // Register each sensorMessageHandler to this activity.
+ for (int cursorInt : usableSensorList) {
+ mSensorManager.registerListener( this, mSensorManager.getDefaultSensor(cursorInt),
+ SensorManager.SENSOR_DELAY_NORMAL, null);
+ }
+ IntentFilter batteryFilter = new IntentFilter( Intent.ACTION_BATTERY_CHANGED );
+ passedContext.registerReceiver( this.batteryReceiver, batteryFilter, null, null);
+ sensorsRegistered = true;
+ }
+
+ /** Unregister listeners. */
+ private void unregisterSensorListeners(){
+ passedContext.unregisterReceiver( this.batteryReceiver );
+ mSensorManager.unregisterListener( this );
+ setGpsPower( false );
+ setAudioPower( false );
+ sensorsRegistered = false;
+ }
+
+ /** Generate a list of on-board phone sensors. */
+ @TargetApi(21)
+ private void parseSensorArray(){
+
+ mSensorManager = (SensorManager) passedContext.getSystemService( Context.SENSOR_SERVICE );
+ List deviceSensors = mSensorManager.getSensorList( Sensor.TYPE_ALL );
+ usableSensorList = new ArrayList<>( deviceSensors.size() );
+
+ for( Sensor i: deviceSensors ){
+ // Use this to filter out trigger(One-shot) sensors, which are dealt with differently.
+ if( i.getReportingMode() != Sensor.REPORTING_MODE_ONE_SHOT ){
+ usableSensorList.add( i.getType() );
+ }
+ }
+
+ batteryReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int batteryData = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int batteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ if ( batteryData > 0 && batteryScale > 0 ) {
+ batteryLevel = batteryData;
+ }
+ }
+ };
+ }
+
+// GPS
+ /** Control method to enable/disable gps recording. */
+ void setGpsPower(boolean power) {
+
+ if( power && sensorLogging && !gpsRegistered ){
+ registerGpsSensors();
+ }
+
+ if( !power && gpsRegistered ){
+ unRegisterGpsSensors();
+ }
+
+ }
+
+
+ /** Register gps sensors to enable recording. */
+ private void registerGpsSensors(){
+
+ boolean gpsPermissionFine = sharedPrefs.getBoolean("gps_permission_FINE", false );
+ boolean gpsPermissionCoarse = sharedPrefs.getBoolean( "gps_permission_COARSE", false );
+
+ try{
+ if( gpsPermissionFine || gpsPermissionCoarse ){
+ locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, sensorRefreshTime - 10, 0, gpsLogger );
+ locationManager.requestLocationUpdates( LocationManager.NETWORK_PROVIDER, sensorRefreshTime - 10, 0, gpsLogger );
+ Log.i( logTag, "GPS listeners registered.");
+ gpsRegistered = true;
+ }else{
+ Log.e(logTag+"regGPS", "Register gps method, gpsPermissionFine == false");
+ }
+ }catch ( SecurityException secEx ) {
+ Log.e( logTag, "Failure turning gps on/off. Cause: " + secEx.getMessage() );
+ secEx.printStackTrace();
+ }catch( RuntimeException runTimeEx ){
+ Log.e( logTag, "StackTrace: " );
+ runTimeEx.printStackTrace();
+ }
+ }
+
+ /** Unregister gps sensors. */
+ private void unRegisterGpsSensors(){
+ locationManager.removeUpdates( gpsLogger );
+ gpsRegistered = false;
+ Log.i( logTag, "GPS unregistered.");
+ }
+
+//AUDIO
+
+ /** Set audio recording on/off. */
+ void setAudioPower(boolean power) {
+ if (power && sensorLogging && !audioRegistered) {
+ registerAudioSensors();
+ }
+ if (!power && audioRegistered) {
+ unregisterAudioSensors();
+ }
+ }
+
+ /** Register audio recording thread. */
+ private void registerAudioSensors(){
+ audioRunnable = new AudioRunnable();
+ threadPool.submit( audioRunnable );
+ audioRegistered = true;
+ Log.i( logTag, "Registered audio sensors." );
+ }
+
+ /** Stop audio recording thread. */
+ private void unregisterAudioSensors(){
+ audioRunnable.setStopAudioThread();
+ audioRegistered = false;
+ Log.i( logTag, "Unregistered audio sensors." );
+ }
+
+
+ /** Required stub. Not used. */
+ @Override
+ public final void onAccuracyChanged(Sensor sensor, int accuracy){} // <- Empty
+
+}
+
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SensorRunnable.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SensorRunnable.java
new file mode 100644
index 0000000..472bb7c
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SensorRunnable.java
@@ -0,0 +1,93 @@
+package ca.dungeons.sensordump;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+
+ /**
+ * Listener class to record sensorMessageHandler data.
+ * @author Gurtok.
+ * @version First version of sensor thread.
+ */
+class SensorRunnable implements Runnable {
+
+ /**
+ * Main activity context.
+ */
+ private final Context passedContext;
+
+ /** */
+ private final SensorListener sensorListener;
+
+ /** */
+ final static String SENSOR_POWER = "esd.serviceManager.message.SENSOR_POWER";
+
+ /** */
+ final static String GPS_POWER = "esd.serviceManager.message.GPS_POWER";
+
+ /** */
+ final static String AUDIO_POWER = "esd.serviceManager.message.AUDIO_POWER";
+
+ /** */
+ final static String INTERVAL = "esd.serviceManager.message.sensor.REFRESH_RATE";
+
+// Guts.
+
+ /**
+ * Constructor:
+ * Initialize the sensorMessageHandler manager.
+ */
+ SensorRunnable(Context context, SharedPreferences sharedPreferences) {
+ passedContext = context;
+ sensorListener = new SensorListener(passedContext, sharedPreferences);
+ }
+
+ /** */
+ @Override
+ public void run() {
+ registerMessageReceiver();
+ }
+
+ /** */
+ private void registerMessageReceiver() {
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(SENSOR_POWER);
+ filter.addAction(GPS_POWER);
+ filter.addAction(AUDIO_POWER);
+ filter.addAction(INTERVAL);
+
+ BroadcastReceiver receiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ // Intent action to start recording phone sensors.
+ if (intent.getAction().equals(SENSOR_POWER)) {
+ sensorListener.setSensorLogging( intent.getBooleanExtra("sensorPower", true ) );
+ }
+
+ // Intent action to start gps recording.
+ if (intent.getAction().equals( GPS_POWER )) {
+ sensorListener.setGpsPower( intent.getBooleanExtra("gpsPower", false) );
+ }
+
+ // Intent action to start frequency recording.
+ if (intent.getAction().equals( AUDIO_POWER )) {
+ sensorListener.setAudioPower( intent.getBooleanExtra("audioPower", false) );
+ }
+
+ // Receiver to adjust the sensor collection interval.
+ if (intent.getAction().equals( INTERVAL )) {
+ sensorListener.setSensorRefreshTime( intent.getIntExtra("sensorInterval", 250) );
+ }
+
+ }
+ };
+
+ passedContext.registerReceiver(receiver, filter);
+ }
+
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SettingsActivity.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SettingsActivity.java
index 4bd277e..aeb9273 100644
--- a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SettingsActivity.java
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/SettingsActivity.java
@@ -1,35 +1,36 @@
package ca.dungeons.sensordump;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceActivity;
import android.os.Bundle;
+
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
import android.util.Log;
+import android.widget.BaseAdapter;
+
+import com.google.android.gms.common.api.CommonStatusCodes;
+/** */
public class SettingsActivity extends PreferenceActivity
{
+
+
+ /** */
@Override
- protected void onCreate(Bundle savedInstanceState)
- {
+ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- getFragmentManager().beginTransaction().replace(android.R.id.content, new MyPreferenceFragment()).commit();
-
checkValues();
- }
+ getFragmentManager().beginTransaction().replace( android.R.id.content, new Fragment_Preference() ).commit();
- public static class MyPreferenceFragment extends PreferenceFragment
- {
- @Override
- public void onCreate(final Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- addPreferencesFromResource(R.xml.preferences);
- }
}
- private void checkValues()
- {
+
+
+ /** */
+ private void checkValues() {
SharedPreferences sharedPrefs =
PreferenceManager.getDefaultSharedPreferences(getBaseContext());
@@ -37,13 +38,15 @@ private void checkValues()
String es_port = sharedPrefs.getString("port", "9200");
String es_index = sharedPrefs.getString("index", "sensor_dump");
String es_type = sharedPrefs.getString("type", "phone_data");
- boolean es_gps = sharedPrefs.getBoolean("GPS_bool_preference", false);
-
String current_values = "http://" + es_host + ":" + es_port + "/"
+ es_index + "/" + es_type;
Log.v("Preferences", current_values);
}
+
+
+
+
}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/Uploads.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/Uploads.java
new file mode 100644
index 0000000..5531096
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/Uploads.java
@@ -0,0 +1,265 @@
+package ca.dungeons.sensordump;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.Authenticator;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.PasswordAuthentication;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import javax.net.ssl.HttpsURLConnection;
+
+
+/**
+ * A class to start a thread upload the database to Kibana.
+ * @author Gurtok.
+ * @version First version of upload Async thread.
+ */
+class Uploads implements Runnable{
+
+ /** ID for logcat. */
+ private final String logTag = "Uploads";
+
+ /** Used to gain access to the application database. */
+ private final Context serviceContext;
+
+ /** A reference to the apps stored preferences. */
+ private final SharedPreferences sharedPreferences;
+
+ /** */
+ private final ElasticSearchIndexer esIndexer;
+
+ /** Static variable for the indexer thread to communicate success or failure of an index attempt. */
+ static boolean uploadSuccess = false;
+
+ /** Control variable to indicate if we should stop uploading to elastic. */
+ private static boolean stopUploadThread = false;
+
+ /** Control variable to indicate if this runnable is currently uploading data. */
+ boolean working = false;
+
+ /** Used to keep track of how many POST requests we are allowed to do each second. */
+ private Long globalUploadTimer = System.currentTimeMillis();
+
+ /** Default Constructor using the application context. */
+ Uploads(Context context, SharedPreferences passedPreferences ) {
+ serviceContext = context;
+ sharedPreferences = passedPreferences;
+ esIndexer = new ElasticSearchIndexer( context );
+ }
+
+ /** Main class entry. The data we need has already been updated. So just go nuts. */
+ @Override
+ public void run() {
+ startUploading();
+ }
+
+ /** Control variable to halt the whole thread. */
+ void stopUploading(){ stopUploadThread = true; }
+
+ /** Main work of upload runnable is accomplished here. */
+ private void startUploading() {
+
+ Log.e( logTag, "Started upload thread." );
+
+ working = true;
+ stopUploadThread = false;
+
+ int timeoutCount = 0;
+
+ DatabaseHelper dbHelper = new DatabaseHelper(serviceContext);
+
+ /* If we cannot establish a connection with the elastic server. */
+ if( !checkForElasticHost() ){
+ // This thread is not working.
+ working = false;
+ // We should stop the service if this is true.
+ stopUploadThread = true;
+ Log.e(logTag, "No elastic host." );
+ return;
+ }
+
+ /* Loop to keep uploading. */
+ while( !stopUploadThread ){
+
+ /* A limit of 5 outs per second */
+ if( System.currentTimeMillis() > globalUploadTimer + 200 ){
+
+ updateIndexerUrl();
+
+ uploadSuccess = false;
+ String nextString = dbHelper.getNextCursor();
+
+ // If nextString has data.
+ if ( nextString != null ) {
+ esIndexer.uploadString = nextString;
+ try{
+ // Start the indexing thread, and join to wait for it to finish.
+ esIndexer.start();
+ esIndexer.join();
+ }catch( InterruptedException interEx ){
+ Log.e(logTag, "Failed to join ESI thread, possibly not running." );
+ }
+ if( uploadSuccess ){
+ globalUploadTimer = System.currentTimeMillis();
+ timeoutCount = 0;
+ indexSuccess( true );
+ dbHelper.deleteJson();
+ //Log.e(logTag, "Successful index.");
+ }else{
+ timeoutCount++;
+ indexSuccess( false );
+ }
+ }else{
+ timeoutCount++;
+ }
+ if( timeoutCount > 9 ){
+ Log.i(logTag, "Failed to index 10 times, shutting down." );
+ stopUploading();
+ }
+
+ }
+ }
+ working = false;
+ }
+
+ /** Our main connection to the UI thread for communication. */
+ private void indexSuccess(boolean result ){
+ Intent messageIntent = new Intent( EsdServiceReceiver.INDEX_SUCCESS );
+ messageIntent.putExtra( "INDEX_SUCCESS", result );
+ serviceContext.sendBroadcast( messageIntent );
+ }
+
+ /** Extract config information from sharedPreferences.
+ * Tag the current date stamp on the index name if set in preferences. Credit: GlenRSmith.
+ */
+ private void updateIndexerUrl() {
+
+ // Security variables.
+ boolean esSSL = sharedPreferences.getBoolean("ssl", false);
+ String esUsername = sharedPreferences.getString( "user", "" );
+ String esPassword = sharedPreferences.getString( "pass", "" );
+
+ // X-Pack security credentials.
+ if (esUsername.length() > 0 && esPassword.length() > 0) {
+ esIndexer.esUsername = esUsername;
+ esIndexer.esPassword = esPassword;
+ }
+
+ String esHost = sharedPreferences.getString("host", "localhost");
+ String esPort = sharedPreferences.getString("port", "9200");
+ String esIndex = sharedPreferences.getString("index", "test_index");
+ String esType = sharedPreferences.getString("type", "esd");
+
+ // Tag the current date stamp on the index name if set in preferences
+ // Thanks GlenRSmith for this idea
+ if (sharedPreferences.getBoolean("index_date", false)) {
+ Date logDate = new Date(System.currentTimeMillis());
+ SimpleDateFormat logDateFormat = new SimpleDateFormat("yyyyMMdd", Locale.US);
+ String dateString = logDateFormat.format(logDate);
+ esIndex = esIndex + "-" + dateString;
+ }
+
+ // Default currently is non-secure. Will change that asap.
+ //TODO: Sanitize the input
+ String httpString = "http://";
+ if( esSSL ){
+ httpString = "https://";
+ }
+
+ String mappingURL = String.format( "%s%s:%s/%s", httpString ,esHost ,esPort ,esIndex );
+
+ // Note the different URLs. Regular post ends with type. Mapping ends with index ID.
+ String postingURL = String.format( "%s%s:%s/%s/%s", httpString ,esHost ,esPort ,esIndex, esType );
+
+ try{
+ esIndexer.mapUrl = new URL( mappingURL );
+ esIndexer.postUrl = new URL( postingURL );
+ }catch( MalformedURLException malformedUrlEx ){
+ Log.e(logTag, "Failed to update URLs." );
+ esIndexer.mapUrl = null;
+ esIndexer.postUrl = null;
+ }
+
+ }
+
+ /** Helper method to determine if we currently have access to an elastic server to upload to. */
+ private boolean checkForElasticHost(){
+
+ boolean responseCodeSuccess = false;
+ int responseCode = 0;
+
+ HttpURLConnection httpConnection;
+ HttpsURLConnection httpsConnection;
+
+ URL esUrl;
+ String esHost = sharedPreferences.getString("host", "192.168.1.120" );
+ String esPort = sharedPreferences.getString("port", "9200" );
+ boolean esSSL = sharedPreferences.getBoolean("ssl", false );
+
+ // Secured Connection
+ if( esSSL ) {
+
+ final String esUsername = sharedPreferences.getString("user", "");
+ final String esPassword = sharedPreferences.getString("pass", "");
+
+ try {
+ esUrl = new URL( String.format( "https://%s:%s/", esHost, esPort ) );
+ httpsConnection = (HttpsURLConnection) esUrl.openConnection();
+
+ // Send authentication if required
+ if (esUsername.length() > 0 && esPassword.length() > 0) {
+ Authenticator.setDefault(new Authenticator() {
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(esUsername, esPassword.toCharArray());
+ }
+ });
+ }
+
+ httpsConnection.setConnectTimeout(2000);
+ httpsConnection.setReadTimeout(2000);
+ httpsConnection.connect();
+
+ responseCode = httpsConnection.getResponseCode();
+ if( responseCode >= 200 && responseCode <= 299 ){
+ responseCodeSuccess = true;
+ httpsConnection.disconnect();
+ }
+ }catch( IOException | NullPointerException ex ){
+ Log.e(logTag + " chkHost.", "Failure to open connection cause." + ex.getMessage() + " " + responseCode);
+ //ex.printStackTrace();
+ }
+ }else{ // Else NON-secured connection.
+
+ try{
+ //Log.e("Uploads-CheckHost", esHostUrlString); // DIAGNOSTICS
+ esUrl = new URL( String.format("http://%s:%s/", esHost, esPort ) );
+ httpConnection = (HttpURLConnection) esUrl.openConnection();
+ httpConnection.setConnectTimeout(2000);
+ httpConnection.setReadTimeout(2000);
+ httpConnection.connect();
+
+ responseCode = httpConnection.getResponseCode();
+ if( responseCode >= 200 && responseCode <= 299 ){
+ responseCodeSuccess = true;
+ httpConnection.disconnect();
+ }
+ }catch( IOException ex ){
+ Log.e(logTag + " chkHost.", "Failure to open connection cause." + ex.getMessage() + " " + responseCode);
+ //ex.printStackTrace();
+ }
+ }
+
+ // Returns TRUE if the response code was valid.
+ return responseCodeSuccess;
+ }
+
+}
diff --git a/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/Uploads_Receiver.java b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/Uploads_Receiver.java
new file mode 100644
index 0000000..2250974
--- /dev/null
+++ b/ElasticSensorDump/src/main/java/ca/dungeons/sensordump/Uploads_Receiver.java
@@ -0,0 +1,100 @@
+package ca.dungeons.sensordump;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.util.concurrent.ExecutorService;
+
+ /**
+ * Broadcast receiver for the upload runnable.
+ */
+class Uploads_Receiver {
+
+ /** Main activity context. */
+ private final Context passedContext;
+
+ /** Instance of the Uploads runnable that we can update data on before indexing. */
+ private final Uploads uploads;
+
+ /** Thread pool from the service manager to execute the uploads runnable. */
+ private final ExecutorService workingThreadPool;
+
+ /** Intent action address: Boolean - Control method to shut down upload thread. */
+ final static String STOP_UPLOAD_THREAD = "esd.intent.action.message.Uploads_Receiver.STOP_UPLOAD_THREAD";
+
+ /** Intent action address: Boolean - If ESIndexer was successful indexing a record. */
+ final static String INDEX_SUCCESS = "esd.intent.action.message.Uploads_Receiver.INDEX_SUCCESS";
+
+ /** Intent action address: Boolean - Request by the service manager to start up the upload thread. */
+ final static String START_UPLOAD_THREAD = "esd.intent.action.message.Uploads_Receiver.START_UPLOAD_THREAD";
+
+ /**
+ * Default constructor:
+ * @param context - ESD service manager context.
+ * @param sharedPreferences - The application preferences, contains URL and ID data.
+ * @param passedThreadPool - Application wide thread pool. Execute uploads runnable on this.
+ */
+ Uploads_Receiver(Context context, SharedPreferences sharedPreferences, ExecutorService passedThreadPool ) {
+ passedContext = context;
+ workingThreadPool = passedThreadPool;
+ uploads = new Uploads( passedContext, sharedPreferences );
+ }
+
+ /** Broadcast receiver initialization. */
+ private final BroadcastReceiver messageReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ //Log.e(logTag+ "Up_chk", "Received indexer response");
+ String logTag = "Uploads_Receiver";
+ switch( intent.getAction() ){
+
+ case START_UPLOAD_THREAD :
+ Log.e( logTag, "Submitted upload runnable." );
+ workingThreadPool.submit( uploads );
+ break;
+
+ case STOP_UPLOAD_THREAD :
+ Log.e( logTag, "Upload thread interrupted." );
+ uploads.stopUploading();
+ break;
+
+ case INDEX_SUCCESS:
+ Uploads.uploadSuccess = intent.getBooleanExtra("INDEX_SUCCESS", false );
+ break;
+
+ default:
+ Log.e(logTag , "Received bad information from ACTION intent." );
+ break;
+ }
+ }
+ };
+
+ /** Used by the service manager to indicate if this runnable is uploading data. */
+ synchronized boolean isWorking(){ return uploads.working; }
+
+ /** */
+ void registerMessageReceiver(){
+
+ IntentFilter filter = new IntentFilter();
+
+ filter.addAction( STOP_UPLOAD_THREAD );
+ filter.addAction(INDEX_SUCCESS);
+ filter.addAction( START_UPLOAD_THREAD );
+
+
+ // Register this broadcast messageReceiver.
+ passedContext.registerReceiver(messageReceiver, filter );
+ }
+
+ /** */
+ void unRegisterUploadReceiver(){
+ passedContext.unregisterReceiver( messageReceiver );
+ }
+
+
+}
diff --git a/ElasticSensorDump/src/main/res/drawable-hdpi/barcode_icon.png b/ElasticSensorDump/src/main/res/drawable-hdpi/barcode_icon.png
new file mode 100644
index 0000000..56f913d
Binary files /dev/null and b/ElasticSensorDump/src/main/res/drawable-hdpi/barcode_icon.png differ
diff --git a/ElasticSensorDump/src/main/res/drawable-hdpi/custom_checkbox_off.png b/ElasticSensorDump/src/main/res/drawable-hdpi/custom_checkbox_off.png
new file mode 100644
index 0000000..fff490d
Binary files /dev/null and b/ElasticSensorDump/src/main/res/drawable-hdpi/custom_checkbox_off.png differ
diff --git a/ElasticSensorDump/src/main/res/drawable-hdpi/custom_checkbox_on.png b/ElasticSensorDump/src/main/res/drawable-hdpi/custom_checkbox_on.png
new file mode 100644
index 0000000..1651d06
Binary files /dev/null and b/ElasticSensorDump/src/main/res/drawable-hdpi/custom_checkbox_on.png differ
diff --git a/ElasticSensorDump/src/main/res/drawable-hdpi/ic_action_navigation_more_vert.png b/ElasticSensorDump/src/main/res/drawable-hdpi/ic_action_navigation_more_vert.png
new file mode 100644
index 0000000..eda3b50
Binary files /dev/null and b/ElasticSensorDump/src/main/res/drawable-hdpi/ic_action_navigation_more_vert.png differ
diff --git a/ElasticSensorDump/src/main/res/drawable-mdpi/ic_action_navigation_more_vert.png b/ElasticSensorDump/src/main/res/drawable-mdpi/ic_action_navigation_more_vert.png
new file mode 100644
index 0000000..0deccb7
Binary files /dev/null and b/ElasticSensorDump/src/main/res/drawable-mdpi/ic_action_navigation_more_vert.png differ
diff --git a/ElasticSensorDump/src/main/res/drawable-xhdpi/ic_action_navigation_more_vert.png b/ElasticSensorDump/src/main/res/drawable-xhdpi/ic_action_navigation_more_vert.png
new file mode 100644
index 0000000..8ead3f1
Binary files /dev/null and b/ElasticSensorDump/src/main/res/drawable-xhdpi/ic_action_navigation_more_vert.png differ
diff --git a/ElasticSensorDump/src/main/res/drawable-xxhdpi/ic_action_navigation_more_vert.png b/ElasticSensorDump/src/main/res/drawable-xxhdpi/ic_action_navigation_more_vert.png
new file mode 100644
index 0000000..363ef22
Binary files /dev/null and b/ElasticSensorDump/src/main/res/drawable-xxhdpi/ic_action_navigation_more_vert.png differ
diff --git a/ElasticSensorDump/src/main/res/drawable/custom_check_box.xml b/ElasticSensorDump/src/main/res/drawable/custom_check_box.xml
new file mode 100644
index 0000000..d0d0632
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/drawable/custom_check_box.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ElasticSensorDump/src/main/res/drawable/main_button_shape_off.xml b/ElasticSensorDump/src/main/res/drawable/main_button_shape_off.xml
new file mode 100644
index 0000000..1435102
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/drawable/main_button_shape_off.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ElasticSensorDump/src/main/res/drawable/main_button_shape_on.xml b/ElasticSensorDump/src/main/res/drawable/main_button_shape_on.xml
new file mode 100644
index 0000000..df451b3
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/drawable/main_button_shape_on.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ElasticSensorDump/src/main/res/layout-land-v17/barcode_activity_main.xml b/ElasticSensorDump/src/main/res/layout-land-v17/barcode_activity_main.xml
new file mode 100644
index 0000000..ce4a181
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/layout-land-v17/barcode_activity_main.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ElasticSensorDump/src/main/res/layout-land/barcode_activity_main.xml b/ElasticSensorDump/src/main/res/layout-land/barcode_activity_main.xml
new file mode 100644
index 0000000..ce4a181
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/layout-land/barcode_activity_main.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ElasticSensorDump/src/main/res/layout-land/barcode_capture.xml b/ElasticSensorDump/src/main/res/layout-land/barcode_capture.xml
new file mode 100644
index 0000000..8566209
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/layout-land/barcode_capture.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ElasticSensorDump/src/main/res/layout-v17/barcode_activity_main.xml b/ElasticSensorDump/src/main/res/layout-v17/barcode_activity_main.xml
new file mode 100644
index 0000000..ce4a181
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/layout-v17/barcode_activity_main.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ElasticSensorDump/src/main/res/layout/activity_main.xml b/ElasticSensorDump/src/main/res/layout/activity_main.xml
index 8145b52..86aaadf 100644
--- a/ElasticSensorDump/src/main/res/layout/activity_main.xml
+++ b/ElasticSensorDump/src/main/res/layout/activity_main.xml
@@ -3,97 +3,317 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:background="@color/backGroundGreyTint"
android:paddingBottom="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
tools:context="ca.dungeons.sensordump.MainActivity"
- android:background="@color/backGroundGreyTint">
-
-
+ tools:ignore="Overdraw">
-
-
-
-
-
-
-
+ android:layout_gravity="center"
+ android:layout_marginBottom="20dp"
+ android:background="@drawable/main_button_shape_off"
+ android:gravity="center"
+ android:text="@string/buttonStart"
+ android:textColorLink="@color/Light_blue_transparent"
+ android:textOff="@string/buttonStart"
+ android:textOn="@string/buttonStop"
+ android:textSize="12sp" />
+ android:id="@+id/seekBar"
+ style="@style/SeekBar"
+ android:layout_width="250dp"
+ android:layout_height="40dp"
+ android:layout_gravity="center"
+ android:max="100"
+ android:progress="25" />
-
+
+
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/ElasticSensorDump/src/main/res/layout/barcode_activity_main.xml b/ElasticSensorDump/src/main/res/layout/barcode_activity_main.xml
new file mode 100644
index 0000000..ce4a181
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/layout/barcode_activity_main.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ElasticSensorDump/src/main/res/layout/barcode_capture.xml b/ElasticSensorDump/src/main/res/layout/barcode_capture.xml
new file mode 100644
index 0000000..f879df4
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/layout/barcode_capture.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ElasticSensorDump/src/main/res/mipmap-hdpi/ic_gear.png b/ElasticSensorDump/src/main/res/mipmap-hdpi/ic_gear.png
deleted file mode 100644
index 928833c..0000000
Binary files a/ElasticSensorDump/src/main/res/mipmap-hdpi/ic_gear.png and /dev/null differ
diff --git a/ElasticSensorDump/src/main/res/mipmap-mdpi/ic_gear.png b/ElasticSensorDump/src/main/res/mipmap-mdpi/ic_gear.png
deleted file mode 100644
index ae58c02..0000000
Binary files a/ElasticSensorDump/src/main/res/mipmap-mdpi/ic_gear.png and /dev/null differ
diff --git a/ElasticSensorDump/src/main/res/mipmap-xhdpi/ic_gear.png b/ElasticSensorDump/src/main/res/mipmap-xhdpi/ic_gear.png
deleted file mode 100644
index c0dc7ee..0000000
Binary files a/ElasticSensorDump/src/main/res/mipmap-xhdpi/ic_gear.png and /dev/null differ
diff --git a/ElasticSensorDump/src/main/res/mipmap-xxhdpi/ic_gear.png b/ElasticSensorDump/src/main/res/mipmap-xxhdpi/ic_gear.png
deleted file mode 100644
index c83c0fe..0000000
Binary files a/ElasticSensorDump/src/main/res/mipmap-xxhdpi/ic_gear.png and /dev/null differ
diff --git a/ElasticSensorDump/src/main/res/mipmap-xxxhdpi/ic_gear.png b/ElasticSensorDump/src/main/res/mipmap-xxxhdpi/ic_gear.png
deleted file mode 100644
index 829d0c9..0000000
Binary files a/ElasticSensorDump/src/main/res/mipmap-xxxhdpi/ic_gear.png and /dev/null differ
diff --git a/ElasticSensorDump/src/main/res/values/barcode_strings.xml b/ElasticSensorDump/src/main/res/values/barcode_strings.xml
new file mode 100644
index 0000000..a9cbe18
--- /dev/null
+++ b/ElasticSensorDump/src/main/res/values/barcode_strings.xml
@@ -0,0 +1,15 @@
+
+
+ OK
+ Access to the camera is needed for detection
+ This application cannot run because it does not have the camera permission. The application will now exit.
+ Face detector dependencies cannot be downloaded due to low device storage
+ Barcode Reader Sample
+ Click "Read Barcode" to read a barcode
+ Read Barcode
+ Auto Focus
+ Use Flash
+ Barcode read successfully
+ No barcode captured
+ "Error reading barcode: %1$s"
+
diff --git a/ElasticSensorDump/src/main/res/values/colors.xml b/ElasticSensorDump/src/main/res/values/colors.xml
index 3d187c8..92b4713 100644
--- a/ElasticSensorDump/src/main/res/values/colors.xml
+++ b/ElasticSensorDump/src/main/res/values/colors.xml
@@ -1,7 +1,8 @@
- #3F51B5
- #303f9f
+ #3f51b5
+ #bbbbc5#ff4081
- #b5b7ba
+ #e0eaf8
+ #4ecceefa
diff --git a/ElasticSensorDump/src/main/res/values/colors_seekbar.xml b/ElasticSensorDump/src/main/res/values/colors_seekbar.xml
deleted file mode 100644
index 3ea04e7..0000000
--- a/ElasticSensorDump/src/main/res/values/colors_seekbar.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/ElasticSensorDump/src/main/res/values/strings.xml b/ElasticSensorDump/src/main/res/values/strings.xml
index edf3b53..cb0f572 100644
--- a/ElasticSensorDump/src/main/res/values/strings.xml
+++ b/ElasticSensorDump/src/main/res/values/strings.xml
@@ -2,19 +2,23 @@
Elastic Sensor DumpStart CollectingStop Collecting
- Logging StoppedCollection Intervalms
- No sensor Readings
- "Sensor Readings:
- Documents Written:
- GPS Updates:
+ Readings:Errors:
- GPS ON
- GPS OFFSettings
- ToggleButton
- Start Button
+ 0
+ Database:
+ Audio:
+ Uploads:
+ AUDIO
+ Location:
+ GPS
+ Elastic password. ( Requires X-pack installation. )
+ Elastic user name. ( Requires Xpack installation. )
+ Select if Elastic X-Pack is installed.
+ QR Code
+ Import host address.
\ No newline at end of file
diff --git a/ElasticSensorDump/src/main/res/values/styles_seekbar.xml b/ElasticSensorDump/src/main/res/values/styles_seekbar.xml
index fa477d7..24a5f9a 100644
--- a/ElasticSensorDump/src/main/res/values/styles_seekbar.xml
+++ b/ElasticSensorDump/src/main/res/values/styles_seekbar.xml
@@ -8,7 +8,7 @@
@drawable/seekbar_progress_indeterminate_horizontal_holo_light
-
diff --git a/ElasticSensorDump/src/main/res/xml/preferences.xml b/ElasticSensorDump/src/main/res/xml/preferences.xml
index 60e0389..5fb7406 100644
--- a/ElasticSensorDump/src/main/res/xml/preferences.xml
+++ b/ElasticSensorDump/src/main/res/xml/preferences.xml
@@ -1,42 +1,42 @@
-
+ android:summary="Host or IP for elastic search server."
+ android:title="ES Host" />
+ android:title="ES Port" />
+ android:key="index"
+ android:summary="Index name for sensor dump. Default is fine."
+ android:title="ES Index" />
+ android:key="index_date"
+ android:summary="Add current date to index name."
+ android:title="Date Stamp Index" />
+
+ android:summary="@string/EsUsernamePreferences"
+ android:title="ES Username" />
+ android:summary="@string/EsPasswordPreferences"
+ android:title="ES Password" />
+ android:summary="@string/Es_ssl_protection"
+ android:title="Use SSL" />
+
diff --git a/build.gradle b/build.gradle
index 74b2ab0..c2eea8e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.2.3'
+ classpath 'com.android.tools.build:gradle:2.3.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7c920a7..360053e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Dec 02 19:45:49 EST 2016
+#Fri Mar 24 16:42:29 MDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip