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: + *

    + *
  1. {@link Graphic#scaleX(float)} and {@link Graphic#scaleY(float)} adjust the size of the + * supplied value from the preview scale to the view scale.
  2. + *
  3. {@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the coordinate + * from the preview's coordinate system to the view coordinate system.
  4. + *
+ */ +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: + *
    + *
  1. {@link Graphic#scaleX(float)} and {@link Graphic#scaleY(float)} adjust the size of + * the supplied value from the preview scale to the view scale.
  2. + *
  3. {@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the + * coordinate from the preview's coordinate system to the view coordinate system.
  4. + *
+ * + * @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 @@ + + + + + + + +