Skip to content

Conversation

owenfromcanada
Copy link
Contributor

@owenfromcanada owenfromcanada commented Apr 22, 2024

Per discussion: #3 (comment)

Summary by CodeRabbit

  • New Features

    • App updated to Android SDK 34 and released as version 1.6.1.
    • Requests runtime notification permission and adds foreground media-playback capability.
    • Dynamic Material theming applied for improved visuals.
  • Improvements

    • Replaced legacy seek bars with Material sliders for a modern control experience.
    • Theme refreshed to VolumeLockr and preference UI updated.
  • Bug Fixes

    • More reliable volume and preference handling.
  • Chores

    • Updated build tooling and Material library.

@BurnyBoi
Copy link

BurnyBoi commented Apr 24, 2024

Tested this PR: Stopping the service and starting the app again with "Allow lower than limit" enabled now stays functional as expected.

I did happen to notice if you set the volume lower than the limit while stopping the service and restarting the app, the upper limit gets changed to that lower volume, meaning you'd have to go into the app and turn it back up if wanted. This could be considered a feature rather than a bug though depending who you ask since it could help prevent getting hit with overly-loud volume in some cases.

Copy link

coderabbitai bot commented Nov 10, 2024

Walkthrough

Multiple build, manifest, resource, and Kotlin source updates: build tooling and SDK versions bumped; Material components and theming applied; notification permission and foreground media playback permission added; Slider/Material components replace legacy widgets; VolumeService preference loading refactored and permission/request flow added in MainActivity.

Changes

Cohort / File(s) Change Summary
Build system & Gradle files
build.gradle, gradle.properties, gradle/wrapper/gradle-wrapper.properties, app/build.gradle
- Kotlin updated to 1.8.0 and AGP to 8.5.0
- Gradle wrapper bumped to 8.7 (distributionUrl + sha)
- Added Gradle properties: android.defaults.buildfeatures.buildconfig, android.nonTransitiveRClass, android.nonFinalResIds
- app/build.gradle: compileSdk→34, minSdk→19, targetSdk→34, versionCode/name bumped, namespace added, kapt set apply false, multiDexEnabled = true, Material lib → 1.12.0
Android manifest
app/src/main/AndroidManifest.xml
- Removed root package attribute
- Added permissions: FOREGROUND_SERVICE_MEDIA_PLAYBACK, POST_NOTIFICATIONS
- Application theme changed to @style/Theme.VolumeLockr
- VolumeService updated with android:foregroundServiceType="mediaPlayback" and android:exported="false"
Service & runtime prefs
app/src/main/java/.../service/VolumeService.kt
- Added private fun loadGeneralPreferences() and refactored preference loading to call it from onStartCommand() and loadPreferences()
- Always register MODE_RINGER_SETTING observer and always read MODE_RINGER_SETTING (removed VERSION guarded branches)
- Use edit {} extension for SharedPreferences writes; minor idiomatic swaps (isEmpty() / isNotEmpty())
Activity: notification permission flow
app/src/main/java/.../ui/MainActivity.kt
- Added runtime POST_NOTIFICATIONS permission check/request (companion constants and request flow) moved into onCreate
- Adjusted NotificationManager retrieval usage
UI components & adapter logic
app/src/main/java/.../ui/VolumeAdapter.kt, app/src/main/res/layout/volume_card.xml, app/src/main/res/layout/preference_switch.xml
- Replaced SeekBar → Material Slider, SwitchCompat → MaterialSwitch, CardView/TextView → MaterialCardView/MaterialTextView in layouts
- Adapter updated to use Slider APIs and Slider listeners; bounds mapping and enable/disable logic preserved
- Added preference_switch.xml layout for MaterialSwitch usage in preferences
Application class & Dynamic Colors
app/src/main/java/.../ui/VolumeLockrApplication.kt
- New VolumeLockrApplication (subclass of Application) calling DynamicColors.applyToActivitiesIfAvailable(this) in onCreate()
Themes & preferences styling
app/src/main/res/values/themes.xml, app/src/main/res/values-night/themes.xml
- Renamed/introduced Theme.VolumeLockr, removed old Theme.Volume from night values
- Added ThemeOverlay.App.Preference and PreferenceMaterialSwitch styles; mapped preference switch widget layout to the new preference_switch.xml
Documentation
README.md
- Updated image paths for screenshots to fastlane/metadata/.../phoneScreenshots

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MainActivity
    participant System as AndroidSystem
    participant VolumeService
    Note over MainActivity: App start / UI init
    User->>MainActivity: Launch app
    MainActivity->>AndroidSystem: check POST_NOTIFICATIONS permission
    alt permission denied
        MainActivity->>User: request permission (runtime dialog)
        User-->>MainActivity: grant/deny
    end
    MainActivity->>AndroidSystem: check DND access (existing flow)
    Note over VolumeService: Service start path
    AndroidSystem->>VolumeService: startService / onStartCommand
    VolumeService->>VolumeService: loadGeneralPreferences()
    VolumeService->>AndroidSystem: register content observers (ringer/volume)
    AndroidSystem-->>VolumeService: content changes (async)
    VolumeService->>AndroidSystem: show/hide notification based on mVolumeLock (tryShowNotification/tryHideNotification)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

