Skip to content

Commit b5cafdb

Browse files
authored
Merge branch 'main' into renovate/kotlinx.serialization
2 parents 0b01542 + 46618ff commit b5cafdb

181 files changed

Lines changed: 2511 additions & 777 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ on:
99
jobs:
1010
build:
1111
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
api-level: [ 34 ]
1215

1316
steps:
1417
- uses: actions/checkout@v4
@@ -33,12 +36,50 @@ jobs:
3336
3437
- name: Run Unit Tests
3538
run: ./gradlew test
36-
37-
# - name: Run Instrumentation Tests
38-
# uses: reactivecircus/android-emulator-runner@v2
39-
# with:
40-
# api-level: 35
41-
# target: default
42-
# arch: x86_64
43-
# profile: Nexus 6
44-
# script: ./gradlew connectedCheck
39+
40+
# - name: Enable KVM
41+
# run: |
42+
# echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
43+
# sudo udevadm control --reload-rules
44+
# sudo udevadm trigger --name-match=kvm
45+
#
46+
# - name: AVD cache
47+
# uses: actions/cache@v4
48+
# id: avd-cache
49+
# with:
50+
# path: |
51+
# ~/.android/avd/*
52+
# ~/.android/adb*
53+
# key: avd-${{ matrix.api-level }}
54+
#
55+
# - name: create AVD and generate snapshot for caching
56+
# if: steps.avd-cache.outputs.cache-hit != 'true'
57+
# uses: reactivecircus/android-emulator-runner@v2
58+
# with:
59+
# api-level: ${{ matrix.api-level }}
60+
# arch: x86_64
61+
# target: default
62+
# force-avd-creation: false
63+
# disable-animations: false
64+
# script: echo "Generated AVD snapshot for caching."
65+
#
66+
# - name: Run Instrumentation Tests
67+
# uses: reactivecircus/android-emulator-runner@v2
68+
# with:
69+
# api-level: ${{ matrix.api-level }}
70+
# arch: x86_64
71+
# target: default
72+
# force-avd-creation: false
73+
# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-front emulated -camera-back emulated
74+
# disable-animations: true
75+
# script: |
76+
# adb wait-for-device
77+
# adb shell input keyevent 82
78+
# ./gradlew connectedOssDebugAndroidTest --continue
79+
80+
- name: Upload Test Reports
81+
if: failure()
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: android-test-report
85+
path: '**/build/reports/'

.github/workflows/publish-release.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,21 @@ jobs:
3737
- name: Run Unit Tests
3838
run: ./gradlew test
3939

40-
- name: Build Signed Release APK
40+
- name: Build Signed Full Release APK
4141
env:
4242
KEYSTORE_FILE: gh-release-keystore.jks
4343
KEYSTORE_PASSWORD: ${{ secrets.GH_RELEASE_KEYSTORE_PASSWORD }}
4444
KEY_ALIAS: ${{ secrets.GH_RELEASE_KEY_ALIAS }}
4545
KEY_PASSWORD: ${{ secrets.GH_RELEASE_KEY_PASSWORD }}
46-
run: ./gradlew assembleRelease
46+
run: ./gradlew assembleFullRelease
47+
48+
- name: Build Signed OSS Release APK
49+
env:
50+
KEYSTORE_FILE: gh-release-keystore.jks
51+
KEYSTORE_PASSWORD: ${{ secrets.GH_RELEASE_KEYSTORE_PASSWORD }}
52+
KEY_ALIAS: ${{ secrets.GH_RELEASE_KEY_ALIAS }}
53+
KEY_PASSWORD: ${{ secrets.GH_RELEASE_KEY_PASSWORD }}
54+
run: ./gradlew assembleOssRelease
4755

4856
- name: Get version name
4957
id: get_version
@@ -58,7 +66,9 @@ jobs:
5866
name: Release ${{ env.VERSION_NAME }}
5967
draft: false
6068
prerelease: false
61-
files: app/build/outputs/apk/release/app-release.apk
69+
files: |
70+
app/build/outputs/apk/full/release/app-full-release.apk
71+
app/build/outputs/apk/oss/release/app-oss-release.apk
6272
token: ${{ secrets.GITHUB_TOKEN }}
6373

