Skip to content
This repository has been archived by the owner on Jan 23, 2024. It is now read-only.

Commit

Permalink
vibe options
Browse files Browse the repository at this point in the history
  • Loading branch information
lucky authored and lucky committed Jun 22, 2022
1 parent 79ce2a5 commit 573458c
Show file tree
Hide file tree
Showing 18 changed files with 212 additions and 108 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/gradle-wrapper-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: "Validate Gradle Wrapper"
on: [push, pull_request]

jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
12 changes: 6 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {
applicationId "me.lucky.volta"
minSdk 23
targetSdk 32
versionCode 5
versionName "1.0.4"
versionCode 6
versionName "1.0.5"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -39,10 +39,10 @@ android {
}

dependencies {
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 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
Expand Down
3 changes: 0 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@
<uses-permission android:name="android.permission.FLASHLIGHT" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
<uses-feature android:name="android.hardware.sensor.proximity" android:required="false" />

<application
android:allowBackup="true"
android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".Application"
Expand Down
110 changes: 24 additions & 86 deletions app/src/main/java/me/lucky/volta/AccessibilityService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@ package me.lucky.volta

import android.Manifest
import android.accessibilityservice.AccessibilityService
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.media.AudioManager
import android.os.*
import android.view.KeyEvent
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import kotlin.concurrent.timerTask

class AccessibilityService : AccessibilityService() {
Expand All @@ -24,19 +19,18 @@ class AccessibilityService : AccessibilityService() {
}

private lateinit var prefs: Preferences
private lateinit var torchCallback: TorchCallback
private lateinit var torchManager: TorchManager
private var audioManager: AudioManager? = null
private var cameraManager: CameraManager? = null
private var vibrator: Vibrator? = null
@RequiresApi(Build.VERSION_CODES.O)
private var vibrationEffect: VibrationEffect? = null
private var longDownTask: Timer? = null
private var longUpTask: Timer? = null
private var doubleUpTask: Timer? = null
private val longDownFlag = AtomicBoolean()
private val longUpFlag = AtomicBoolean()
private val doubleUpFlag = AtomicBoolean()
private val upTime = AtomicLong()
private var longDownFlag = false
private var longUpFlag = false
private var doubleUpFlag = false
private var upTime = 0L

override fun onCreate() {
super.onCreate()
Expand All @@ -52,31 +46,22 @@ class AccessibilityService : AccessibilityService() {
longDownTask?.cancel()
longUpTask?.cancel()
doubleUpTask?.cancel()
cameraManager?.unregisterTorchCallback(torchCallback)
torchCallback.disableFlashlightTask?.cancel()
torchManager.deinit()
}

private fun init() {
prefs = Preferences(this)
torchCallback = TorchCallback(WeakReference(this))
torchManager = TorchManager(this)
audioManager = getSystemService(AudioManager::class.java)
cameraManager = getSystemService(CameraManager::class.java)
vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
getSystemService(VibratorManager::class.java)?.defaultVibrator
} else {
else
getSystemService(Vibrator::class.java)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
vibrationEffect = VibrationEffect.createOneShot(
VIBE_DURATION,
VibrationEffect.DEFAULT_AMPLITUDE,
)
}
try {
cameraManager?.registerTorchCallback(torchCallback, null)
} catch (exc: IllegalArgumentException) {
cameraManager = null
}
}

@RequiresPermission(Manifest.permission.VIBRATE)
Expand All @@ -94,7 +79,7 @@ class AccessibilityService : AccessibilityService() {

override fun onKeyEvent(event: KeyEvent?): Boolean {
if (event == null ||
!prefs.isServiceEnabled ||
!prefs.isEnabled ||
audioManager?.mode != AudioManager.MODE_NORMAL) return false
val isMusicActive = audioManager?.isMusicActive == true
if (prefs.isTrackChecked && isMusicActive)
Expand All @@ -115,15 +100,15 @@ class AccessibilityService : AccessibilityService() {
longDownTask?.cancel()
longDownTask = Timer()
longDownTask?.schedule(timerTask {
longDownFlag.set(true)
longDownFlag = true
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS)
vibrate()
if (prefs.trackOptions.and(TrackOption.VIBRATE.value) != 0) vibrate()
}, LONG_PRESS_DURATION)
return true
}
KeyEvent.ACTION_UP -> {
longDownTask?.cancel()
if (!longDownFlag.getAndSet(false)) volumeDown()
if (!longDownFlag) volumeDown() else longDownFlag = false
return true
}
}
Expand Down Expand Up @@ -163,15 +148,15 @@ class AccessibilityService : AccessibilityService() {
longUpTask?.cancel()
longUpTask = Timer()
longUpTask?.schedule(timerTask {
longUpFlag.set(true)
longUpFlag = true
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT)
vibrate()
if (prefs.trackOptions.and(TrackOption.VIBRATE.value) != 0) vibrate()
}, LONG_PRESS_DURATION)
return true
}
KeyEvent.ACTION_UP -> {
longUpTask?.cancel()
if (!longUpFlag.getAndSet(false)) volumeUp()
if (!longUpFlag) volumeUp() else longUpFlag = false
return true
}
}
Expand All @@ -181,70 +166,23 @@ class AccessibilityService : AccessibilityService() {
private fun flashlight(event: KeyEvent): Boolean {
when (event.action) {
KeyEvent.ACTION_DOWN -> {
if (event.eventTime - upTime.getAndSet(event.eventTime) < DOUBLE_PRESS_DURATION) {
if (event.eventTime - upTime < DOUBLE_PRESS_DURATION) {
doubleUpTask?.cancel()
doubleUpFlag.set(true)
toggleFlashlight()
vibrate()
}
doubleUpFlag = true
torchManager.toggle()
if (prefs.flashlightOptions.and(FlashlightOption.VIBRATE.value) != 0) vibrate()
} else { upTime = event.eventTime }
return true
}
KeyEvent.ACTION_UP -> {
if (!doubleUpFlag.getAndSet(false)) {
if (!doubleUpFlag) {
doubleUpTask?.cancel()
doubleUpTask = Timer()
doubleUpTask?.schedule(timerTask { volumeUp() }, DOUBLE_PRESS_DURATION)
}
} else { doubleUpFlag = false }
return true
}
}
return false
}

private fun toggleFlashlight() {
val state = !torchCallback.state.get()
torchCallback.internalState.set(state)
if (!setTorchMode(state)) torchCallback.internalState.set(false)
}

@RequiresPermission("android.permission.FLASHLIGHT")
private fun setTorchMode(value: Boolean): Boolean {
try {
cameraManager?.setTorchMode(
cameraManager
?.cameraIdList
?.firstOrNull {
cameraManager
?.getCameraCharacteristics(it)
?.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
} ?: return false,
value,
)
} catch (exc: Exception) { return false }
return true
}

private class TorchCallback(
private val service: WeakReference<me.lucky.volta.AccessibilityService>,
) : CameraManager.TorchCallback() {
companion object {
private const val DISABLE_FLASHLIGHT_DELAY = 15 * 60 * 1000L
}

val state = AtomicBoolean()
val internalState = AtomicBoolean()
var disableFlashlightTask: Timer? = null

override fun onTorchModeChanged(cameraId: String, enabled: Boolean) {
super.onTorchModeChanged(cameraId, enabled)
disableFlashlightTask?.cancel()
state.set(enabled)
if (internalState.getAndSet(false) && enabled) {
disableFlashlightTask = Timer()
disableFlashlightTask?.schedule(timerTask {
service.get()?.setTorchMode(false)
}, DISABLE_FLASHLIGHT_DELAY)
}
}
}
}
56 changes: 53 additions & 3 deletions app/src/main/java/me/lucky/volta/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class MainActivity : AppCompatActivity() {
binding.apply {
track.isChecked = prefs.isTrackChecked
flashlight.isChecked = prefs.isFlashlightChecked
toggle.isChecked = prefs.isServiceEnabled
toggle.isChecked = prefs.isEnabled
}
}

Expand All @@ -56,17 +56,67 @@ class MainActivity : AppCompatActivity() {
track.setOnCheckedChangeListener { _, isChecked ->
prefs.isTrackChecked = isChecked
}
track.setOnLongClickListener {
showTrackOptions()
true
}
flashlight.setOnCheckedChangeListener { _, isChecked ->
prefs.isFlashlightChecked = isChecked
}
flashlight.setOnLongClickListener {
showFlashlightOptions()
true
}
toggle.setOnCheckedChangeListener { _, isChecked ->
prefs.isServiceEnabled = isChecked
prefs.isEnabled = isChecked
if (isChecked && !hasPermissions())
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
}
}
}

