Skip to content

Commit

Permalink
Take a photo upon alarm trigger
Browse files Browse the repository at this point in the history
  • Loading branch information
Haydart committed Oct 26, 2017
1 parent ea983b3 commit ee9581a
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 29 deletions.
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package="pl.rmakowiecki.smartalarmcore">

<uses-library android:name="com.google.android.things" />
<uses-permission android:name="android.permission.CAMERA"/>

<application
android:allowBackup="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import pl.rmakowiecki.smartalarmcore.peripheral.beam.BeamBreakDetectorPeripheryContract
import pl.rmakowiecki.smartalarmcore.peripheral.camera.CameraPeripheryContract
import pl.rmakowiecki.smartalarmcore.remote.AlarmBackendContract
import pl.rmakowiecki.smartalarmcore.setup.UsbSetupProviderContract

Expand All @@ -22,6 +23,7 @@ class AlarmActivity : AppCompatActivity() {

private fun initSystemController() = AlarmController(
BeamBreakDetectorPeripheryContract.create(),
CameraPeripheryContract.create(this),
AlarmBackendContract.create(this),
UsbSetupProviderContract.create(this)
)
Expand Down
30 changes: 24 additions & 6 deletions app/src/main/java/pl/rmakowiecki/smartalarmcore/AlarmController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ package pl.rmakowiecki.smartalarmcore

import io.reactivex.disposables.Disposables
import io.reactivex.rxkotlin.subscribeBy
import pl.rmakowiecki.smartalarmcore.AlarmTriggerState.TRIGGERED
import pl.rmakowiecki.smartalarmcore.extensions.applyIoSchedulers
import pl.rmakowiecki.smartalarmcore.extensions.logD
import pl.rmakowiecki.smartalarmcore.peripheral.beam.BeamBreakDetectorPeripheryContract
import pl.rmakowiecki.smartalarmcore.peripheral.camera.CameraPeripheryContract
import pl.rmakowiecki.smartalarmcore.remote.AlarmBackendContract
import pl.rmakowiecki.smartalarmcore.setup.UsbSetupProviderContract

class AlarmController(
val beamBreakDetector: BeamBreakDetectorPeripheryContract,
val backendInteractor: AlarmBackendContract,
val usbSetupProviderContract: UsbSetupProviderContract
private val beamBreakDetector: BeamBreakDetectorPeripheryContract,
private val camera: CameraPeripheryContract,
private val backendInteractor: AlarmBackendContract,
private val usbSetupProvider: UsbSetupProviderContract
) {

private var alarmArmingDisposable = Disposables.disposed()
private var alarmTriggerDisposable = Disposables.disposed()
private var backendConnectionDisposable = Disposables.disposed()

init {
camera.openCamera()
connectToBackend()
usbSetupProviderContract.registerBroadcastListener({ }, { })
usbSetupProvider.registerBroadcastListener({ }, { })
}

private fun connectToBackend() {
Expand Down Expand Up @@ -51,18 +56,31 @@ class AlarmController(
.registerForChanges()
.applyIoSchedulers()
.subscribeBy(
onNext = this::updateTriggerState
onNext = {
updateTriggerState(it)
if (it == TRIGGERED) {
capturePhoto()
}
}
)
}
}

private fun capturePhoto() {
camera.capturePhoto()
.flatMapSingle(backendInteractor::uploadPhoto)
.subscribeBy(
onNext = { logD("Photo upload success? $it") }
)
}

private fun updateTriggerState(alarmTriggerState: AlarmTriggerState)
= backendInteractor.updateAlarmState(alarmTriggerState)

fun onAppDestroy() {
alarmArmingDisposable.dispose()
alarmTriggerDisposable.dispose()
backendConnectionDisposable.dispose()
usbSetupProviderContract.unregisterBroadcastListener()
usbSetupProvider.unregisterBroadcastListener()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ private const val USB_STATE_CHANGE_ACTION = "android.hardware.usb.action.USB_STA
private const val USB_DEVICE_ATTACHED_ACTION = "android.hardware.usb.action.USB_DEVICE_ATTACHED"
private const val USB_DEVICE_DETACHED_ACTION = "android.hardware.usb.action.USB_DEVICE_DETACHED"

class UsbStateBroadcastReceiver(val onAttach: () -> Unit, val onDetach: () -> Unit) : BroadcastReceiver() {
class UsbStateBroadcastReceiver(private val onAttach: () -> Unit, private val onDetach: () -> Unit) : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) = when (intent.action) {
USB_STATE_CHANGE_ACTION -> logD("USB state changed")
USB_STATE_CHANGE_ACTION -> logD("USB state changed")
USB_DEVICE_ATTACHED_ACTION -> onAttach()
USB_DEVICE_DETACHED_ACTION -> onDetach()
else -> Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class BeamBreakDetectorPeriphery : BeamBreakDetectorPeripheryContract {
private val gpioStateListener = object : GpioCallback() {
override fun onGpioEdge(gpio: Gpio): Boolean {
statePublisher.onNext(gpio.value.toTriggerState())
logD("GPIO 19 state changed to: ${gpio.value}")
return true
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
package pl.rmakowiecki.smartalarmcore.peripheral.camera

import android.content.Context
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.graphics.ImageFormat
import android.hardware.camera2.*
import android.media.ImageReader
import android.media.ImageReader.OnImageAvailableListener
import android.os.Handler
import android.os.HandlerThread
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import pl.rmakowiecki.smartalarmcore.extensions.logD
import pl.rmakowiecki.smartalarmcore.extensions.logE
import pl.rmakowiecki.smartalarmcore.extensions.logW
import pl.rmakowiecki.smartalarmcore.extensions.printStackTrace
import java.util.*

class CameraPeriphery(private var context: Context?) : CameraPeripheryContract {

private var backgroundThread: HandlerThread? = null
private var backgroundHandler: Handler? = null
private lateinit var cameraDevice: CameraDevice
private lateinit var cameraId: String
private var cameraDevice: CameraDevice? = null
private var cameraCaptureSession: CameraCaptureSession? = null
private var imageReadProcessor: ImageReader? = null
private var cameraId: String = ""

private val photoPublishSubject: PublishSubject<ByteArray> = PublishSubject.create()

private val stateCallback = object : CameraDevice.StateCallback() {
private val cameraStateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
logD(javaClass.simpleName, "onCameraOpened")
cameraDevice = camera
Expand All @@ -34,23 +43,98 @@ class CameraPeriphery(private var context: Context?) : CameraPeripheryContract {
}
}

private var cameraSessionCallback = object : CameraCaptureSession.StateCallback() {

override fun onConfigured(captureSession: CameraCaptureSession) {
if (cameraDevice == null) {
logE("The camera is already closed")
return
}

cameraCaptureSession = captureSession
triggerImageCapture()
}

override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) = logW("Failed to configure camera")
}

private val cameraCaptureCallback = object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(session: CameraCaptureSession?, request: CaptureRequest?, result: TotalCaptureResult?) {
super.onCaptureCompleted(session, request, result)
if (session != null) {
session.close()
cameraCaptureSession = null
logD("CaptureSession closed")
}
}
}

private val imageAvailabilityListener = OnImageAvailableListener { reader ->
val image = reader.acquireLatestImage()
val imageByteBuffer = image.planes[0].buffer
val imageBytes = ByteArray(imageByteBuffer.remaining())
imageByteBuffer.get(imageBytes)
image.close()

onPictureTaken(imageBytes)
}

private fun onPictureTaken(imageBytes: ByteArray?) {
if (imageBytes != null) {
photoPublishSubject.onNext(imageBytes)
}
}

private fun triggerImageCapture() {
try {
imageReadProcessor = ImageReader.newInstance(1280, 768,
ImageFormat.JPEG, 10)
imageReadProcessor?.setOnImageAvailableListener(imageAvailabilityListener, backgroundHandler)

val captureBuilder = cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureBuilder?.addTarget(imageReadProcessor?.surface)
captureBuilder?.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
logD("Session initialized.")
cameraCaptureSession?.capture(captureBuilder?.build(), cameraCaptureCallback, null)
} catch (cameraAccessException: CameraAccessException) {
printStackTrace(cameraAccessException)
}

}

override fun openCamera() {
val manager = context?.getSystemService(Context.CAMERA_SERVICE) as CameraManager
logD("opening camera")
try {
cameraId = manager.cameraIdList[0]
val cameraCharacteristics = manager.getCameraCharacteristics(cameraId)
val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
manager.openCamera(cameraId, stateCallback, null)
manager.openCamera(cameraId, cameraStateCallback, null)
} catch (ex: CameraAccessException) {
ex.printStackTrace()
}
}

override fun closeCamera() {
cameraDevice.close()
context = null
override fun capturePhoto(): Observable<ByteArray> {
takePicture()
return photoPublishSubject
}

private fun takePicture() {
if (cameraDevice == null) {
logE("Cannot capture image. Camera not initialized.")
return
}

try {
cameraDevice?.createCaptureSession(Collections.singletonList(imageReadProcessor?.surface), cameraSessionCallback, null)
} catch (cameraAccessException: CameraAccessException) {
printStackTrace(cameraAccessException)
}

}

override fun captureFrame() = Unit
override fun closeCamera() {
cameraDevice?.close()
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package pl.rmakowiecki.smartalarmcore.peripheral.camera

import android.content.Context
import io.reactivex.Observable

interface CameraPeripheryContract {
fun openCamera()
fun closeCamera()
fun captureFrame()
fun capturePhoto(): Observable<ByteArray>

companion object {
fun create(context: Context) = CameraPeriphery(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface AlarmBackendContract {
fun signInToBackend(): Single<Boolean>
fun observeAlarmArmingState(): Observable<AlarmArmingState>
fun updateAlarmState(alarmState: AlarmTriggerState)
fun uploadPhoto(photo: ByteArray): Single<Boolean>

companion object {
fun create(activity: AlarmActivity) = AlarmBackendInteractor(activity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener
import com.google.firebase.storage.FirebaseStorage
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.SingleEmitter
Expand All @@ -17,15 +18,22 @@ import pl.rmakowiecki.smartalarmcore.extensions.logD
import pl.rmakowiecki.smartalarmcore.extensions.printStackTrace
import pl.rmakowiecki.smartalarmcore.toArmingState

class AlarmBackendInteractor(val activity: AlarmActivity) : AlarmBackendContract {
private const val USERS_DIRECTORY = "users"
private const val IMAGES_DIRECTORY = "images"
private const val DIRECT_FILE_PATH = "profilepic.jpg"

class AlarmBackendInteractor(private val activity: AlarmActivity) : AlarmBackendContract {

private val databaseNode = FirebaseDatabase
.getInstance()
.reference

private val storageNode = FirebaseStorage
.getInstance()
.getReferenceFromUrl("gs://smartalarmcore.appspot.com")

override fun signInToBackend(): Single<Boolean> = Single.create { emitter ->

FirebaseAuth.getInstance().signOut()
logD(Settings.Secure.getString(activity.contentResolver, Settings.Secure.ANDROID_ID))

getCurrentBackendUser()?.let {
Expand All @@ -36,13 +44,12 @@ class AlarmBackendInteractor(val activity: AlarmActivity) : AlarmBackendContract
}

private fun signUpAsAuthorizedUser(firebaseUser: FirebaseUser, emitter: SingleEmitter<Boolean>) {
logD(firebaseUser.uid, "UID USERA")
FirebaseAuth.getInstance().createUserWithEmailAndPassword(
"${firebaseUser.uid}@smarthome.com",
Settings.Secure.getString(activity.contentResolver, Settings.Secure.ANDROID_ID)
).addOnSuccessListener {
initializeCoreDeviceNoSqlModel(emitter)
}.addOnFailureListener { printStackTrace(it) }
}.addOnFailureListener(::printStackTrace)
}

private fun initializeCoreDeviceNoSqlModel(emitter: SingleEmitter<Boolean>) = databaseNode
Expand All @@ -53,10 +60,7 @@ class AlarmBackendInteractor(val activity: AlarmActivity) : AlarmBackendContract
override fun isLoggedInToBackend(): Single<Boolean> =
Single.just(getCurrentBackendUser() != null)

private fun getCurrentBackendUser(): FirebaseUser? {
logD(FirebaseAuth.getInstance().currentUser, "GET UID")
return FirebaseAuth.getInstance().currentUser
}
private fun getCurrentBackendUser() = FirebaseAuth.getInstance().currentUser

override fun observeAlarmArmingState(): Observable<AlarmArmingState> = Observable.create { emitter ->
val valueListener = object : ValueEventListener {
Expand All @@ -82,6 +86,14 @@ class AlarmBackendInteractor(val activity: AlarmActivity) : AlarmBackendContract
.setValue(alarmState.toBoolean())
.addOnCompleteListener { }
}

override fun uploadPhoto(photo: ByteArray): Single<Boolean> {
storageNode.getReferenceFromUrl(STORAGE_URL)
.child(USERS_DIRECTORY)
.child(FirebaseUserReloader.reloadCurrentUser(firebaseAuth).getUid())
.child(IMAGES_DIRECTORY)
.child(DIRECT_FILE_PATH)
}
}

private fun DataSnapshot.getArmingState() = (this.value as Boolean).toArmingState()
Expand Down

0 comments on commit ee9581a

Please sign in to comment.