6474
build-and-publish-play-store:
@@ -94,13 +104,13 @@ jobs:
94104
run: |
95105
echo $ENCODED_KEYSTORE | base64 -d > app/keystore.jks
96106
97-
- name: Build Release AAB
107+
- name: Build Full Release AAB
98108
env:
99109
KEYSTORE_FILE: keystore.jks
100110
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
101111
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
102112
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
103-
run: ./gradlew bundleRelease
113+
run: ./gradlew bundleFullRelease
104114

105115
- name: Setup Fastlane
106116
run: |

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
*The camera that minds its own business.*
44

5-
[![codebeat badge](https://codebeat.co/badges/ca9cc888-4897-46a2-ba22-809a778ab57d)](https://codebeat.co/projects/github-com-wavesonics-securecamera-main)
5+
[![codebeat badge](https://codebeat.co/badges/1d47f0fa-2155-4e63-85ba-aafd01812d8c)](https://codebeat.co/projects/github-com-securecamera-securecameraandroid-main)
66

77
_Available on:_
88

99
[![Google Play](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dcom.darkrockstudios.app.securecamera%26l%3DGoogle%2520Play%26m%3D%24version)](https://play.google.com/store/apps/details?id=com.darkrockstudios.app.securecamera)
10-
[![GitHub](https://img.shields.io/github/v/release/Wavesonics/SecureCamera?include_prereleases&logo=github)](https://github.com/Wavesonics/SecureCamera/releases/latest)
10+
[![GitHub](https://img.shields.io/github/v/release/SecureCamera/SecureCameraAndroid?include_prereleases&logo=github)](https://github.com/SecureCamera/SecureCameraAndroid/releases/latest)
1111

1212
**SnapSafe** is an Android camera app that keeps every pixel—and every byte of data—exactly where it belongs: on **your
1313
**

app/build.gradle.kts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ android {
3636
}
3737
}
3838

39+
flavorDimensions += "version"
40+
productFlavors {
41+
create("oss") {
42+
dimension = "version"
43+
}
44+
create("full") {
45+
dimension = "version"
46+
}
47+
}
3948
buildTypes {
4049
release {
4150
isMinifyEnabled = true
@@ -55,6 +64,7 @@ android {
5564
}
5665
kotlinOptions {
5766
jvmTarget = libs.versions.javaVersion.get()
67+
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.time.ExperimentalTime"
5868
}
5969
buildFeatures {
6070
compose = true
@@ -103,9 +113,11 @@ dependencies {
103113
implementation(libs.androidx.lifecycle.runtime.compose)
104114
implementation(libs.zoomable)
105115
implementation(libs.androidx.runtime.livedata)
106-
implementation(libs.face.detection)
107116
implementation(libs.bcrypt)
108117
implementation(libs.androidx.work.runtime.ktx)
118+
implementation(libs.argon2kt)
119+
120+
"fullImplementation"(libs.face.detection)
109121

110122
testImplementation(libs.junit)
111123
testImplementation(libs.koin.test.junit4)
@@ -114,11 +126,13 @@ dependencies {
114126
testImplementation(libs.kotlinx.coroutines.test)
115127
testImplementation(kotlin("test"))
116128
androidTestImplementation(libs.androidx.junit)
117-
androidTestImplementation(libs.androidx.espresso.core)
129+
androidTestImplementation(libs.androidx.rules)
118130
androidTestImplementation(platform(libs.androidx.compose.bom))
119131
androidTestImplementation(libs.androidx.ui.test.junit4)
120132
androidTestImplementation(libs.mockk.android)
121133
androidTestImplementation(libs.mockk.agent)
134+
androidTestImplementation(libs.ui.test.junit4)
135+
debugImplementation(libs.ui.test.manifest)
122136
debugImplementation(libs.androidx.ui.tooling)
123137
debugImplementation(libs.androidx.ui.test.manifest)
124138
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.darkrockstudios.app.securecamera
2+
3+
import android.app.Application
4+
import android.content.res.Resources
5+
import androidx.annotation.StringRes
6+
import androidx.compose.ui.semantics.Role
7+
import androidx.compose.ui.semantics.SemanticsProperties
8+
import androidx.compose.ui.test.*
9+
import androidx.compose.ui.test.junit4.ComposeContentTestRule
10+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
11+
import androidx.test.core.app.ApplicationProvider
12+
import androidx.test.rule.GrantPermissionRule
13+
import org.junit.Rule
14+
import org.junit.Test
15+
import kotlin.time.Duration
16+
import kotlin.time.Duration.Companion.seconds
17+
18+
19+
class SmokeTestUiTest {
20+
21+
@get:Rule
22+
val permissionsRule = GrantPermissionRule.grant(
23+
android.Manifest.permission.POST_NOTIFICATIONS,
24+
android.Manifest.permission.ACCESS_FINE_LOCATION,
25+
android.Manifest.permission.CAMERA
26+
)
27+
28+
@get:Rule
29+
val composeTestRule = createAndroidComposeRule<MainActivity>()
30+
31+
@Test
32+
fun smokeTest() {
33+
composeTestRule.apply {
34+
onNodeWithText(str(R.string.intro_next)).performClick()
35+
onNodeWithText(str(R.string.intro_slide1_title)).assertIsDisplayed()
36+
37+
onNodeWithText(str(R.string.intro_next)).performClick()
38+
onNodeWithText(str(R.string.intro_slide2_title)).assertIsDisplayed()
39+
40+
onNodeWithText(str(R.string.intro_skip)).performClick()
41+
onNodeWithText(str(R.string.security_intro_supported_security_label)).assertIsDisplayed()
42+
43+
onNodeWithText(str(R.string.intro_next)).performClick()
44+
onNodeWithText(str(R.string.pin_creation_title)).assertIsDisplayed()
45+
46+
setPinFields("3133734", "313373")
47+
onNodeWithText(str(R.string.pin_creation_button)).performClick()
48+
waitForText(R.string.pin_creation_error)
49+
50+
setPinFields("123456", "123456")
51+
onNodeWithText(str(R.string.pin_creation_button)).performClick()
52+
waitForText(R.string.pin_creation_error_weak_pin)
53+
54+
setPinFields("313373", "313373")
55+
onNodeWithText(str(R.string.pin_creation_button)).performClick()
56+
57+
waitForText(R.string.pin_creating_vault)
58+
59+
composeTestRule.waitUntil(
60+
timeoutMillis = 30.seconds.inWholeMilliseconds
61+
) {
62+
composeTestRule
63+
.onAllNodes(hasRole(Role.Button) and hasContentDescription(str(R.string.camera_shutter_button_desc)))
64+
.fetchSemanticsNodes().isNotEmpty()
65+
}
66+
67+
onNode(
68+
hasRole(Role.Button) and hasContentDescription(str(R.string.camera_shutter_button_desc))
69+
).assertExists()
70+
}
71+
}
72+
73+
private fun ComposeContentTestRule.setPinFields(primary: String, confirm: String) {
74+
setTextField(
75+
placeholder = R.string.pin_creation_hint,
76+
value = primary,
77+
)
78+
79+
setTextField(
80+
placeholder = R.string.pin_creation_confirm_hint,
81+
value = confirm,
82+
)
83+
}
84+
85+
fun hasRole(role: Role): SemanticsMatcher =
86+
SemanticsMatcher.expectValue(SemanticsProperties.Role, role)
87+
88+
private fun str(@StringRes id: Int): String = r.getString(id)
89+
private val r: Resources
90+
get() {
91+
val application = ApplicationProvider.getApplicationContext<Application>()
92+
return application.resources
93+
}
94+
95+
private fun ComposeContentTestRule.waitForText(@StringRes text: Int, timeout: Duration = 10.seconds) {
96+
waitForText(str(text), timeout)
97+
}
98+
99+
fun ComposeContentTestRule.waitForText(
100+
text: String,
101+
timeout: Duration = 10.seconds,
102+
useUnmergedTree: Boolean = true,
103+
substring: Boolean = true
104+
) {
105+
waitUntil(timeout.inWholeMilliseconds) {
106+
onAllNodes(
107+
hasText(text, substring = substring),
108+
useUnmergedTree = useUnmergedTree
109+
).fetchSemanticsNodes().isNotEmpty()
110+
}
111+
onNodeWithText(text, substring = substring)
112+
.assertIsDisplayed()
113+
}
114+
115+
private fun ComposeContentTestRule.setTextField(value: String, placeholder: Int) {
116+
onNode(
117+
hasSetTextAction() and hasTextExactly(
118+
str(placeholder),
119+
includeEditableText = false
120+
)
121+
).apply {
122+
performTextClearance()
123+
performTextInput(value)
124+
}
125+
}
126+
}

app/src/full/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
5+
<application
6+
android:name=".FullSnapSafeApplication"
7+
tools:replace="android:name">
8+
</application>
9+
10+
</manifest>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.darkrockstudios.app.securecamera
2+
3+
import com.darkrockstudios.app.securecamera.obfuscation.FacialDetection
4+
import com.darkrockstudios.app.securecamera.obfuscation.MlFacialDetection
5+
import org.koin.core.module.Module
6+
import org.koin.core.module.dsl.factoryOf
7+
import org.koin.dsl.bind
8+
import org.koin.dsl.module
9+
10+
class FullSnapSafeApplication : SnapSafeApplication() {
11+
override fun flavorModule(): Module = module {
12+
factoryOf(::MlFacialDetection) bind FacialDetection::class
13+
}
14+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.darkrockstudios.app.securecamera.obfuscation
2+
3+
import android.graphics.Bitmap
4+
import com.google.mlkit.vision.common.InputImage
5+
import com.google.mlkit.vision.face.FaceDetection
6+
import com.google.mlkit.vision.face.FaceDetectorOptions
7+
import com.google.mlkit.vision.face.FaceLandmark
8+
import kotlinx.coroutines.suspendCancellableCoroutine
9+
import timber.log.Timber
10+
import kotlin.coroutines.resume
11+
12+
class MlFacialDetection : FacialDetection {
13+
private val detector = FaceDetection.getClient(
14+
FaceDetectorOptions.Builder()
15+
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
16+
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
17+
.setMinFaceSize(0.02f)
18+
.build()
19+
)
20+
21+
override suspend fun processForFaces(bitmap: Bitmap): List<FacialDetection.FoundFace> {
22+
val inputImage = InputImage.fromBitmap(bitmap, 0)
23+
24+
return suspendCancellableCoroutine { continuation ->
25+
detector.process(inputImage)
26+
.addOnSuccessListener { foundFaces ->
27+
val newRegions = foundFaces.map { face ->
28+
val leftEye =
29+
face.allLandmarks.find { it.landmarkType == FaceLandmark.LEFT_EYE }
30+
val rightEye =
31+
face.allLandmarks.find { it.landmarkType == FaceLandmark.RIGHT_EYE }
32+
val eyes = if (leftEye != null && rightEye != null) {
33+
FacialDetection.FoundFace.Eyes(
34+
left = leftEye.position,
35+
right = rightEye.position,
36+
)
37+
} else {
38+
null
39+
}
40+
FacialDetection.FoundFace(
41+
boundingBox = face.boundingBox,
42+
eyes = eyes
43+
)
44+
}
45+
continuation.resume(newRegions)
46+
}.addOnFailureListener { e ->
47+
Timber.Forest.e(e, "Failed face detection in Image")
48+
continuation.resume(emptyList())
49+
}
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)