Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fcbc53c
DelayPrefs object with getter/setter methods for screen-on delay conf…
Dan8Oren Oct 8, 2025
2a86b9a
DelayedActivationManager class with coroutine-based delay handling
Dan8Oren Oct 8, 2025
5789863
Enhance MicLockService with delay integration
Dan8Oren Oct 8, 2025
aeec535
modified ScreenStateReceiver for delay support and added unit tests
Dan8Oren Oct 8, 2025
3250553
updated UI to set delay
Dan8Oren Oct 8, 2025
b665fb1
Fix delay logic to distinguish between service running and mic active…
Dan8Oren Oct 8, 2025
bc6b943
Fix state management to distinguish screen-off pause from silence pause
Dan8Oren Oct 8, 2025
b615153
Update Quick Settings tile for delay states
Dan8Oren Oct 8, 2025
6f04258
fixed a bug when the tile wasn't updating back to active after delay
Dan8Oren Oct 8, 2025
5223c65
delay slider share the same constant as the code
Dan8Oren Oct 9, 2025
e9fadd7
change the logic into screen behavior with option to never and always on
Dan8Oren Oct 9, 2025
8e8a6d7
working on slider UI
Dan8Oren Oct 9, 2025
b15180f
some UI tweaks and fixed MicLockService.kt not taking into account Ne…
Dan8Oren Oct 9, 2025
cc15052
fixed tile functionality and app status on paused by screen off.
Dan8Oren Oct 9, 2025
ac28bfc
fixed tile functionality and app status on paused by screen off.
Dan8Oren Oct 9, 2025
a188fdc
added some more unit tests
Dan8Oren Oct 9, 2025
c15c37a
refined feature labels and extracted to strings.xml
Dan8Oren Oct 9, 2025
5222d32
MainActivity start button to handle screen-off pause state
Dan8Oren Oct 9, 2025
c26f232
updated docs and version
Dan8Oren Oct 9, 2025
08ce967
gitignore update
Dan8Oren Oct 9, 2025
2d9184b
updated Changelog
Dan8Oren Oct 9, 2025
ae07137
minor changes
Dan8Oren Oct 9, 2025
87dfe6f
updated versionCode to count past releases
Dan8Oren Oct 9, 2025
d4eb983
Update CHANGELOG.md
Dan8Oren Oct 10, 2025
3c1fa4b
Apply suggestion from @Copilot
Dan8Oren Oct 10, 2025
76cf9f0
gitignore update
Dan8Oren Oct 10, 2025
043db5d
stuck in isPausedBySilence state bug fix (#10)
Dan8Oren Oct 12, 2025
62a4daa
Revert "stuck in isPausedBySilence state bug fix (#10)" (#11)
Dan8Oren Oct 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*.aab
*.aar
*.apk
/app/release/
/app/debug/

# Files for the Dalvik VM
*.dex
Expand Down Expand Up @@ -54,4 +56,7 @@ captures/
*.keystore

# External libraries
libs/ # If you are not managing libraries through Gradle dependencies
libs/ # If you are not managing libraries through Gradle dependencies

.kiro
.vscode
37 changes: 31 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2025-01-24
## [1.1.1] - 2025-10-09

### Added
- **Configurable Screen-Off Behavior**:
Default configuration is set to 1.3 seconds of Delayed Reactivation. Configuration options are:
a. **Always-On**: Keeps the microphone usage on even when the screen is off. Might be a good option for those who have quick microphone usage even when the screen is off (more battery usage, less recommended).
b. **Delayed Reactivation**: (Recommended Approach) Microphone turns off when the screen is off with a configurable delay for reactivating the microphone when the screen turns back on, preventing unnecessary battery drain during brief screen interactions like checking notifications or battery level.
c. **Stays-Off**: Turns the microphone usage off once the screen turns off and does not reactivate it. (Recommended for those who want minimum battery usage, but requires manual reactivation before usage.)

- **DelayedActivationManager**: New component for robust delay handling with proper race condition management and coroutine-based implementation.


### Enhanced
- **Battery Optimization**: Significantly reduced power consumption by avoiding microphone operations during brief screen interactions while maintaining responsive behavior for legitimate usage.
- **Quick Settings Tile**: Enhanced tile to display "Activating..." state during delay periods with manual override capability.


### Technical Improvements
- Coroutine-based delay implementation with proper job cancellation and cleanup
- Atomic state updates and synchronized operations for thread safety
- Timestamp-based race condition detection and latest-event-wins strategy
- Enhanced service lifecycle integration with delay state persistence

## [1.1.0] - 2025-10-01

### Added
- **Intelligent Quick Settings Tile**: A new state-aware Quick Settings tile that provides at-a-glance status (On, Off, Paused) and one-tap control of the MicLock service.
Expand All @@ -15,7 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The tile now displays a "Paused" state when another app is using the microphone, providing clearer feedback to the user.
- The tile shows an unavailable state with a "No Permission" label if required permissions have not been granted.

## [1.0.1] - 2025-01-23
## [1.0.1] - 2025-09-23

### Enhanced
- **Improved Service Reliability (Always-On Foreground Service):** The MicLockService now remains in the foreground at all times when active, even when the screen is off. This significantly improves service reliability by preventing the Android system from terminating the background process.
Expand All @@ -26,10 +49,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Addressed issues where the service could be terminated by aggressive OEM power management when the screen was off

## [1.0.0] - 2024-01-20
## [1.0.0] - 2025-09-21

### Added
- Initial public release of Mic-Lock
- Initial public release of MicLock
- Core functionality to reroute audio from faulty bottom microphone to earpiece microphone on Google Pixel devices
- Battery-efficient background service with dual recording strategy (MediaRecorder/AudioRecord modes)
- Polite background holding mechanism that yields to foreground applications
Expand Down Expand Up @@ -59,5 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Contributing guidelines for open source development
- Issue templates for bug reports and feature requests

[1.1.0]: https://github.com/yourusername/mic-lock/releases/tag/v1.1.0
[1.0.0]: https://github.com/yourusername/mic-lock/releases/tag/v1.0.0
[1.1.1]: https://github.com/Dan8Oren/MicLock/releases/tag/v1.1.1
[1.1.0]: https://github.com/Dan8Oren/MicLock/releases/tag/v1.1.0
[1.0.1]: https://github.com/Dan8Oren/MicLock/releases/tag/v1.0.1
[1.0.0]: https://github.com/Dan8Oren/MicLock/releases/tag/v1.0.0
25 changes: 21 additions & 4 deletions DEV_SPECS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Mic-Lock should avoid requesting audio modes or flags that might inadvertently b

### 2.6 Proper Foreground Service (FGS) Lifecycle and Android 14+ Compatibility

Mic-Lock must integrate correctly with Android's Foreground Service lifecycle to ensure stable policy classification, while gracefully handling Android 14+ background service restrictions:
Mic-Lock must integrate correctly with Android's Foreground Service lifecycle to ensure stable policy classification, while gracefully handling Android 14+ background service restrictions and implementing intelligent delayed activation:

* **Open Input After FGS Start:** The microphone input should only be opened *after* the Foreground Service is fully running and its notification is visible.
* **Persistent Notification:** Maintain a clear, ongoing notification that indicates the service status and allows user control.
Expand All @@ -74,9 +74,26 @@ Mic-Lock must integrate correctly with Android's Foreground Service lifecycle to
- Using regular `startService()` for already-running services to avoid background restrictions
* **Screen State Integration:** To prevent termination by the OS, the service remains in the foreground at all times when active.
- When the screen turns **off**, the service pauses microphone usage to save battery but **does not exit the foreground state**. The notification is updated to show a "Paused (Screen off)" status.
- When the screen turns **on**, the service resumes active microphone holding.
- When the screen turns **on**, the service implements intelligent delayed activation with configurable delays (default 1.3 seconds) to prevent unnecessary battery drain during brief screen interactions.
* **Delayed Activation Management:** The service must properly handle delayed microphone re-activation:
- Start foreground service immediately when delay is scheduled to comply with Android 14+ restrictions
- Cancel pending delays if screen turns off during delay period
- Restart delay from beginning if screen turns on again during existing delay
- Respect existing service states (manual stops, active sessions, paused by other apps) when applying delays

### 2.7 User Interface and Preferences
### 2.7 Intelligent Screen State Management

Mic-Lock must implement configurable delayed activation to optimize battery usage while maintaining responsive behavior:

* **Configurable Delay Period:** Provide user-configurable delay (0-5000ms) before re-activating microphone when screen turns on
* **Smart Cancellation Logic:** Cancel pending activation if screen turns off during delay period, preventing unnecessary operations
* **Race Condition Handling:** Handle rapid screen state changes with latest-event-wins strategy and proper coroutine job management
* **State Validation:** Ensure delays only apply when appropriate (service paused by screen-off, not manually stopped or already active)
* **Foreground Service Coordination:** Start foreground service immediately when delay is scheduled to comply with Android 14+ background restrictions
* **Notification Updates:** Update service notification to reflect delay status with countdown display during delay periods
* **Manual Override Support:** Allow immediate activation through Quick Settings tile or manual service start, cancelling any pending delays

### 2.8 User Interface and Preferences

* **Quick Settings Tile**: A state-aware tile provides at-a-glance status and one-tap control. It must reflect the service's state (On, Off, Paused) and become unavailable if permissions are missing.

Expand All @@ -90,7 +107,7 @@ Mic-Lock must integrate correctly with Android's Foreground Service lifecycle to
* **Battery Usage Awareness:** Clearly communicate to users that MediaRecorder mode uses more battery but provides better compatibility.
* **Battery Optimization Exemption:** Upon first launch, the app prompts the user to grant an exemption from battery optimizations. This is critical to prevent the Android system from terminating the service during long periods of device inactivity, ensuring continuous background operation.

### 2.8 Service Resilience and User Experience
### 2.9 Service Resilience and User Experience

To ensure the service remains active and is easy to manage, Mic-Lock implements several resilience features:

Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ cd MicLock
Mic-Lock acts as a "polite background holder" that:
1. **Detects Faulty Microphone**: Identifies when the default microphone path is compromised (typically the bottom mic on Pixel devices).
2. **Secures Working Mic**: Establishes and holds a connection to your device's *working* earpiece microphone array in a battery-efficient manner.
3. **Graceful Handover**: When other apps start recording, Mic-Lock gracefully releases its hold.
4. **Correct Path Inheritance**: The other app then inherits the correctly routed audio path to the functional microphone instead of defaulting to the broken one.
5. **Seamless Experience**: Your recordings and calls work perfectly without manual intervention!
3. **Intelligent Screen Management**: Uses configurable delays (default 1.3 seconds) before re-activating when screen turns on, preventing unnecessary battery drain during brief interactions like checking notifications.
4. **Graceful Handover**: When other apps start recording, Mic-Lock gracefully releases its hold.
5. **Correct Path Inheritance**: The other app then inherits the correctly routed audio path to the functional microphone instead of defaulting to the broken one.
6. **Seamless Experience**: Your recordings and calls work perfectly without manual intervention!

## 🔒 Security & Privacy

Expand Down Expand Up @@ -79,6 +80,7 @@ For even easier access, Mic-Lock includes a Quick Settings tile.

- **AudioRecord Mode**: (Default) More battery-efficient, optimized for most modern devices. If you experience high battery usage, switch to this mode.
- **MediaRecorder Mode**: Offers wider compatibility, especially on older or more problematic devices, but might use slightly more battery.
- **Screen-On Delay**: Configurable delay (0-5000ms, default 1.3 seconds) before re-activating microphone when screen turns on. This prevents unnecessary battery drain during brief screen interactions like checking notifications or battery level. Set to 0 for immediate activation.

## 🛠️ Troubleshooting

Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
applicationId = "io.github.miclock"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.1.0"
versionCode = 4
versionName = "1.1.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
Binary file removed app/release/baselineProfiles/0/app-release.dm
Binary file not shown.
Binary file removed app/release/baselineProfiles/1/app-release.dm
Binary file not shown.
37 changes: 0 additions & 37 deletions app/release/output-metadata.json

This file was deleted.

138 changes: 134 additions & 4 deletions app/src/main/java/io/github/miclock/data/Prefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,29 @@ package io.github.miclock.data

import android.content.Context
import androidx.core.content.edit
import kotlin.math.round

object Prefs {
private const val FILE = "miclock_prefs"

private const val KEY_USE_MEDIA_RECORDER = "use_media_recorder"
private const val KEY_LAST_RECORDING_METHOD = "last_recording_method"
private const val KEY_SCREEN_ON_DELAY = "screen_on_delay_ms"

const val VALUE_AUTO = "auto"

// Screen-on delay constants
const val DEFAULT_SCREEN_ON_DELAY_MS = 1300L
const val MIN_SCREEN_ON_DELAY_MS = 0L
const val MAX_SCREEN_ON_DELAY_MS = 5000L

// Special behavior values
const val NEVER_REACTIVATE_VALUE = -1L // Never re-enable after screen-off
const val ALWAYS_KEEP_ON_VALUE = -2L // Always keep mic on (ignore screen state)

fun getUseMediaRecorder(ctx: Context): Boolean =
ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.getBoolean(KEY_USE_MEDIA_RECORDER, false)
ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.getBoolean(KEY_USE_MEDIA_RECORDER, false)

fun setUseMediaRecorder(ctx: Context, value: Boolean) {
ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE).edit {
Expand All @@ -22,12 +33,131 @@ object Prefs {
}

fun getLastRecordingMethod(ctx: Context): String? =
ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.getString(KEY_LAST_RECORDING_METHOD, null)
ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.getString(KEY_LAST_RECORDING_METHOD, null)

fun setLastRecordingMethod(ctx: Context, method: String) {
ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE).edit {
putString(KEY_LAST_RECORDING_METHOD, method)
}
}

/**
* Gets the screen-on delay in milliseconds.
* @return delay in milliseconds, defaults to 1300ms
*/
fun getScreenOnDelayMs(ctx: Context): Long =
ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.getLong(KEY_SCREEN_ON_DELAY, DEFAULT_SCREEN_ON_DELAY_MS)

/**
* Sets the screen-on delay in milliseconds with validation.
* @param delayMs delay in milliseconds, must be between 0-5000ms, -1 for never re-enable, or -2
* for always on
* @throws IllegalArgumentException if delay is outside valid range
*/
fun setScreenOnDelayMs(ctx: Context, delayMs: Long) {
require(isValidScreenOnDelay(delayMs)) {
"Screen-on delay must be between ${MIN_SCREEN_ON_DELAY_MS}ms and ${MAX_SCREEN_ON_DELAY_MS}ms, ${NEVER_REACTIVATE_VALUE} for never re-enable, or ${ALWAYS_KEEP_ON_VALUE} for always on, got ${delayMs}ms"
}

ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE).edit {
putLong(KEY_SCREEN_ON_DELAY, delayMs)
}
}

/**
* Validates if the given delay value is within acceptable range.
* @param delayMs delay in milliseconds to validate
* @return true if valid, false otherwise
*/
fun isValidScreenOnDelay(delayMs: Long): Boolean =
delayMs == NEVER_REACTIVATE_VALUE ||
delayMs == ALWAYS_KEEP_ON_VALUE ||
delayMs in MIN_SCREEN_ON_DELAY_MS..MAX_SCREEN_ON_DELAY_MS

// Slider mapping constants
const val SLIDER_MIN = 0f
const val SLIDER_MAX = 100f
const val SLIDER_ALWAYS_ON = 0f // Far left (immediate/no delay)
const val SLIDER_NEVER_REACTIVATE = 100f // Far right (maximum restriction)
const val SLIDER_DELAY_START = 10f // Start of delay range
const val SLIDER_DELAY_END = 90f // End of delay range

/**
* Converts slider position (0-100) to delay value in milliseconds with snappy transitions.
* @param sliderValue slider position (0-100)
* @return delay in milliseconds or special behavior value
*/
fun sliderToDelayMs(sliderValue: Float): Long {
return when {
// Snap zone for "Always on" (0-5) - far left
sliderValue <= 9f -> ALWAYS_KEEP_ON_VALUE

// Snap zone for "Never re-enable" (95-100) - far right
sliderValue >= 91f -> NEVER_REACTIVATE_VALUE

// Delay range (10-90) with snapping to nearest valid position
else -> {
// Snap to the delay range boundaries if close
val snappedValue =
when {
sliderValue < SLIDER_DELAY_START -> SLIDER_DELAY_START
sliderValue > SLIDER_DELAY_END -> SLIDER_DELAY_END
else -> sliderValue
}

// Map to delay range (0-5000ms)
val normalizedValue =
(snappedValue - SLIDER_DELAY_START) /
(SLIDER_DELAY_END - SLIDER_DELAY_START)
val delayMs = (normalizedValue * MAX_SCREEN_ON_DELAY_MS).toLong()
// Round to nearest 100ms
(delayMs / 100L) * 100L
}
}
}

/**
* Snaps slider value to the nearest valid position with clear phase boundaries.
* @param sliderValue current slider position
* @return snapped slider position
*/
fun snapSliderValue(sliderValue: Float): Float {
return when {
// Snap to "Always on" zone (far left)
sliderValue <= 5f -> SLIDER_ALWAYS_ON

// Snap to "Never re-enable" zone (far right)
sliderValue >= 95f -> SLIDER_NEVER_REACTIVATE

// Snap to delay range boundaries if in transition zones
sliderValue < SLIDER_DELAY_START -> SLIDER_DELAY_START
sliderValue > SLIDER_DELAY_END -> SLIDER_DELAY_END

// Within delay range - round to nearest integer
else -> round(sliderValue)
}
}

/**
* Converts delay value to slider position (0-100).
* @param delayMs delay in milliseconds or special behavior value
* @return slider position (0-100), rounded to integer
*/
fun delayMsToSlider(delayMs: Long): Float {
return when (delayMs) {
ALWAYS_KEEP_ON_VALUE -> SLIDER_ALWAYS_ON // Position 0 (far left)
NEVER_REACTIVATE_VALUE -> SLIDER_NEVER_REACTIVATE // Position 100 (far right)
else -> {
// Map delay range (0-5000ms) to slider range (10-90)
val normalizedDelay = delayMs.toFloat() / MAX_SCREEN_ON_DELAY_MS.toFloat()
val sliderValue =
SLIDER_DELAY_START +
(normalizedDelay * (SLIDER_DELAY_END - SLIDER_DELAY_START))
// Round to nearest integer to align with stepSize
round(sliderValue)
}
}
}
}
Loading