"I hop through the code with a carrot and grin,
Sliders and themes make the UI sing.
Permissions requested, services set right,
Dynamic colors dancing into the night.
Hooray for small hops that make big delight!" 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title “Corrected volume max setting not loading properly” clearly summarizes the primary fix implemented by this PR, namely ensuring the volume maximum setting loads correctly on restart, and it aligns directly with the stated objectives without unnecessary detail.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (9)
app/src/main/java/com/klee/volumelockr/ui/VolumeLockrApplication.kt (1)

7-10: Consider adding version check and error handling.

While the implementation is functional, it could be more robust:

  1. DynamicColors might not be available on all Android versions
  2. The application should gracefully handle cases where dynamic colors aren't supported

Consider this more defensive implementation:

 override fun onCreate() {
     super.onCreate()
-    DynamicColors.applyToActivitiesIfAvailable(this)
+    try {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+            DynamicColors.applyToActivitiesIfAvailable(this)
+        }
+    } catch (e: Exception) {
+        android.util.Log.w("VolumeLockr", "Failed to apply dynamic colors", e)
+    }
 }
app/src/main/res/values/themes.xml (1)

3-4: LGTM! Good choice using Material3 with dynamic colors.

The migration to Material3 with dynamic colors support is a good modernization step. This will provide better theming capabilities and ensure proper dark mode support.

Consider documenting the theme capabilities in the README.md to help future contributors understand the theming system, especially regarding dynamic colors support.

build.gradle (2)

Line range hint 23-23: Remove deprecated jcenter() repository.

The jcenter() repository is deprecated and will be shut down. Consider removing it and ensuring all dependencies are available through mavenCentral() or google().

Apply this diff:

allprojects {
    repositories {
        google()
        mavenCentral()
-       jcenter() // Warning: this repository is going to shut down soon
    }
}

Line range hint 1-31: Consider implementing version catalogs for dependency management.

The current build file uses hardcoded versions. Consider implementing Gradle version catalogs to centralize dependency management and make version updates more maintainable.

Create a new file gradle/libs.versions.toml:

[versions]
kotlin = "1.8.0"
agp = "8.5.0"
aboutlibraries = "8.9.0"

[libraries]
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
android-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }
aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlibraries" }

Then update the build.gradle:

buildscript {
-   ext.kotlin_version = '1.8.0'
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://plugins.gradle.org/m2/")
        }
    }
    dependencies {
-       classpath 'com.android.tools.build:gradle:8.5.0'
-       classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
-       classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:8.9.0"
+       classpath(libs.android.gradle)
+       classpath(libs.kotlin.gradle)
+       classpath(libs.aboutlibraries.gradle)
    }
}
app/src/main/res/layout/volume_card.xml (1)

39-44: Add contentDescription for accessibility support.

The MaterialSwitch should have a content description to improve accessibility for users with screen readers.

Apply this diff:

 <com.google.android.material.materialswitch.MaterialSwitch
     android:id="@+id/switchButton"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="center"
-    android:text="@string/lock" />
+    android:text="@string/lock"
+    android:contentDescription="@string/lock" />
app/src/main/java/com/klee/volumelockr/ui/VolumeAdapter.kt (1)

122-124: Fix indentation for better readability.

The conditional expression could be formatted more clearly.