private fun showTrackOptions() {
var options = prefs.trackOptions
val values = TrackOption.values()
MaterialAlertDialogBuilder(this)
.setTitle(R.string.track)
.setMultiChoiceItems(
resources.getStringArray(R.array.track_options),
values.map { options.and(it.value) != 0 }.toBooleanArray()
) { _, index, isChecked ->
val flag = values[index]
options = when (isChecked) {
true -> options.or(flag.value)
false -> options.and(flag.value.inv())
}
}
.setPositiveButton(android.R.string.ok) { _, _ ->
prefs.trackOptions = options
}
.show()
}

private fun showFlashlightOptions() {
var options = prefs.flashlightOptions
val values = TrackOption.values()
MaterialAlertDialogBuilder(this)
.setTitle(R.string.flashlight)
.setMultiChoiceItems(
resources.getStringArray(R.array.flashlight_options),
values.map { options.and(it.value) != 0 }.toBooleanArray()
) { _, index, isChecked ->
val flag = values[index]
options = when (isChecked) {
true -> options.or(flag.value)
false -> options.and(flag.value.inv())
}
}
.setPositiveButton(android.R.string.ok) { _, _ ->
prefs.flashlightOptions = options
}
.show()
}

private fun showProminentDisclosure() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.prominent_disclosure_title)
Expand All @@ -91,7 +141,7 @@ class MainActivity : AppCompatActivity() {
}

