Skip to content

Commit f73f14f

Browse files
committed
Rename to The Observer Effect
1 parent 7b0c172 commit f73f14f

File tree

13 files changed

+141
-57
lines changed

13 files changed

+141
-57
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
**Heisenberg's Lux** is a minimalist Android app (Kotlin, min SDK 26) that wakes the device screen when motion or ambient light changes are detected. The name is a playful reference to Heisenberg's uncertainty principle (you can't observe without affecting the system) combined with "lux" (the SI unit for illuminance). The app is designed as a canonical example of modern Android development with minimal dependencies and maximum simplicity.
7+
**Observer Effect** (displayed as "The Observer Effect" in the app) is a minimalist Android app (Kotlin, min SDK 26) that wakes the device screen when motion or ambient light changes are detected. The name refers to the observer effect in physics, where the act of observation changes what is being observed—in this case, your approach to the tablet causes it to wake up. The app is designed as a canonical example of modern Android development with minimal dependencies and maximum simplicity.
88

99
**Core Philosophy**: Simplicity, robustness, minimal dependencies. Use traditional Android Views, not Jetpack Compose. This app should be elegant enough to serve as a reference implementation.
1010

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
# Heisenberg's Lux
1+
# The Observer Effect
22

33
I wanted my wall-mounted home automation tablets to light up as I walked by. This app wakes the screen when it detects motion or light changes, and can optionally launch any app you want.
44

55
Works great for Home Assistant dashboards, Alarmo alarm panels, or any app you want to see when you approach a mounted tablet. No more tapping the screen to wake it.
66

7-
The name is a nod to Heisenberg's uncertainty principle (you can't observe without affecting the system) combined with "lux" (the SI unit for illuminance).
7+
Similar concept to [Yakk](https://yakk.bkappz.com/), but open-source and with more sensors (camera + light) and the ability to auto-unlock and launch apps.
8+
9+
The name refers to the observer effect in physics, where the act of observation changes what is being observed—in this case, your approach to the tablet causes it to wake up.
810

911
## Screenshots
1012

@@ -25,6 +27,16 @@ The name is a nod to Heisenberg's uncertainty principle (you can't observe witho
2527
- Material Design 3 with dark mode and tablet layouts
2628
- Full accessibility support with TalkBack
2729

30+
## Setup Recommendations
31+
32+
**For best results, disable your device's lock screen security:**
33+
34+
Go to **Settings → Security → Screen Lock** and set it to **"None"** or **"Swipe"**.
35+
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.
39+
2840
## Building
2941

3042
```bash

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
android:icon="@mipmap/ic_launcher"
2424
android:label="@string/app_name"
2525
android:supportsRtl="true"
26-
android:theme="@style/Theme.HeisenbergLux">
26+
android:theme="@style/Theme.ObserverEffect">
2727

2828
<activity
2929
android:name=".MainActivity"

app/src/main/kotlin/com/heisenberg/lux/DetectionService.kt

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class DetectionService : Service(), LifecycleOwner {
7777
wakeLock =
7878
powerManager.newWakeLock(
7979
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
80-
"HeisenbergLux::WakeLock",
80+
"ObserverEffect::WakeLock",
8181
)
8282

8383
// Register screen on/off receiver
@@ -266,19 +266,26 @@ class DetectionService : Service(), LifecycleOwner {
266266
wakeLock.acquire(WAKE_DURATION_MS)
267267
// Don't call release() - the timeout handles it automatically
268268

269-
// Dismiss keyguard if user has enabled bypass lock screen
270-
val unlockScreen = prefs.getBoolean(MainActivity.KEY_BYPASS_LOCK_SCREEN, true)
271-
if (unlockScreen) {
272-
if (keyguardManager.isKeyguardLocked) {
273-
Log.i(TAG, "Bypassing lock screen via transparent activity")
274-
// Launch a transparent activity that shows on lock screen and dismisses itself
275-
val intent = Intent(this, UnlockActivity::class.java).apply {
276-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
277-
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
278-
}
279-
startActivity(intent)
269+
// Play notification sound if configured
270+
val notificationSoundUri = prefs.getString(MainActivity.KEY_NOTIFICATION_SOUND, "") ?: ""
271+
if (notificationSoundUri.isNotEmpty()) {
272+
try {
273+
val ringtone = android.media.RingtoneManager.getRingtone(this, android.net.Uri.parse(notificationSoundUri))
274+
ringtone?.play()
275+
Log.i(TAG, "Playing notification sound: $notificationSoundUri")
276+
} catch (e: Exception) {
277+
Log.e(TAG, "Error playing notification sound", e)
280278
}
281279
}
280+
281+
// Always launch unlock activity to dismiss lock screen and/or launch app
282+
val launchApp = prefs.getString(MainActivity.KEY_LAUNCH_APP, "") ?: ""
283+
Log.i(TAG, "Launching unlock activity (app=$launchApp, locked=${keyguardManager.isKeyguardLocked})")
284+
val intent = Intent(this, UnlockActivity::class.java).apply {
285+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
286+
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
287+
}
288+
startActivity(intent)
282289
}
283290
} catch (e: Exception) {
284291
Log.e(TAG, "Error waking screen", e)

app/src/main/kotlin/com/heisenberg/lux/MainActivity.kt

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class MainActivity : AppCompatActivity() {
6565

6666
prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
6767

68-
// On first run, detect and save default camera (but keep sensitivity at 0 for safety)
68+
// On first run, detect and save defaults (but keep sensitivity at 0 for safety)
6969
val isFirstRun = !prefs.contains(KEY_CAMERA_SELECTION)
7070
if (isFirstRun) {
7171
val defaultCamera = getDefaultCamera()
@@ -78,8 +78,8 @@ class MainActivity : AppCompatActivity() {
7878
val cameraSensitivity = prefs.getInt(KEY_CAMERA_SENSITIVITY, 0)
7979
val lightSensitivity = prefs.getInt(KEY_LIGHT_SENSITIVITY, 0)
8080
val startAtBoot = prefs.getBoolean(KEY_START_AT_BOOT, false)
81-
val unlockScreen = prefs.getBoolean(KEY_BYPASS_LOCK_SCREEN, true)
8281
val launchApp = prefs.getString(KEY_LAUNCH_APP, "") ?: ""
82+
val notificationSound = prefs.getString(KEY_NOTIFICATION_SOUND, "") ?: ""
8383

8484
with(binding) {
8585
// Setup camera spinner
@@ -198,13 +198,6 @@ class MainActivity : AppCompatActivity() {
198198
Log.i(TAG, "Start at boot set to $isChecked")
199199
}
200200

201-
// Setup unlock screen checkbox
202-
unlockScreenCheckbox?.isChecked = unlockScreen
203-
unlockScreenCheckbox?.setOnCheckedChangeListener { _, isChecked ->
204-
prefs.edit().putBoolean(KEY_BYPASS_LOCK_SCREEN, isChecked).apply()
205-
Log.i(TAG, "Unlock screen set to $isChecked")
206-
}
207-
208201
// Setup launch app spinner
209202
val installedApps = getLaunchableApps()
210203
val appNames = mutableListOf(getString(R.string.launch_app_none))
@@ -235,6 +228,39 @@ class MainActivity : AppCompatActivity() {
235228
Log.i(TAG, "Launch app set to $selectedPackage")
236229
}
237230

231+
override fun onNothingSelected(parent: AdapterView<*>?) {}
232+
}
233+
234+
// Setup notification sound spinner
235+
val notificationSounds = getNotificationSounds()
236+
val soundNames = mutableListOf(getString(R.string.notification_sound_none))
237+
val soundUris = mutableListOf("")
238+
notificationSounds.forEach { (name, uri) ->
239+
soundNames.add(name)
240+
soundUris.add(uri)
241+
}
242+
243+
val soundAdapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, soundNames)
244+
soundAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
245+
notificationSoundSpinner?.adapter = soundAdapter
246+
247+
// Set current selection
248+
val currentSoundIndex = soundUris.indexOf(notificationSound).coerceAtLeast(0)
249+
notificationSoundSpinner?.setSelection(currentSoundIndex)
250+
251+
notificationSoundSpinner?.onItemSelectedListener =
252+
object : AdapterView.OnItemSelectedListener {
253+
override fun onItemSelected(
254+
parent: AdapterView<*>?,
255+
view: View?,
256+
position: Int,
257+
id: Long,
258+
) {
259+
val selectedUri = soundUris[position]
260+
prefs.edit().putString(KEY_NOTIFICATION_SOUND, selectedUri).apply()
261+
Log.i(TAG, "Notification sound set to $selectedUri")
262+
}
263+
238264
override fun onNothingSelected(parent: AdapterView<*>?) {}
239265
}
240266
}
@@ -450,15 +476,35 @@ class MainActivity : AppCompatActivity() {
450476
.sortedBy { it.loadLabel(pm).toString().lowercase() }
451477
}
452478

479+
private fun getNotificationSounds(): List<Pair<String, String>> {
480+
val sounds = mutableListOf<Pair<String, String>>()
481+
val ringtoneManager = android.media.RingtoneManager(this)
482+
ringtoneManager.setType(android.media.RingtoneManager.TYPE_NOTIFICATION)
483+
val cursor = ringtoneManager.cursor
484+
485+
try {
486+
while (cursor.moveToNext()) {
487+
val title = cursor.getString(android.media.RingtoneManager.TITLE_COLUMN_INDEX)
488+
val uri = ringtoneManager.getRingtoneUri(cursor.position).toString()
489+
sounds.add(Pair(title, uri))
490+
}
491+
} catch (e: Exception) {
492+
Log.e(TAG, "Error loading notification sounds", e)
493+
}
494+
495+
return sounds.sortedBy { it.first.lowercase() }
496+
}
497+
453498
companion object {
454499
private const val TAG = "MainActivity"
455-
const val PREFS_NAME = "HeisenbergLuxPrefs"
500+
const val PREFS_NAME = "ObserverEffectPrefs"
456501
const val KEY_CAMERA_SELECTION = "camera_selection"
457502
const val KEY_CAMERA_SENSITIVITY = "camera_sensitivity"
458503
const val KEY_LIGHT_SENSITIVITY = "light_sensitivity"
459504
const val KEY_START_AT_BOOT = "start_at_boot"
460505
const val KEY_BYPASS_LOCK_SCREEN = "bypass_lock_screen"
461506
const val KEY_LAUNCH_APP = "launch_app"
507+
const val KEY_NOTIFICATION_SOUND = "notification_sound"
462508

463509
const val CAMERA_NONE = 0
464510
const val CAMERA_REAR = 1

app/src/main/res/layout-sw600dp/activity_main.xml

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
android:id="@+id/appTitle"
4141
android:layout_width="0dp"
4242
android:layout_height="wrap_content"
43-
android:text="@string/app_name"
43+
android:text="@string/app_title"
4444
android:textSize="48sp"
4545
android:textStyle="bold"
4646
android:textColor="?attr/colorPrimary"
@@ -287,16 +287,6 @@
287287
android:layout_height="wrap_content"
288288
android:orientation="vertical">
289289

290-
<CheckBox
291-
android:id="@+id/unlockScreenCheckbox"
292-
android:layout_width="wrap_content"
293-
android:layout_height="wrap_content"
294-
android:minHeight="48dp"
295-
android:text="@string/unlock_screen"
296-
android:textSize="16sp"
297-
android:textColor="?android:attr/textColorPrimary"
298-
android:contentDescription="@string/unlock_screen" />
299-
300290
<CheckBox
301291
android:id="@+id/startAtBootCheckbox"
302292
android:layout_width="wrap_content"
@@ -323,7 +313,25 @@
323313
android:layout_width="match_parent"
324314
android:layout_height="wrap_content"
325315
android:minHeight="48dp"
316+
android:layout_marginBottom="16dp"
326317
android:contentDescription="@string/launch_app" />
318+
319+
<TextView
320+
android:layout_width="wrap_content"
321+
android:layout_height="wrap_content"
322+
android:text="@string/notification_sound"
323+
android:textSize="15sp"
324+
android:textStyle="bold"
325+
android:textColor="?android:attr/textColorPrimary"
326+
android:layout_marginBottom="8dp"
327+
android:labelFor="@id/notificationSoundSpinner" />
328+
329+
<Spinner
330+
android:id="@+id/notificationSoundSpinner"
331+
android:layout_width="match_parent"
332+
android:layout_height="wrap_content"
333+
android:minHeight="48dp"
334+
android:contentDescription="@string/notification_sound" />
327335
</LinearLayout>
328336
</com.google.android.material.card.MaterialCardView>
329337

app/src/main/res/layout/activity_main.xml

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
android:id="@+id/appTitle"
2424
android:layout_width="0dp"
2525
android:layout_height="wrap_content"
26-
android:text="@string/app_name"
27-
android:textSize="38sp"
26+
android:text="@string/app_title"
27+
android:textSize="28sp"
2828
android:textStyle="bold"
2929
android:textColor="?attr/colorPrimary"
3030
android:layout_marginStart="24dp"
@@ -277,16 +277,6 @@
277277
android:layout_height="wrap_content"
278278
android:orientation="vertical">
279279

280-
<CheckBox
281-
android:id="@+id/unlockScreenCheckbox"
282-
android:layout_width="wrap_content"
283-
android:layout_height="wrap_content"
284-
android:minHeight="48dp"
285-
android:text="@string/unlock_screen"
286-
android:textSize="16sp"
287-
android:textColor="?android:attr/textColorPrimary"
288-
android:contentDescription="@string/unlock_screen" />
289-
290280
<CheckBox
291281
android:id="@+id/startAtBootCheckbox"
292282
android:layout_width="wrap_content"
@@ -313,7 +303,25 @@
313303
android:layout_width="match_parent"
314304
android:layout_height="wrap_content"
315305
android:minHeight="48dp"
306+
android:layout_marginBottom="16dp"
316307
android:contentDescription="@string/launch_app" />
308+
309+
<TextView
310+
android:layout_width="wrap_content"
311+
android:layout_height="wrap_content"
312+
android:text="@string/notification_sound"
313+
android:textSize="14sp"
314+
android:textStyle="bold"
315+
android:textColor="?android:attr/textColorPrimary"
316+
android:layout_marginBottom="8dp"
317+
android:labelFor="@id/notificationSoundSpinner" />
318+
319+
<Spinner
320+
android:id="@+id/notificationSoundSpinner"
321+
android:layout_width="match_parent"
322+
android:layout_height="wrap_content"
323+
android:minHeight="48dp"
324+
android:contentDescription="@string/notification_sound" />
317325
</LinearLayout>
318326
</com.google.android.material.card.MaterialCardView>
319327

app/src/main/res/values-night/themes.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33
<!-- Dark Theme -->
4-
<style name="Theme.HeisenbergLux" parent="Theme.Material3.Dark.NoActionBar">
4+
<style name="Theme.ObserverEffect" parent="Theme.Material3.Dark.NoActionBar">
55
<item name="colorPrimary">@color/primary</item>
66
<item name="colorPrimaryVariant">@color/primary_dark</item>
77
<item name="colorSecondary">@color/accent</item>

app/src/main/res/values/strings.xml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
3-
<string name="app_name">Heisenberg\'s Lux💡</string>
4-
<string name="service_notification_title">Heisenberg\'s Lux</string>
3+
<string name="app_name">Observer Effect</string>
4+
<string name="app_title">The Observer Effect</string>
5+
<string name="service_notification_title">The Observer Effect</string>
56
<string name="service_notification_text">Monitoring for motion</string>
6-
<string name="app_subtitle">Enables the display when motion is detected</string>
7+
<string name="app_subtitle">Observing the system changes it</string>
78
<string name="motion_detection">Camera Sensor</string>
89
<string name="light_detection">Ambient Light Sensor</string>
910
<string name="sensitivity">Minimum activity level</string>
@@ -28,9 +29,11 @@
2829
<string name="camera_rear_option">Rear Camera</string>
2930
<string name="camera_front_option">Front Camera</string>
3031
<string name="start_at_boot">Start at boot</string>
31-
<string name="unlock_screen">Unlock screen</string>
32+
<string name="unlock_screen">Dismiss lock screen (swipe only)</string>
3233
<string name="launch_app">Launch app</string>
33-
<string name="launch_app_none">None (just unlock)</string>
34+
<string name="launch_app_none">None</string>
35+
<string name="notification_sound">Notification sound</string>
36+
<string name="notification_sound_none">None</string>
3437
<string name="sensor_not_available">N/A</string>
3538
<string name="sensor_not_available_message">This device does not have an ambient light sensor</string>
3639
</resources>

app/src/main/res/values/themes.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33
<!-- Light Theme -->
4-
<style name="Theme.HeisenbergLux" parent="Theme.Material3.Light.NoActionBar">
4+
<style name="Theme.ObserverEffect" parent="Theme.Material3.Light.NoActionBar">
55
<item name="colorPrimary">@color/primary</item>
66
<item name="colorPrimaryVariant">@color/primary_dark</item>
77
<item name="colorSecondary">@color/accent</item>

0 commit comments

Comments
 (0)