Skip to content

Commit 008dcbd

Browse files
committed
Overhaul how we forcibly open apps to the foreground
1 parent da8a1d0 commit 008dcbd

File tree

8 files changed

+98
-51
lines changed

8 files changed

+98
-51
lines changed

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,21 @@ Similar concept to [Yakk](https://yakk.bkappz.com/), but open-source with more s
2323
- Camera motion detection with adjustable sensitivity
2424
- Ambient light sensor for detecting lighting changes
2525
- Auto-unlock screen and launch your dashboard app
26+
- Automatic keyguard dismissal (works with swipe-to-unlock and no security)
27+
- Configurable notification sound on wake
2628
- Start at boot for set-and-forget operation
27-
- Material Design 3 with dark mode and tablet layouts
29+
- Clean, simple UI with tablet layouts
2830
- Full accessibility support with TalkBack
2931

30-
## Setup Recommendations
32+
## Permissions
3133

32-
**For best results, disable your device's lock screen security:**
34+
On first launch, the app will request the following permissions:
3335

34-
Go to **Settings → Security → Screen Lock** and set it to **"None"** or **"Swipe"**.
36+
- **Camera** - For motion detection
37+
- **Display over other apps** (SYSTEM_ALERT_WINDOW) - Required to reliably launch apps from background on Android 10+
38+
- **Battery optimization exemption** - For reliable background operation
3539

36-
**Why?** Android's security model prevents apps from bypassing PIN, pattern, or biometric authentication. If you have secure lock screen enabled, the app will wake the screen but you'll still need to authenticate manually. For wall-mounted tablets that don't leave your home, disabling the lock screen provides the smoothest experience.
37-
38-
If you need security, consider using Smart Lock (Settings → Security → Smart Lock) with trusted locations or devices instead.
40+
These permissions are essential for the app to function properly.
3941

4042
## Building
4143

app/build.gradle.kts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ android {
1212
applicationId = "com.observer.effect"
1313
minSdk = 26
1414
targetSdk = 34
15-
versionCode = 9
16-
versionName = "0.7.2"
15+
versionCode = 10
16+
versionName = "0.7.3"
1717
}
1818

1919
signingConfigs {
2020
create("release") {
2121
// Use keystore from environment variable or default locations
22-
val keystorePath = System.getenv("ANDROID_KEYSTORE")
23-
?: "${System.getProperty("user.home")}/android.jks"
22+
val keystorePath =
23+
System.getenv("ANDROID_KEYSTORE")
24+
?: "${System.getProperty("user.home")}/android.jks"
2425
val keystoreFile = file(keystorePath)
2526

2627
if (keystoreFile.exists()) {
1 Byte
Binary file not shown.
5 Bytes
Binary file not shown.

app/release/output-metadata.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
"type": "APK",
55
"kind": "Directory"
66
},
7-
"applicationId": "com.heisenberg.lux",
7+
"applicationId": "com.observer.effect",
88
"variantName": "release",
99
"elements": [
1010
{
1111
"type": "SINGLE",
1212
"filters": [],
1313
"attributes": [],
14-
"versionCode": 8,
15-
"versionName": "0.7.1",
14+
"versionCode": 9,
15+
"versionName": "0.7.2",
1616
"outputFile": "app-release.apk"
1717
}
1818
],

app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
88
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
99
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
10+
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
1011

1112
<uses-feature android:name="android.hardware.camera" android:required="false" />
1213
<uses-feature android:name="android.hardware.sensor.light" android:required="false" />
@@ -35,6 +36,14 @@
3536
</intent-filter>
3637
</activity>
3738

39+
<!-- Transparent activity for dismissing keyguard and launching target app -->
40+
<activity
41+
android:name=".LauncherActivity"
42+
android:excludeFromRecents="true"
43+
android:exported="false"
44+
android:launchMode="singleTask"
45+
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
46+
3847
<service
3948
android:name=".DetectionService"
4049
android:enabled="true"

app/src/main/kotlin/com/observer/effect/DetectionService.kt

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.observer.effect
22

3-
import android.app.ActivityManager
43
import android.app.KeyguardManager
54
import android.app.Notification
65
import android.app.NotificationChannel
@@ -11,7 +10,6 @@ import android.content.Context
1110
import android.content.Intent
1211
import android.content.IntentFilter
1312
import android.content.SharedPreferences
14-
import android.os.Build
1513
import android.os.IBinder
1614
import android.os.PowerManager
1715
import android.util.Log
@@ -266,52 +264,74 @@ class DetectionService : Service(), LifecycleOwner {
266264
try {
267265
// Don't auto-open if screen is already on (user is actively using device)
268266
if (powerManager.isInteractive) {
269-
Log.d(TAG, "Screen already on, skipping auto-open")
267+
Log.d(TAG, "Screen already on, skipping wake operation")
270268
return
271269
}
272270

271+
// Check if wake lock is already held to prevent double-wake
272+
if (wakeLock.isHeld) {
273+
Log.d(TAG, "Wake lock already held, skipping duplicate wake operation")
274+
return
275+
}
276+
277+
Log.i(TAG, "Motion/light detected - initiating screen wake sequence")
278+
273279
// CRITICAL FIX: Use acquire with timeout, don't release immediately
274280
// The timeout will auto-release after WAKE_DURATION_MS
275-
if (!wakeLock.isHeld) {
276-
Log.i(TAG, "Waking screen")
277-
wakeLock.acquire(WAKE_DURATION_MS)
278-
// Don't call release() - the timeout handles it automatically
279-
280-
// Play notification sound if configured
281-
val notificationSoundUri = prefs.getString(MainActivity.KEY_NOTIFICATION_SOUND, "") ?: ""
282-
if (notificationSoundUri.isNotEmpty()) {
283-
try {
284-
val ringtone = android.media.RingtoneManager.getRingtone(this, android.net.Uri.parse(notificationSoundUri))
285-
ringtone?.play()
286-
Log.i(TAG, "Playing notification sound: $notificationSoundUri")
287-
} catch (e: Exception) {
288-
Log.e(TAG, "Error playing notification sound", e)
281+
wakeLock.acquire(WAKE_DURATION_MS)
282+
Log.d(TAG, "Wake lock acquired for ${WAKE_DURATION_MS}ms")
283+
284+
// Play notification sound if configured
285+
val notificationSoundUri = prefs.getString(MainActivity.KEY_NOTIFICATION_SOUND, "") ?: ""
286+
if (notificationSoundUri.isNotEmpty()) {
287+
try {
288+
Log.d(TAG, "Playing notification sound")
289+
val ringtone = android.media.RingtoneManager.getRingtone(this, android.net.Uri.parse(notificationSoundUri))
290+
if (ringtone != null) {
291+
ringtone.play()
292+
Log.i(TAG, "Notification sound started: $notificationSoundUri")
293+
} else {
294+
Log.w(TAG, "Ringtone object is null for URI: $notificationSoundUri")
289295
}
296+
} catch (e: SecurityException) {
297+
Log.e(TAG, "Security exception playing notification sound - missing permissions?", e)
298+
} catch (e: Exception) {
299+
Log.e(TAG, "Unexpected error playing notification sound: $notificationSoundUri", e)
290300
}
301+
} else {
302+
Log.d(TAG, "No notification sound configured")
303+
}
304+
305+
// Launch app if configured
306+
val launchApp = prefs.getString(MainActivity.KEY_LAUNCH_APP, "") ?: ""
307+
if (launchApp.isNotEmpty()) {
308+
try {
309+
Log.i(
310+
TAG,
311+
"Initiating app launch: package=$launchApp, keyguardLocked=${keyguardManager.isKeyguardLocked}",
312+
)
291313

292-
// Launch app if configured
293-
val launchApp = prefs.getString(MainActivity.KEY_LAUNCH_APP, "") ?: ""
294-
if (launchApp.isNotEmpty()) {
295-
try {
296-
Log.i(TAG, "Bringing app to foreground: $launchApp (locked=${keyguardManager.isKeyguardLocked})")
297-
298-
val launchIntent = packageManager.getLaunchIntentForPackage(launchApp)
299-
if (launchIntent != null) {
300-
// Use CLEAR_TASK + NEW_TASK to forcefully bring the app to foreground
301-
// This clears the existing task and creates a fresh one on top
302-
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
303-
startActivity(launchIntent)
304-
Log.i(TAG, "Launched app with CLEAR_TASK flags=${launchIntent.flags}")
305-
} else {
306-
Log.w(TAG, "No launch intent found for package: $launchApp")
314+
// Use our transparent LauncherActivity to properly dismiss keyguard
315+
// and then launch the target app
316+
val launcherIntent =
317+
Intent(this, LauncherActivity::class.java).apply {
318+
putExtra(LauncherActivity.EXTRA_TARGET_PACKAGE, launchApp)
319+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
307320
}
308-
} catch (e: Exception) {
309-
Log.e(TAG, "Error launching app: $launchApp", e)
310-
}
321+
startActivity(launcherIntent)
322+
Log.i(TAG, "LauncherActivity started successfully")
323+
} catch (e: SecurityException) {
324+
Log.e(TAG, "Security exception starting LauncherActivity - missing permissions?", e)
325+
} catch (e: Exception) {
326+
Log.e(TAG, "Unexpected error starting LauncherActivity for app: $launchApp", e)
311327
}
328+
} else {
329+
Log.d(TAG, "No launch app configured")
312330
}
331+
} catch (e: SecurityException) {
332+
Log.e(TAG, "Security exception during screen wake - missing WAKE_LOCK permission?", e)
313333
} catch (e: Exception) {
314-
Log.e(TAG, "Error waking screen", e)
334+
Log.e(TAG, "Unexpected error during screen wake sequence", e)
315335
}
316336
}
317337

app/src/main/kotlin/com/observer/effect/MainActivity.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,23 @@ class MainActivity : AppCompatActivity() {
284284
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
285285
}
286286

287-
// Request battery optimization exemption for reliable background operation
287+
// Request SYSTEM_ALERT_WINDOW permission for reliable app launching from background
288288
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
289+
if (!android.provider.Settings.canDrawOverlays(this)) {
290+
try {
291+
val intent =
292+
Intent(
293+
android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
294+
android.net.Uri.parse("package:$packageName"),
295+
)
296+
startActivity(intent)
297+
Log.i(TAG, "Requesting overlay permission for background activity launch")
298+
} catch (e: Exception) {
299+
Log.e(TAG, "Error requesting overlay permission", e)
300+
}
301+
}
302+
303+
// Request battery optimization exemption for reliable background operation
289304
val powerManager = getSystemService(Context.POWER_SERVICE) as android.os.PowerManager
290305
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
291306
try {

0 commit comments

Comments
 (0)