private fun update() {
if (prefs.isServiceEnabled && !hasPermissions())
if (prefs.isEnabled && !hasPermissions())
Snackbar.make(
binding.toggle,
R.string.service_unavailable_popup,
Expand Down
29 changes: 25 additions & 4 deletions app/src/main/java/me/lucky/volta/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,48 @@ import androidx.preference.PreferenceManager

class Preferences(ctx: Context) {
companion object {
private const val SERVICE_ENABLED = "service_enabled"
private const val ENABLED = "enabled"
private const val TRACK_CHECKED = "track_checked"
private const val TRACK_OPTIONS = "track_options"
private const val FLASHLIGHT_CHECKED = "flashlight_checked"
private const val FLASHLIGHT_OPTIONS = "flashlight_options"
private const val SHOW_PROMINENT_DISCLOSURE = "show_prominent_disclosure"

// migration
private const val SERVICE_ENABLED = "service_enabled"
}

private val prefs = PreferenceManager.getDefaultSharedPreferences(ctx)

var isServiceEnabled: Boolean
get() = prefs.getBoolean(SERVICE_ENABLED, false)
set(value) = prefs.edit { putBoolean(SERVICE_ENABLED, value) }
var isEnabled: Boolean
get() = prefs.getBoolean(ENABLED, prefs.getBoolean(SERVICE_ENABLED, false))
set(value) = prefs.edit { putBoolean(ENABLED, value) }

var isTrackChecked: Boolean
get() = prefs.getBoolean(TRACK_CHECKED, false)
set(value) = prefs.edit { putBoolean(TRACK_CHECKED, value) }

var trackOptions: Int
get() = prefs.getInt(TRACK_OPTIONS, TrackOption.VIBRATE.value)
set(value) = prefs.edit { putInt(TRACK_OPTIONS, value) }

var isFlashlightChecked: Boolean
get() = prefs.getBoolean(FLASHLIGHT_CHECKED, false)
set(value) = prefs.edit { putBoolean(FLASHLIGHT_CHECKED, value) }

var flashlightOptions: Int
get() = prefs.getInt(FLASHLIGHT_OPTIONS, FlashlightOption.VIBRATE.value)
set(value) = prefs.edit { putInt(FLASHLIGHT_OPTIONS, value) }

var isShowProminentDisclosure: Boolean
get() = prefs.getBoolean(SHOW_PROMINENT_DISCLOSURE, true)
set(value) = prefs.edit { putBoolean(SHOW_PROMINENT_DISCLOSURE, value) }
}

enum class TrackOption(val value: Int) {
VIBRATE(1),
}

enum class FlashlightOption(val value: Int) {
VIBRATE(1),
}
Loading

0 comments on commit 573458c

Please sign in to comment.