-            holder.binding.slider.isEnabled =
-                mService?.getMode() == 2 &&
-                mService?.getLocks()?.containsKey(AudioManager.STREAM_NOTIFICATION) == false
+            holder.binding.slider.isEnabled = mService?.getMode() == 2 &&
+                !mService?.getLocks()?.containsKey(AudioManager.STREAM_NOTIFICATION)!!
app/src/main/java/com/klee/volumelockr/service/VolumeService.kt (3)

179-182: LGTM! Consider adding documentation.

The new method effectively centralizes the loading of general preferences. Consider adding KDoc documentation to explain its purpose and the impact of the ALLOW_LOWER setting.

+    /**
+     * Loads general preferences including the "Allow lower than limit" setting
+     * which determines if volumes can be set below their locked values.
+     */
     private fun loadGeneralPreferences() {

Line range hint 186-195: Consider additional synchronization for preference access.

While checkVolumes() is synchronized, it reads mAllowLower which can be modified by loadGeneralPreferences() without synchronization. This could potentially lead to a race condition.

Consider these options:

  1. Add synchronization to preference access:
     @WorkerThread
     @Synchronized
     private fun checkVolumes() {
+        val allowLower = synchronized(this) { mAllowLower }
         for ((stream, volume) in mVolumeLock) {
             val current = mAudioManager.getStreamVolume(stream)
-            if ((current > volume) || (!mAllowLower && current != volume)) {
+            if ((current > volume) || (!allowLower && current != volume)) {
                 mAudioManager.setStreamVolume(stream, volume, 0)
                 invokeVolumeListenerCallback()
             }
         }
     }
  1. Or make mAllowLower volatile:
-    private var mAllowLower = false
+    @Volatile
+    private var mAllowLower = false

Line range hint 186-195: Document the volume adjustment behavior.

The current implementation matches BurnyBoi's observation: when the service restarts with a volume lower than the limit, it maintains that lower volume. While this behavior is reasonable from a user experience perspective (preventing unexpected volume increases), it should be documented.

Consider adding a comment explaining this behavior:

     @WorkerThread
     @Synchronized
     private fun checkVolumes() {
+        // Note: When "Allow lower than limit" is enabled and the service restarts,
+        // if the current volume is below the limit, it will be maintained at that level.
+        // This prevents unexpected volume increases on service restart.
         for ((stream, volume) in mVolumeLock) {
🛑 Comments failed to post (2)
app/src/main/res/layout/volume_card.xml (1)

33-37: ⚠️ Potential issue

Add essential Slider attributes for volume control.

The Slider component is missing crucial attributes needed for proper volume control functionality:

Apply this diff to add the necessary attributes:

 <com.google.android.material.slider.Slider
     android:id="@+id/slider"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_gravity="center"/>
+    android:layout_gravity="center"
+    android:valueFrom="0"
+    android:valueTo="100"
+    android:stepSize="1"
+    android:value="0"/>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            <com.google.android.material.slider.Slider
                android:id="@+id/slider"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:valueFrom="0"
                android:valueTo="100"
                android:stepSize="1"
                android:value="0"/>
app/src/main/java/com/klee/volumelockr/ui/VolumeAdapter.kt (1)

65-73: ⚠️ Potential issue

Add validation for volume limit changes.

The current implementation allows immediate value changes without checking against previous maximum limits. This explains the reported behavior where the maximum volume limit adjusts to lower settings after restart.

Consider adding validation to:

  1. Check if the new value is below the previous maximum when "Allow lower than limit" is enabled
  2. Store the original maximum value separately to allow restoration
 private fun registerSeekBarCallback(holder: ViewHolder, volume: Volume) {
     val listener =
         Slider.OnChangeListener { _, value, _ ->
+            val previousMax = mService?.getStoredMaxVolume(volume.stream)
+            val allowLower = mService?.isAllowLowerThanLimitEnabled() ?: false
+            
+            val newValue = if (!allowLower && previousMax != null) {
+                kotlin.math.min(value, previousMax.toFloat())
+            } else {
+                value
+            }
+
             if (volume.stream != AudioManager.STREAM_NOTIFICATION || mService?.getMode() == 2) {
-                mAudioManager.setStreamVolume(volume.stream, value.toInt(), 0)
+                mAudioManager.setStreamVolume(volume.stream, newValue.toInt(), 0)
             }

-            volume.value = value.toInt()
+            volume.value = newValue.toInt()
         }
     holder.binding.slider.addOnChangeListener(listener)
 }

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b32a6a and 6117d1b.

⛔ Files ignored due to path filters (4)
  • fastlane/metadata/android/en-US/images/phoneScreenshots/1.png is excluded by !**/*.png
  • fastlane/metadata/android/en-US/images/phoneScreenshots/2.png is excluded by !**/*.png
  • screenshot.png is excluded by !**/*.png
  • screenshot_night.png is excluded by !**/*.png
📒 Files selected for processing (14)
  • README.md (1 hunks)
  • app/build.gradle (2 hunks)
  • app/src/main/AndroidManifest.xml (2 hunks)
  • app/src/main/java/com/klee/volumelockr/service/VolumeService.kt (11 hunks)
  • app/src/main/java/com/klee/volumelockr/ui/MainActivity.kt (1 hunks)
  • app/src/main/java/com/klee/volumelockr/ui/VolumeAdapter.kt (7 hunks)
  • app/src/main/java/com/klee/volumelockr/ui/VolumeLockrApplication.kt (1 hunks)
  • app/src/main/res/layout/preference_switch.xml (1 hunks)
  • app/src/main/res/layout/volume_card.xml (3 hunks)
  • app/src/main/res/values-night/themes.xml (0 hunks)
  • app/src/main/res/values/themes.xml (1 hunks)
  • build.gradle (2 hunks)
  • gradle.properties (1 hunks)
  • gradle/wrapper/gradle-wrapper.properties (1 hunks)
💤 Files with no reviewable changes (1)
  • app/src/main/res/values-night/themes.xml
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/res/layout/preference_switch.xml
  • app/src/main/AndroidManifest.xml
  • build.gradle
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
README.md

4-4: Images should have alternate text (alt text)

(MD045, no-alt-text)


4-4: Images should have alternate text (alt text)

(MD045, no-alt-text)

🔇 Additional comments (14)
app/src/main/java/com/klee/volumelockr/ui/VolumeLockrApplication.kt (1)

6-11: LGTM!

The application class correctly integrates Material's dynamic color theming. The implementation follows best practices by calling super.onCreate() first and using the recommended applyToActivitiesIfAvailable method.

app/src/main/java/com/klee/volumelockr/ui/MainActivity.kt (1)

43-43: LGTM!

Good improvement using the NOTIFICATION_SERVICE constant instead of the string literal.

app/src/main/java/com/klee/volumelockr/service/VolumeService.kt (6)

182-185: LGTM! This fixes the volume max setting not loading properly.

The new loadGeneralPreferences() method correctly centralizes the loading of the ALLOW_LOWER setting from default shared preferences. This ensures the setting is reloaded when the service restarts, addressing the PR objective.


101-101: LGTM! Ensures preferences are reloaded on service restart.

Calling loadGeneralPreferences() in onStartCommand ensures that the ALLOW_LOWER preference is reloaded each time the service is started, which is essential for the fix.


166-168: LGTM! Idiomatic Kotlin preference editing.

Using the edit extension function is the recommended approach for Kotlin. This makes the code more concise and ensures apply() is called automatically.


241-243: Verify mode observer registration without version check.

The mode observer registration now occurs unconditionally, whereas it may have previously been guarded by a version check (based on the pattern in the removed code). Verify that registering a content observer for Settings.Global.getUriFor(MODE_RINGER_SETTING) is safe across all supported API levels (19+).


254-254: LGTM! Idiomatic collection checks.

Using isEmpty() and isNotEmpty() is more idiomatic Kotlin compared to size-based checks.

Also applies to: 273-273


91-91: No API level guard needed for Settings.Global.getInt – Settings.Global was introduced in API 17 and your minSdkVersion is 19 (app/build.gradle:14), so the calls on lines 91 and 217 are safe.

gradle.properties (1)

20-22: LGTM! Appropriate configuration for AGP 8.5.0.

The added build feature flags are correct for the Android Gradle Plugin 8.5.0 upgrade:

  • buildconfig=true ensures BuildConfig class generation
  • nonTransitiveRClass=false maintains R class transitivity for compatibility
  • nonFinalResIds=false keeps resource IDs non-final for reflection-based libraries

Based on learnings.

app/src/main/res/values/themes.xml (1)

3-14: LGTM: confirmed preference_switch.xml exists.

gradle/wrapper/gradle-wrapper.properties (1)

3-4: Resolve build failure with Gradle 8.7 wrapper

After updating from 7.2 to 8.7 the build (./gradlew clean build --no-daemon) fails with “Could not connect to the Gradle daemon”. Investigate and fix the daemon connectivity or wrapper configuration so that the build succeeds under Gradle 8.7, then re-verify.

app/src/main/res/layout/volume_card.xml (1)

8-47: LGTM – VolumeAdapter fully migrated to Material Slider; slider ID and API in use, no seekBar remnants.

app/src/main/java/com/klee/volumelockr/ui/VolumeAdapter.kt (2)

47-49: Slider initialization looks correct.

The migration from SeekBar to Material Slider properly initializes the slider's value from the service's locked volume (if available) or the current volume value, and sets the bounds from the volume's min/max values.


59-62: LGTM! Slider enable/disable logic is correct.

All instances of enabling/disabling the slider are properly handled for:

  • Password protection
  • Volume lock status
  • Notification stream ringer mode conditions

The migration from seekBar.isEnabled to slider.isEnabled is consistent throughout.

Also applies to: 92-96, 122-126, 133-134, 142-143

targetSdkVersion 34
versionCode 11
versionName "1.6.1"
multiDexEnabled true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add the multidex runtime dependency for pre‑21 devices.

With multiDexEnabled true and minSdkVersion 19, ART/DEX on API 19–20 will crash at launch unless androidx.multidex:multidex is packaged and initialized. Please add the dependency and ensure VolumeLockrApplication calls MultiDex.install() (or extends MultiDexApplication) so the app boots on legacy devices.

 dependencies {
@@
-    implementation 'androidx.preference:preference-ktx:1.1.1'
+    implementation 'androidx.preference:preference-ktx:1.1.1'
+    implementation 'androidx.multidex:multidex:2.0.1'
 }

If VolumeLockrApplication doesn’t already handle it, add:

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    MultiDex.install(this)
}
🤖 Prompt for AI Agents
In app/build.gradle around line 18, multiDexEnabled is set true but the multidex
runtime dependency is missing; add the AndroidX multidex dependency
(implementation "androidx.multidex:multidex:2.0.1") to the dependencies block
and keep multiDexEnabled true; then ensure the app's VolumeLockrApplication
initializes multidex on pre‑21 devices by either extending
androidx.multidex.MultiDexApplication or adding an override of attachBaseContext
that calls MultiDex.install(this) so the app won't crash on API 19–20.

Comment on lines +32 to +37
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(applicationContext, NOTIFICATION_PERMISSION)
== PackageManager.PERMISSION_DENIED) {
requestPermissions(arrayOf(NOTIFICATION_PERMISSION), PERMISSION_REQUEST_CODE)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use correct API level constant for POST_NOTIFICATIONS.

The notification permission check uses Build.VERSION_CODES.M (API 23), but POST_NOTIFICATIONS was introduced in API 33 (TIRAMISU). This will cause the permission check to run on API levels 23-32 where the permission doesn't exist.

Apply this diff to use the correct API level:

-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
             if (ContextCompat.checkSelfPermission(applicationContext, NOTIFICATION_PERMISSION)
                 == PackageManager.PERMISSION_DENIED) {
                 requestPermissions(arrayOf(NOTIFICATION_PERMISSION), PERMISSION_REQUEST_CODE)
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(applicationContext, NOTIFICATION_PERMISSION)
== PackageManager.PERMISSION_DENIED) {
requestPermissions(arrayOf(NOTIFICATION_PERMISSION), PERMISSION_REQUEST_CODE)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(applicationContext, NOTIFICATION_PERMISSION)
== PackageManager.PERMISSION_DENIED) {
requestPermissions(arrayOf(NOTIFICATION_PERMISSION), PERMISSION_REQUEST_CODE)
}
}
🤖 Prompt for AI Agents
In app/src/main/java/com/klee/volumelockr/ui/MainActivity.kt around lines 32 to
37, the code checks for POST_NOTIFICATIONS using Build.VERSION_CODES.M (API 23)
which is incorrect because POST_NOTIFICATIONS was introduced in API 33; update
the API-level guard to check Build.VERSION_CODES.TIRAMISU (or SDK_INT >= 33)
before requesting or checking NOTIFICATION_PERMISSION so the permission logic
only runs on Android 13+ where the permission exists.

Comment on lines 64 to 74
private fun registerSeekBarCallback(holder: ViewHolder, volume: Volume) {
val listener = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(view: SeekBar?, progress: Int, fromUser: Boolean) {
val listener =
Slider.OnChangeListener { _, value, _ ->
if (volume.stream != AudioManager.STREAM_NOTIFICATION || mService?.getMode() == 2) {
mAudioManager.setStreamVolume(volume.stream, progress, 0)
mAudioManager.setStreamVolume(volume.stream, value.toInt(), 0)
}

volume.value = progress
}

override fun onStartTrackingTouch(view: SeekBar?) {
volume.value = value.toInt()
}

override fun onStopTrackingTouch(view: SeekBar?) {
}
}
holder.binding.seekBar.setOnSeekBarChangeListener(listener)
holder.binding.slider.addOnChangeListener(listener)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Memory leak: Multiple listeners accumulate on slider.

Each time onBindViewHolder is called for a ViewHolder (which happens frequently in RecyclerView during scrolling or data updates), a new OnChangeListener is added to the slider without clearing previous listeners. This causes:

  • The callback to be invoked multiple times for a single slider change
  • Memory leaks as old listeners accumulate
  • Potential performance degradation

Recommended solution: Clear existing listeners before adding a new one:

 private fun registerSeekBarCallback(holder: ViewHolder, volume: Volume) {
+    holder.binding.slider.clearOnChangeListeners()
     val listener =
         Slider.OnChangeListener { _, value, _ ->
             if (volume.stream != AudioManager.STREAM_NOTIFICATION || mService?.getMode() == 2) {
                 mAudioManager.setStreamVolume(volume.stream, value.toInt(), 0)
             }

             volume.value = value.toInt()
         }
     holder.binding.slider.addOnChangeListener(listener)
 }

Alternatively, consider registering the listener once during ViewHolder creation in onCreateViewHolder if the callback logic doesn't need per-bind customization (though this would require restructuring to pass volume data differently).

🤖 Prompt for AI Agents
In app/src/main/java/com/klee/volumelockr/ui/VolumeAdapter.kt around lines 64 to
74, the code adds a new Slider.OnChangeListener on every bind causing multiple
callbacks and memory leaks; fix it by removing existing listeners before adding
a new one (e.g., call slider.clearOnChangeListeners() or remove the previous
listener) or move listener registration to onCreateViewHolder and update the
ViewHolder’s bound Volume via a stored reference so the listener is only
registered once.

VolumeLockr allows you to control your Android device volume levels and set locks for each one of them.

<p><img src="https://github.com/jonathanklee/VolumeLockr/blob/main/screenshot.png" width="200"/>&nbsp&nbsp<img src="https://github.com/jonathanklee/VolumeLockr/blob/main/screenshot_night.png" width="200"/></p>
<p><img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png?raw=true" width="200"/>&nbsp&nbsp<img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png?raw=true" width="200"/></p>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add alt text to images for accessibility.

The screenshot images are missing alternative text, which is important for accessibility and SEO.

Apply this diff to add descriptive alt text:

-<p><img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png?raw=true" width="200"/>&nbsp&nbsp<img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png?raw=true" width="200"/></p>
+<p><img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png?raw=true" alt="VolumeLockr main screen showing volume controls" width="200"/>&nbsp&nbsp<img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png?raw=true" alt="VolumeLockr settings screen" width="200"/></p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p><img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png?raw=true" width="200"/>&nbsp&nbsp<img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png?raw=true" width="200"/></p>
<p><img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png?raw=true" alt="VolumeLockr main screen showing volume controls" width="200"/>&nbsp&nbsp<img src="https://github.com/jonathanklee/VolumeLockr/blob/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png?raw=true" alt="VolumeLockr settings screen" width="200"/></p>
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

4-4: Images should have alternate text (alt text)

(MD045, no-alt-text)


4-4: Images should have alternate text (alt text)

(MD045, no-alt-text)

🤖 Prompt for AI Agents
In README.md around line 4, the two <img> tags lack alt attributes; update each
tag to include descriptive alt text for accessibility and SEO (e.g.,
alt="VolumeLockr screenshot — main screen" and alt="VolumeLockr screenshot —
settings screen"), preserving the existing src and width attributes so the
images render the same while providing meaningful alternative text.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants