Skip to content

Commit 273bcd8

Browse files
committed
Partly rework security incident hierarchy & write thumbnail generating function
1 parent c6b6c09 commit 273bcd8

File tree

9 files changed

+165
-19
lines changed

9 files changed

+165
-19
lines changed

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import pl.rmakowiecki.smartalarmcore.extensions.logD
88
import pl.rmakowiecki.smartalarmcore.peripheral.beam.BeamBreakDetectorPeripheryContract
99
import pl.rmakowiecki.smartalarmcore.peripheral.camera.CameraPeripheryContract
1010
import pl.rmakowiecki.smartalarmcore.remote.AlarmBackendContract
11+
import pl.rmakowiecki.smartalarmcore.remote.AlarmTriggerReason
12+
import pl.rmakowiecki.smartalarmcore.remote.SecurityIncident
1113
import pl.rmakowiecki.smartalarmcore.setup.UsbSetupProviderContract
1214

1315
class AlarmController(
@@ -58,27 +60,33 @@ class AlarmController(
5860
.applyIoSchedulers()
5961
.subscribeBy(
6062
onNext = {
61-
updateTriggerState(it)
63+
updateAlarmTriggerState(it)
6264
if (it == TRIGGERED) {
63-
capturePhoto()
65+
reportBeamBreakIncident()
6466
}
6567
}
6668
)
6769
}
6870
}
6971

