Skip to content

Commit ee9581a

Browse files
committed
Take a photo upon alarm trigger
1 parent ea983b3 commit ee9581a

File tree

9 files changed

+149
-29
lines changed

9 files changed

+149
-29
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package="pl.rmakowiecki.smartalarmcore">
44

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

78
<application
89
android:allowBackup="true"

app/src/main/java/pl/rmakowiecki/smartalarmcore/AlarmActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.os.Bundle
66
import android.support.v7.app.AppCompatActivity
77
import android.util.Log
88
import pl.rmakowiecki.smartalarmcore.peripheral.beam.BeamBreakDetectorPeripheryContract
9+
import pl.rmakowiecki.smartalarmcore.peripheral.camera.CameraPeripheryContract
910
import pl.rmakowiecki.smartalarmcore.remote.AlarmBackendContract
1011
import pl.rmakowiecki.smartalarmcore.setup.UsbSetupProviderContract
1112

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

2324
private fun initSystemController() = AlarmController(
2425
BeamBreakDetectorPeripheryContract.create(),
26+
CameraPeripheryContract.create(this),
2527
AlarmBackendContract.create(this),
2628
UsbSetupProviderContract.create(this)
2729
)

app/src/main/java/pl/rmakowiecki/smartalarmcore/AlarmController.kt

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,29 @@ package pl.rmakowiecki.smartalarmcore
22

33
import io.reactivex.disposables.Disposables
44
import io.reactivex.rxkotlin.subscribeBy
5+
import pl.rmakowiecki.smartalarmcore.AlarmTriggerState.TRIGGERED
56
import pl.rmakowiecki.smartalarmcore.extensions.applyIoSchedulers
7+
import pl.rmakowiecki.smartalarmcore.extensions.logD
68
import pl.rmakowiecki.smartalarmcore.peripheral.beam.BeamBreakDetectorPeripheryContract
9+
import pl.rmakowiecki.smartalarmcore.peripheral.camera.CameraPeripheryContract
710
import pl.rmakowiecki.smartalarmcore.remote.AlarmBackendContract
811
import pl.rmakowiecki.smartalarmcore.setup.UsbSetupProviderContract
912

1013
class AlarmController(
11-
val beamBreakDetector: BeamBreakDetectorPeripheryContract,
12-
val backendInteractor: AlarmBackendContract,
13-
val usbSetupProviderContract: UsbSetupProviderContract
14+
private val beamBreakDetector: BeamBreakDetectorPeripheryContract,
15+
private val camera: CameraPeripheryContract,
16+
private val backendInteractor: AlarmBackendContract,
17+
private val usbSetupProvider: UsbSetupProviderContract
1418
) {
1519

1620
private var alarmArmingDisposable = Disposables.disposed()
1721
private var alarmTriggerDisposable = Disposables.disposed()
1822
private var backendConnectionDisposable = Disposables.disposed()
1923

2024
init {
25+
camera.openCamera()
2126
connectToBackend()
22-
usbSetupProviderContract.registerBroadcastListener({ }, { })
27+
usbSetupProvider.registerBroadcastListener({ }, { })
2328
}
2429

2530
private fun connectToBackend() {
@@ -51,18 +56,31 @@ class AlarmController(
5156
.registerForChanges()
5257
.applyIoSchedulers()
5358
.subscribeBy(
54-
onNext = this::updateTriggerState
59+
onNext = {
60+
updateTriggerState(it)
61+
if (it == TRIGGERED) {
62+
capturePhoto()
63+
}
64+
}
5565
)
5666
}
5767
}
5868

69+
private fun capturePhoto() {
70+
camera.capturePhoto()
71+
.flatMapSingle(backendInteractor::uploadPhoto)
72+
.subscribeBy(
73+
onNext = { logD("Photo upload success? $it") }
74+
)
75+
}
76+
5977
private fun updateTriggerState(alarmTriggerState: AlarmTriggerState)
6078
= backendInteractor.updateAlarmState(alarmTriggerState)
6179

6280
fun onAppDestroy() {
6381
alarmArmingDisposable.dispose()
6482
alarmTriggerDisposable.dispose()
6583
backendConnectionDisposable.dispose()
66-
usbSetupProviderContract.unregisterBroadcastListener()
84+
usbSetupProvider.unregisterBroadcastListener()
6785
}
6886
}

