Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# VolumeLockr
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>

## Installation
You can either download it from F-Droid or build it manually.
Expand Down
16 changes: 9 additions & 7 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.kapt'
id 'org.jetbrains.kotlin.kapt' apply false
id 'kotlin-android'
id 'com.mikepenz.aboutlibraries.plugin'
}

android {
compileSdkVersion 31
compileSdk 34
buildToolsVersion "30.0.3"

defaultConfig {
applicationId "com.klee.volumelockr"
minSdkVersion 16
targetSdkVersion 31
versionCode 9
versionName "1.5.0"
minSdkVersion 19
targetSdkVersion 34
versionCode 11
versionName "1.6.1"
multiDexEnabled true

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand All @@ -36,14 +37,15 @@ android {
buildFeatures {
viewBinding true
}
namespace 'com.klee.volumelockr'
}

dependencies {

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
Expand Down
12 changes: 8 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.klee.volumelockr">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_volumelockr"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_volumelockr_round"
android:supportsRtl="true"
android:theme="@style/Theme.Volume">
android:theme="@style/Theme.VolumeLockr">
<activity
android:name=".ui.MainActivity"
android:launchMode="singleTop"
Expand All @@ -24,7 +25,10 @@
</intent-filter>
</activity>

<service android:name=".service.VolumeService" />
<service
android:name=".service.VolumeService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />

<receiver
android:name=".BootReceiver"
Expand Down
43 changes: 23 additions & 20 deletions app/src/main/java/com/klee/volumelockr/service/VolumeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.ContentObserver
import android.media.AudioManager
import android.net.Uri
Expand All @@ -19,6 +20,8 @@ import android.os.Looper
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.core.app.ActivityCompat.requestPermissions
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
Expand All @@ -29,6 +32,8 @@ import com.klee.volumelockr.ui.SettingsFragment.Companion.ALLOW_LOWER
import java.util.Timer
import java.util.TimerTask
import kotlin.collections.HashMap
import androidx.core.content.edit


class VolumeService : Service() {

Expand Down Expand Up @@ -78,14 +83,12 @@ class VolumeService : Service() {
override fun onCreate() {
super.onCreate()

mAudioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager
mVolumeProvider = VolumeProvider(this)

loadPreferences()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
mMode = Settings.Global.getInt(contentResolver, MODE_RINGER_SETTING)
}
mMode = Settings.Global.getInt(contentResolver, MODE_RINGER_SETTING)

registerObservers()

Expand All @@ -95,9 +98,7 @@ class VolumeService : Service() {
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
mAllowLower = sharedPreferences.getBoolean(ALLOW_LOWER, false)

loadGeneralPreferences()
return START_STICKY
}

Expand Down Expand Up @@ -162,12 +163,13 @@ class VolumeService : Service() {

private fun savePreferences() {
val sharedPreferences = getSharedPreferences(APP_SHARED_PREFERENCES, MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString(LOCKS_KEY, Gson().toJson(mVolumeLock))
editor.apply()
sharedPreferences.edit {
putString(LOCKS_KEY, Gson().toJson(mVolumeLock))
}
}

private fun loadPreferences() {
loadGeneralPreferences()
val sharedPreferences = getSharedPreferences(APP_SHARED_PREFERENCES, MODE_PRIVATE)
class Token : TypeToken<HashMap<Int, Int>>()
val value = sharedPreferences.getString(LOCKS_KEY, "")
Expand All @@ -177,6 +179,11 @@ class VolumeService : Service() {
}
}

private fun loadGeneralPreferences() {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
mAllowLower = sharedPreferences.getBoolean(ALLOW_LOWER, false)
}

@WorkerThread
@Synchronized
private fun checkVolumes() {
Expand Down Expand Up @@ -207,9 +214,7 @@ class VolumeService : Service() {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
mMode = Settings.Global.getInt(contentResolver, MODE_RINGER_SETTING)
}
mMode = Settings.Global.getInt(contentResolver, MODE_RINGER_SETTING)

mModeListener?.invoke()
}
Expand All @@ -233,11 +238,9 @@ class VolumeService : Service() {
registerObserver(VOLUME_VOICE_HEADSET_SETTING)
registerObserver(VOLUME_VOICE_BT_SETTING)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
contentResolver.registerContentObserver(
Settings.Global.getUriFor(MODE_RINGER_SETTING), true, mModeObserver
)
}
contentResolver.registerContentObserver(
Settings.Global.getUriFor(MODE_RINGER_SETTING), true, mModeObserver
)
}

private fun registerObserver(setting: String) {
Expand All @@ -248,7 +251,7 @@ class VolumeService : Service() {
@Synchronized
fun tryShowNotification() {

if (mVolumeLock.size == 0) {
if (mVolumeLock.isEmpty()) {
return
}

Expand All @@ -267,7 +270,7 @@ class VolumeService : Service() {
@RequiresApi(Build.VERSION_CODES.N)
@Synchronized
fun tryHideNotification() {
if (mVolumeLock.size > 0) {
if (mVolumeLock.isNotEmpty()) {
return
}

Expand Down
22 changes: 15 additions & 7 deletions app/src/main/java/com/klee/volumelockr/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,44 @@ package com.klee.volumelockr.ui
import android.app.AlertDialog
import android.app.Dialog
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import com.klee.volumelockr.R
import com.klee.volumelockr.service.VolumeService

class MainActivity : AppCompatActivity() {

companion object {
private const val NOTIFICATION_PERMISSION = "android.permission.POST_NOTIFICATIONS"
private const val PERMISSION_REQUEST_CODE = 25
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

override fun onResume() {
super.onResume()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
checkDoNotDisturbPermission()
}

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)
}
}
}

@RequiresApi(Build.VERSION_CODES.M)
private fun checkDoNotDisturbPermission() {
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
getSystemService(NOTIFICATION_SERVICE) as NotificationManager

if (!notificationManager.isNotificationPolicyAccessGranted) {
PolicyAccessDialog().show(supportFragmentManager, PolicyAccessDialog.TAG)
Expand Down
37 changes: 14 additions & 23 deletions app/src/main/java/com/klee/volumelockr/ui/VolumeAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import android.media.AudioManager
import android.os.Build
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.SeekBar
import androidx.annotation.MainThread
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.slider.Slider
import com.klee.volumelockr.databinding.VolumeCardBinding
import com.klee.volumelockr.service.VolumeService
import com.klee.volumelockr.ui.SettingsFragment
Expand Down Expand Up @@ -44,11 +44,9 @@ class VolumeAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val volume = mVolumeList[position]
holder.binding.mediaTextView.text = volume.name
holder.binding.seekBar.progress = mService?.getLocks()?.get(volume.stream) ?: volume.value
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
holder.binding.seekBar.min = volume.min
}
holder.binding.seekBar.max = volume.max
holder.binding.slider.value = mService?.getLocks()?.get(volume.stream)?.toFloat() ?: volume.value.toFloat()
holder.binding.slider.valueFrom = volume.min.toFloat()
holder.binding.slider.valueTo = volume.max.toFloat()

registerSeekBarCallback(holder, volume)
registerSwitchButtonCallback(holder, volume)
Expand All @@ -58,28 +56,21 @@ class VolumeAdapter(
handleRingerMode(holder, volume)

if (isPasswordProtected()) {
holder.binding.seekBar.isEnabled = false
holder.binding.slider.isEnabled = false
holder.binding.switchButton.isEnabled = false
}
}

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)
}

private fun registerSwitchButtonCallback(holder: ViewHolder, volume: Volume) {
Expand All @@ -98,7 +89,7 @@ class VolumeAdapter(
for (key in it) {
if (volume.stream == key) {
holder.binding.switchButton.isChecked = true
holder.binding.seekBar.isEnabled = false
holder.binding.slider.isEnabled = false
}
}
}
Expand Down Expand Up @@ -128,7 +119,7 @@ class VolumeAdapter(

private fun handleRingerMode(holder: ViewHolder, volume: Volume) {
if (volume.stream == AudioManager.STREAM_NOTIFICATION) {
holder.binding.seekBar.isEnabled =
holder.binding.slider.isEnabled =
mService?.getMode() == 2 &&
mService?.getLocks()?.containsKey(AudioManager.STREAM_NOTIFICATION) == false
}
Expand All @@ -139,7 +130,7 @@ class VolumeAdapter(
it.addLock(volume.stream, volume.value)
adjustService()
adjustNotification()
holder.binding.seekBar.isEnabled = false
holder.binding.slider.isEnabled = false
}
}

Expand All @@ -148,7 +139,7 @@ class VolumeAdapter(
it.removeLock(volume.stream)
adjustService()
adjustNotification()
holder.binding.seekBar.isEnabled = true
holder.binding.slider.isEnabled = true
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.klee.volumelockr.ui

import android.app.Application
import com.google.android.material.color.DynamicColors

class VolumeLockrApplication: Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
}
}
12 changes: 12 additions & 0 deletions app/src/main/res/layout/preference_switch.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>

<!-- Derived from https://github.com/androidx/androidx/blob/005e9694795cee9a42375d80b0d813af9e700ac1/preference/preference/res/layout/preference_widget_switch_compat.xml -->
<!-- Thanks to https://stackoverflow.com/a/73782598/9077356 -->
<com.google.android.material.materialswitch.MaterialSwitch
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:background="@null" />
Loading