70-
private fun capturePhoto() {
72+
private fun updateAlarmTriggerState(alarmTriggerState: AlarmTriggerState)
73+
= backendInteractor.updateAlarmState(alarmTriggerState)
74+
75+
private fun reportBeamBreakIncident() {
76+
val reportTimestamp = System.currentTimeMillis()
77+
7178
cameraPhotoSessionDisposable = camera.capturePhoto()
72-
.flatMapSingle(backendInteractor::uploadPhoto)
73-
.applyIoSchedulers()
79+
.flatMapSingle { backendInteractor.uploadIncidentPhoto(it, reportTimestamp) }
80+
.flatMapSingle {
81+
backendInteractor.reportSecurityIncident(
82+
SecurityIncident(AlarmTriggerReason.BEAM_BREAK_DETECTOR, reportTimestamp)
83+
)
84+
}.applyIoSchedulers()
7485
.subscribeBy(
75-
onNext = { logD("Photo upload success? $it") }
86+
onNext = { logD("Security incident report successful? $it") }
7687
)
7788
}
7889

79-
private fun updateTriggerState(alarmTriggerState: AlarmTriggerState)
80-
= backendInteractor.updateAlarmState(alarmTriggerState)
81-
8290
fun onAppDestroy() {
8391
alarmArmingDisposable.dispose()
8492
alarmTriggerDisposable.dispose()

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ interface AlarmBackendContract {
1111
fun signInToBackend(): Single<Boolean>
1212
fun observeAlarmArmingState(): Observable<AlarmArmingState>
1313
fun updateAlarmState(alarmState: AlarmTriggerState)
14-
fun uploadPhoto(photoBytes: ByteArray): Single<Boolean>
14+
fun reportSecurityIncident(securityIncident: SecurityIncident): Single<Boolean>
15+
fun uploadIncidentPhoto(photoBytes: ByteArray, reportTimestamp: Long): Single<Boolean>
1516

1617
companion object {
1718
fun create(activity: AlarmActivity) = AlarmBackendInteractor(activity)
1819
}
19-
}
2020

21+
}

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import pl.rmakowiecki.smartalarmcore.AlarmArmingState
1616
import pl.rmakowiecki.smartalarmcore.AlarmTriggerState
1717
import pl.rmakowiecki.smartalarmcore.extensions.logD
1818
import pl.rmakowiecki.smartalarmcore.extensions.printStackTrace
19+
import pl.rmakowiecki.smartalarmcore.remote.Nodes.ALARM_ARMING
20+
import pl.rmakowiecki.smartalarmcore.remote.Nodes.ALARM_STATE
21+
import pl.rmakowiecki.smartalarmcore.remote.Nodes.ALARM_TRIGGER
22+
import pl.rmakowiecki.smartalarmcore.remote.Nodes.CORE_DEVICE_DIRECTORY
23+
import pl.rmakowiecki.smartalarmcore.remote.Nodes.IMAGES_DIRECTORY
1924
import pl.rmakowiecki.smartalarmcore.toArmingState
2025
import java.util.*
2126

22-
private const val CORE_DEVICE_DIRECTORY = "core_assets"
23-
private const val IMAGES_DIRECTORY = "images"
24-
2527
class AlarmBackendInteractor(private val activity: AlarmActivity) : AlarmBackendContract {
2628

2729
private val databaseNode = FirebaseDatabase
@@ -54,6 +56,7 @@ class AlarmBackendInteractor(private val activity: AlarmActivity) : AlarmBackend
5456

5557
private fun initializeCoreDeviceNoSqlModel(emitter: SingleEmitter<Boolean>) = databaseNode
5658
.child(getCurrentBackendUser()?.uid)
59+
.child(ALARM_STATE)
5760
.setValue(RemoteAlarmStateModel(true, false))
5861
.addOnSuccessListener { emitter.onSuccess(true) }
5962

@@ -71,7 +74,8 @@ class AlarmBackendInteractor(private val activity: AlarmActivity) : AlarmBackend
7174

7275
val alarmArmingNode = databaseNode
7376
.child(getCurrentBackendUser()?.uid)
74-
.child(Nodes.ALARM_ARMING)
77+
.child(ALARM_STATE)
78+
.child(ALARM_ARMING)
7579

7680
alarmArmingNode.addValueEventListener(valueListener)
7781

@@ -82,15 +86,24 @@ class AlarmBackendInteractor(private val activity: AlarmActivity) : AlarmBackend
8286

8387
override fun updateAlarmState(alarmState: AlarmTriggerState) {
8488
databaseNode.child(getCurrentBackendUser()?.uid)
85-
.child(Nodes.ALARM_TRIGGER)
89+
.child(ALARM_STATE)
90+
.child(ALARM_TRIGGER)
8691
.setValue(alarmState.toBoolean())
8792
.addOnCompleteListener { }
8893
}
8994

90-
override fun uploadPhoto(photoBytes: ByteArray): Single<Boolean> = Single.create { emitter ->
95+
override fun reportSecurityIncident(securityIncident: SecurityIncident): Single<Boolean> = Single.create { emitter ->
96+
databaseNode.child(getCurrentBackendUser()?.uid)
97+
.child(Nodes.INCIDENTS)
98+
.push()
99+
.setValue(RemoteSecurityIncident.from(securityIncident))
100+
.addOnCompleteListener { emitter.onSuccess(it.isSuccessful) }
101+
}
102+
103+
override fun uploadIncidentPhoto(photoBytes: ByteArray, reportTimestamp: Long): Single<Boolean> = Single.create { emitter ->
91104
storageNode.child(CORE_DEVICE_DIRECTORY)
92105
.child(IMAGES_DIRECTORY)
93-
.child(getCurrentBackendUser()?.uid ?: "non_assignable_photos")
106+
.child(getCurrentBackendUser()?.uid ?: "non_assignable_incidents")
94107
.child("alarm_photo_${Calendar.getInstance().timeInMillis}.jpg")
95108
.putBytes(photoBytes)
96109
.addOnCompleteListener { emitter.onSuccess(it.isSuccessful) }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package pl.rmakowiecki.smartalarmcore.remote
2+
3+
enum class AlarmTriggerReason {
4+
BEAM_BREAK_DETECTOR,
5+
MOTION_SENSOR
6+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ package pl.rmakowiecki.smartalarmcore.remote
33
object Nodes {
44
const val ALARM_TRIGGER = "triggered"
55
const val ALARM_ARMING = "active"
6+
const val ALARM_STATE = "state"
7+
const val CORE_DEVICE_DIRECTORY = "core_assets"
8+
const val IMAGES_DIRECTORY = "images"
9+
const val INCIDENTS = "incidents"
610
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package pl.rmakowiecki.smartalarmcore.remote
2+
3+
class RemoteSecurityIncident private constructor(
4+
val reason: AlarmTriggerReason,
5+
val timestamp: Long,
6+
val thumbnailUrl: String = "",
7+
val archived: Boolean = false) {
8+
9+
companion object {
10+
fun from(securityIncident: SecurityIncident) = RemoteSecurityIncident(securityIncident.reason, securityIncident.timestamp)
11+
}
12+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package pl.rmakowiecki.smartalarmcore.remote
2+
3+
class SecurityIncident(
4+
val reason: AlarmTriggerReason,
5+
val timestamp: Long
6+
)

functions/index.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,96 @@ exports.sendAlarmNotification = functions.database.ref('/{currentCoreDeviceUid}/
5050
});
5151
});
5252
});
53+
});
54+
55+
56+
57+
'use strict';
58+
59+
const mkdirp = require('mkdirp-promise');
60+
// Include a Service Account Key to use a Signed URL
61+
const gcs = require('@google-cloud/storage')({keyFilename: 'service-account-credentials.json'});
62+
const spawn = require('child-process-promise').spawn;
63+
const path = require('path');
64+
const os = require('os');
65+
const fs = require('fs');
66+
67+
const THUMB_MAX_HEIGHT = 256;
68+
const THUMB_MAX_WIDTH = 256;
69+
const THUMB_PREFIX = 'thumb_';
70+
71+
exports.generateThumbnail = functions.storage.bucket('images').object().onChange(event => {
72+
// File and directory paths.
73+
const filePath = event.data.name;
74+
const fileDir = path.dirname(filePath);
75+
const fileName = path.basename(filePath);
76+
const thumbFilePath = path.normalize(path.join(fileDir, `${THUMB_PREFIX}${fileName}`));
77+
const tempLocalFile = path.join(os.tmpdir(), filePath);
78+
const tempLocalDir = path.dirname(tempLocalFile);
79+
const tempLocalThumbFile = path.join(os.tmpdir(), thumbFilePath);
80+
81+
// Exit if this is triggered on a file that is not an image.
82+
if (!event.data.contentType.startsWith('image/')) {
83+
console.log('This is not an image.');
84+
return;
85+
}
86+
87+
// Exit is this isn't a new file, when only metadata changed
88+
if (resourceState === 'exists' && metageneration > 1) {
89+
console.log('This is a metadata change event.');
90+
return;
91+
}
92+
93+
// Exit if the image is already a thumbnail.
94+
if (fileName.startsWith(THUMB_PREFIX)) {
95+
console.log('Already a Thumbnail.');
96+
return;
97+
}
98+
99+
// Exit if this is a move or deletion event.
100+
if (event.data.resourceState === 'not_exists') {
101+
console.log('This is a deletion event.');
102+
return;
103+
}
104+
105+
// Cloud Storage files.
106+
const bucket = gcs.bucket(event.data.bucket);
107+
const file = bucket.file(filePath);
108+
const thumbFile = bucket.file(thumbFilePath);
109+
110+
// Create the temp directory where the storage file will be downloaded.
111+
return mkdirp(tempLocalDir).then(() => {
112+
// Download file from bucket.
113+
return file.download({destination: tempLocalFile});
114+
}).then(() => {
115+
console.log('The file has been downloaded to', tempLocalFile);
116+
// Generate a thumbnail using ImageMagick.
117+
return spawn('convert', [tempLocalFile, '-thumbnail', `${THUMB_MAX_WIDTH}x${THUMB_MAX_HEIGHT}>`, tempLocalThumbFile]);
118+
}).then(() => {
119+
console.log('Thumbnail created at', tempLocalThumbFile);
120+
// Uploading the Thumbnail.
121+
return bucket.upload(tempLocalThumbFile, {destination: thumbFilePath});
122+
}).then(() => {
123+
console.log('Thumbnail uploaded to Storage at', thumbFilePath);
124+
// Once the image has been uploaded delete the local files to free up disk space.
125+
fs.unlinkSync(tempLocalFile);
126+
fs.unlinkSync(tempLocalThumbFile);
127+
// Get the Signed URLs for the thumbnail and original image.
128+
const config = {
129+
action: 'read',
130+
expires: '03-01-2500'
131+
};
132+
return Promise.all([
133+
thumbFile.getSignedUrl(config),
134+
file.getSignedUrl(config)
135+
]);
136+
}).then(results => {
137+
console.log('Got Signed URLs.');
138+
const thumbResult = results[0];
139+
const originalResult = results[1];
140+
const thumbFileUrl = thumbResult[0];
141+
const fileUrl = originalResult[0];
142+
// Add the URLs to the Database
143+
return admin.database().ref('images').push({path: fileUrl, thumbnail: thumbFileUrl});
144+
}).then(() => console.log('Thumbnail URLs saved to database.'));
53145
});

functions/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
"description": "Cloud Functions for Firebase",
44
"dependencies": {
55
"firebase-admin": "~4.2.1",
6-
"firebase-functions": "^0.5.7"
6+
"firebase-functions": "^0.5.7",
7+
"@google-cloud/storage": "^0.4.0",
8+
"child-process-promise": "^2.2.0",
9+
"mkdirp": "^0.5.1",
10+
"mkdirp-promise": "^4.0.0"
711
},
812
"private": true
913
}

0 commit comments

Comments
 (0)