app/src/main/java/pl/rmakowiecki/smartalarmcore/background/UsbStateBroadcastReceiver.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ private const val USB_STATE_CHANGE_ACTION = "android.hardware.usb.action.USB_STA
1010
private const val USB_DEVICE_ATTACHED_ACTION = "android.hardware.usb.action.USB_DEVICE_ATTACHED"
1111
private const val USB_DEVICE_DETACHED_ACTION = "android.hardware.usb.action.USB_DEVICE_DETACHED"
1212

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

1515
override fun onReceive(context: Context, intent: Intent) = when (intent.action) {
16-
USB_STATE_CHANGE_ACTION -> logD("USB state changed")
16+
USB_STATE_CHANGE_ACTION -> logD("USB state changed")
1717
USB_DEVICE_ATTACHED_ACTION -> onAttach()
1818
USB_DEVICE_DETACHED_ACTION -> onDetach()
1919
else -> Unit

app/src/main/java/pl/rmakowiecki/smartalarmcore/peripheral/beam/BeamBreakDetectorPeriphery.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class BeamBreakDetectorPeriphery : BeamBreakDetectorPeripheryContract {
2525
private val gpioStateListener = object : GpioCallback() {
2626
override fun onGpioEdge(gpio: Gpio): Boolean {
2727
statePublisher.onNext(gpio.value.toTriggerState())
28+
logD("GPIO 19 state changed to: ${gpio.value}")
2829
return true
2930
}
3031

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
package pl.rmakowiecki.smartalarmcore.peripheral.camera
22

33
import android.content.Context
4-
import android.hardware.camera2.CameraAccessException
5-
import android.hardware.camera2.CameraCharacteristics
6-
import android.hardware.camera2.CameraDevice
7-
import android.hardware.camera2.CameraManager
4+
import android.graphics.ImageFormat
5+
import android.hardware.camera2.*
6+
import android.media.ImageReader
7+
import android.media.ImageReader.OnImageAvailableListener
88
import android.os.Handler
99
import android.os.HandlerThread
10+
import io.reactivex.Observable
11+
import io.reactivex.subjects.PublishSubject
1012
import pl.rmakowiecki.smartalarmcore.extensions.logD
1113
import pl.rmakowiecki.smartalarmcore.extensions.logE
1214
import pl.rmakowiecki.smartalarmcore.extensions.logW
15+
import pl.rmakowiecki.smartalarmcore.extensions.printStackTrace
16+
import java.util.*
1317

1418
class CameraPeriphery(private var context: Context?) : CameraPeripheryContract {
19+
1520
private var backgroundThread: HandlerThread? = null
1621
private var backgroundHandler: Handler? = null
17-
private lateinit var cameraDevice: CameraDevice
18-
private lateinit var cameraId: String
22+
private var cameraDevice: CameraDevice? = null
23+
private var cameraCaptureSession: CameraCaptureSession? = null
24+
private var imageReadProcessor: ImageReader? = null
25+
private var cameraId: String = ""
26+
27+
private val photoPublishSubject: PublishSubject<ByteArray> = PublishSubject.create()
1928

20-
private val stateCallback = object : CameraDevice.StateCallback() {
29+
private val cameraStateCallback = object : CameraDevice.StateCallback() {
2130
override fun onOpened(camera: CameraDevice) {
2231
logD(javaClass.simpleName, "onCameraOpened")
2332
cameraDevice = camera
@@ -34,23 +43,98 @@ class CameraPeriphery(private var context: Context?) : CameraPeripheryContract {
3443
}
3544
}
3645

46+
private var cameraSessionCallback = object : CameraCaptureSession.StateCallback() {
47+
48+
override fun onConfigured(captureSession: CameraCaptureSession) {
49+
if (cameraDevice == null) {
50+
logE("The camera is already closed")
51+
return
52+
}
53+
54+
cameraCaptureSession = captureSession
55+
triggerImageCapture()
56+
}
57+
58+
override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) = logW("Failed to configure camera")
59+
}
60+
61+
private val cameraCaptureCallback = object : CameraCaptureSession.CaptureCallback() {
62+
override fun onCaptureCompleted(session: CameraCaptureSession?, request: CaptureRequest?, result: TotalCaptureResult?) {
63+
super.onCaptureCompleted(session, request, result)
64+
if (session != null) {
65+
session.close()
66+
cameraCaptureSession = null
67+
logD("CaptureSession closed")
68+
}
69+
}
70+
}
71+
72+
private val imageAvailabilityListener = OnImageAvailableListener { reader ->
73+
val image = reader.acquireLatestImage()
74+
val imageByteBuffer = image.planes[0].buffer
75+
val imageBytes = ByteArray(imageByteBuffer.remaining())
76+
imageByteBuffer.get(imageBytes)
77+
image.close()
78+
79+
onPictureTaken(imageBytes)
80+
}
81+
82+
private fun onPictureTaken(imageBytes: ByteArray?) {
83+
if (imageBytes != null) {
84+
photoPublishSubject.onNext(imageBytes)
85+
}
86+
}
87+
88+
private fun triggerImageCapture() {
89+
try {
90+
imageReadProcessor = ImageReader.newInstance(1280, 768,
91+
ImageFormat.JPEG, 10)
92+
imageReadProcessor?.setOnImageAvailableListener(imageAvailabilityListener, backgroundHandler)
93+
94+
val captureBuilder = cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
95+
captureBuilder?.addTarget(imageReadProcessor?.surface)
96+
captureBuilder?.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
97+
logD("Session initialized.")
98+
cameraCaptureSession?.capture(captureBuilder?.build(), cameraCaptureCallback, null)
99+
} catch (cameraAccessException: CameraAccessException) {
100+
printStackTrace(cameraAccessException)
101+
}
102+
103+
}
104+
37105
override fun openCamera() {
38106
val manager = context?.getSystemService(Context.CAMERA_SERVICE) as CameraManager
39107
logD("opening camera")
40108
try {
41109
cameraId = manager.cameraIdList[0]
42110
val cameraCharacteristics = manager.getCameraCharacteristics(cameraId)
43111
val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
44-
manager.openCamera(cameraId, stateCallback, null)
112+
manager.openCamera(cameraId, cameraStateCallback, null)
45113
} catch (ex: CameraAccessException) {
46114
ex.printStackTrace()
47115
}
48116
}
49117

50-
override fun closeCamera() {
51-
cameraDevice.close()
52-
context = null
118+
override fun capturePhoto(): Observable<ByteArray> {
119+
takePicture()
120+
return photoPublishSubject
121+
}
122+
123+
private fun takePicture() {
124+
if (cameraDevice == null) {
125+
logE("Cannot capture image. Camera not initialized.")
126+
return
127+
}
128+
129+
try {
130+
cameraDevice?.createCaptureSession(Collections.singletonList(imageReadProcessor?.surface), cameraSessionCallback, null)
131+
} catch (cameraAccessException: CameraAccessException) {
132+
printStackTrace(cameraAccessException)
133+
}
134+
53135
}
54136

55-
override fun captureFrame() = Unit
137+
override fun closeCamera() {
138+
cameraDevice?.close()
139+
}
56140
}

app/src/main/java/pl/rmakowiecki/smartalarmcore/peripheral/camera/CameraPeripheryContract.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package pl.rmakowiecki.smartalarmcore.peripheral.camera
22

33
import android.content.Context
4+
import io.reactivex.Observable
45

56
interface CameraPeripheryContract {
67
fun openCamera()
78
fun closeCamera()
8-
fun captureFrame()
9+
fun capturePhoto(): Observable<ByteArray>
910

1011
companion object {
1112
fun create(context: Context) = CameraPeriphery(context)

app/src/main/java/pl/rmakowiecki/smartalarmcore/remote/AlarmBackendContract.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface AlarmBackendContract {
1111
fun signInToBackend(): Single<Boolean>
1212
fun observeAlarmArmingState(): Observable<AlarmArmingState>
1313
fun updateAlarmState(alarmState: AlarmTriggerState)
14+
fun uploadPhoto(photo: ByteArray): Single<Boolean>
1415

1516
companion object {
1617
fun create(activity: AlarmActivity) = AlarmBackendInteractor(activity)

app/src/main/java/pl/rmakowiecki/smartalarmcore/remote/AlarmBackendInteractor.kt

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.google.firebase.database.DataSnapshot
77
import com.google.firebase.database.DatabaseError
88
import com.google.firebase.database.FirebaseDatabase
99
import com.google.firebase.database.ValueEventListener
10+
import com.google.firebase.storage.FirebaseStorage
1011
import io.reactivex.Observable
1112
import io.reactivex.Single
1213
import io.reactivex.SingleEmitter
@@ -17,15 +18,22 @@ import pl.rmakowiecki.smartalarmcore.extensions.logD
1718
import pl.rmakowiecki.smartalarmcore.extensions.printStackTrace
1819
import pl.rmakowiecki.smartalarmcore.toArmingState
1920

20-
class AlarmBackendInteractor(val activity: AlarmActivity) : AlarmBackendContract {
21+
private const val USERS_DIRECTORY = "users"
22+
private const val IMAGES_DIRECTORY = "images"
23+
private const val DIRECT_FILE_PATH = "profilepic.jpg"
24+
25+
class AlarmBackendInteractor(private val activity: AlarmActivity) : AlarmBackendContract {
2126

2227
private val databaseNode = FirebaseDatabase
2328
.getInstance()
2429
.reference
2530

31+
private val storageNode = FirebaseStorage
32+
.getInstance()
33+
.getReferenceFromUrl("gs://smartalarmcore.appspot.com")
34+
2635
override fun signInToBackend(): Single<Boolean> = Single.create { emitter ->
2736

28-
FirebaseAuth.getInstance().signOut()
2937
logD(Settings.Secure.getString(activity.contentResolver, Settings.Secure.ANDROID_ID))
3038

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

3846
private fun signUpAsAuthorizedUser(firebaseUser: FirebaseUser, emitter: SingleEmitter<Boolean>) {
39-
logD(firebaseUser.uid, "UID USERA")
4047
FirebaseAuth.getInstance().createUserWithEmailAndPassword(
4148
"${firebaseUser.uid}@smarthome.com",
4249
Settings.Secure.getString(activity.contentResolver, Settings.Secure.ANDROID_ID)
4350
).addOnSuccessListener {
4451
initializeCoreDeviceNoSqlModel(emitter)
45-
}.addOnFailureListener { printStackTrace(it) }
52+
}.addOnFailureListener(::printStackTrace)
4653
}
4754

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

56-
private fun getCurrentBackendUser(): FirebaseUser? {
57-
logD(FirebaseAuth.getInstance().currentUser, "GET UID")
58-
return FirebaseAuth.getInstance().currentUser
59-
}
63+
private fun getCurrentBackendUser() = FirebaseAuth.getInstance().currentUser
6064

6165
override fun observeAlarmArmingState(): Observable<AlarmArmingState> = Observable.create { emitter ->
6266
val valueListener = object : ValueEventListener {
@@ -82,6 +86,14 @@ class AlarmBackendInteractor(val activity: AlarmActivity) : AlarmBackendContract
8286
.setValue(alarmState.toBoolean())
8387
.addOnCompleteListener { }
8488
}
89+
90+
override fun uploadPhoto(photo: ByteArray): Single<Boolean> {
91+
storageNode.getReferenceFromUrl(STORAGE_URL)
92+
.child(USERS_DIRECTORY)
93+
.child(FirebaseUserReloader.reloadCurrentUser(firebaseAuth).getUid())
94+
.child(IMAGES_DIRECTORY)
95+
.child(DIRECT_FILE_PATH)
96+
}
8597
}
8698

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

0 commit comments

Comments
 (0)