Merge pull request #174 from KryptKode/feat/brightness-control

Add torch brightness level support for Android 13+
This commit is contained in:
Tibor Kaputa 2022-11-15 15:37:41 +01:00 committed by GitHub
commit 858533e991
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 257 additions and 173 deletions

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
import android.content.res.ColorStateList
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.os.Bundle import android.os.Bundle
@ -19,6 +18,8 @@ import com.simplemobiletools.commons.models.FAQItem
import com.simplemobiletools.flashlight.BuildConfig import com.simplemobiletools.flashlight.BuildConfig
import com.simplemobiletools.flashlight.R import com.simplemobiletools.flashlight.R
import com.simplemobiletools.flashlight.extensions.config import com.simplemobiletools.flashlight.extensions.config
import com.simplemobiletools.flashlight.helpers.CameraTorchListener
import com.simplemobiletools.flashlight.helpers.MIN_BRIGHTNESS_LEVEL
import com.simplemobiletools.flashlight.helpers.MyCameraImpl import com.simplemobiletools.flashlight.helpers.MyCameraImpl
import com.simplemobiletools.flashlight.models.Events import com.simplemobiletools.flashlight.models.Events
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
@ -179,10 +180,17 @@ class MainActivity : SimpleActivity() {
} }
private fun setupCameraImpl() { private fun setupCameraImpl() {
mCameraImpl = MyCameraImpl.newInstance(this) mCameraImpl = MyCameraImpl.newInstance(this, object : CameraTorchListener {
override fun onTorchEnabled(isEnabled: Boolean) {
if (mCameraImpl!!.supportsBrightnessControl()) {
brightness_bar.beVisibleIf(isEnabled)
}
}
})
if (config.turnFlashlightOn) { if (config.turnFlashlightOn) {
mCameraImpl!!.enableFlashlight() mCameraImpl!!.enableFlashlight()
} }
setupBrightness()
} }
private fun setupStroboscope() { private fun setupStroboscope() {
@ -211,6 +219,16 @@ class MainActivity : SimpleActivity() {
} }
} }
private fun setupBrightness() {
brightness_bar.max = mCameraImpl?.getMaximumBrightnessLevel() ?: MIN_BRIGHTNESS_LEVEL
brightness_bar.progress = mCameraImpl?.getCurrentBrightnessLevel() ?: MIN_BRIGHTNESS_LEVEL
brightness_bar.onSeekBarChangeListener { level ->
val newLevel = level.coerceAtLeast(MIN_BRIGHTNESS_LEVEL)
mCameraImpl?.updateBrightnessLevel(newLevel)
config.brightnessLevel = newLevel
}
}
private fun cameraPermissionGranted(isSOS: Boolean) { private fun cameraPermissionGranted(isSOS: Boolean) {
if (isSOS) { if (isSOS) {
val isSOSRunning = mCameraImpl!!.toggleSOS() val isSOSRunning = mCameraImpl!!.toggleSOS()

View File

@ -0,0 +1,12 @@
package com.simplemobiletools.flashlight.helpers
interface CameraFlash {
fun initialize()
fun toggleFlashlight(enable: Boolean)
fun changeTorchBrightness(level: Int) {}
fun getMaximumBrightnessLevel(): Int = DEFAULT_BRIGHTNESS_LEVEL
fun supportsBrightnessControl(): Boolean = false
fun getCurrentBrightnessLevel(): Int = DEFAULT_BRIGHTNESS_LEVEL
fun unregisterListeners(){}
fun release(){}
}

View File

@ -0,0 +1,5 @@
package com.simplemobiletools.flashlight.helpers
interface CameraTorchListener {
fun onTorchEnabled(isEnabled:Boolean)
}

View File

@ -40,4 +40,8 @@ class Config(context: Context) : BaseConfig(context) {
var forcePortraitMode: Boolean var forcePortraitMode: Boolean
get() = prefs.getBoolean(FORCE_PORTRAIT_MODE, true) get() = prefs.getBoolean(FORCE_PORTRAIT_MODE, true)
set(forcePortraitMode) = prefs.edit().putBoolean(FORCE_PORTRAIT_MODE, forcePortraitMode).apply() set(forcePortraitMode) = prefs.edit().putBoolean(FORCE_PORTRAIT_MODE, forcePortraitMode).apply()
var brightnessLevel: Int
get() = prefs.getInt(BRIGHTNESS_LEVEL, DEFAULT_BRIGHTNESS_LEVEL)
set(brightnessLevel) = prefs.edit().putInt(BRIGHTNESS_LEVEL, brightnessLevel).apply()
} }

View File

@ -11,3 +11,6 @@ const val STROBOSCOPE_FREQUENCY = "stroboscope_frequency"
const val STROBOSCOPE_PROGRESS = "stroboscope_progress" const val STROBOSCOPE_PROGRESS = "stroboscope_progress"
const val FORCE_PORTRAIT_MODE = "force_portrait_mode" const val FORCE_PORTRAIT_MODE = "force_portrait_mode"
const val SOS = "sos" const val SOS = "sos"
const val BRIGHTNESS_LEVEL = "brightness_level"
const val MIN_BRIGHTNESS_LEVEL = 1
const val DEFAULT_BRIGHTNESS_LEVEL = -1

View File

@ -0,0 +1,37 @@
@file:Suppress("DEPRECATION")
package com.simplemobiletools.flashlight.helpers
import android.graphics.SurfaceTexture
import android.hardware.Camera
class LollipopCameraFlash : CameraFlash {
private var camera: Camera? = null
private var params: Camera.Parameters? = null
override fun toggleFlashlight(enable: Boolean) {
if (camera == null || params == null || camera!!.parameters == null) {
return
}
val flashMode = if (enable) Camera.Parameters.FLASH_MODE_ON else Camera.Parameters.FLASH_MODE_OFF
params!!.flashMode = flashMode
camera!!.parameters = params
if (enable) {
val dummy = SurfaceTexture(1)
camera!!.setPreviewTexture(dummy)
camera!!.startPreview()
}
}
override fun initialize() {
camera = Camera.open()
params = camera!!.parameters
params!!.flashMode = Camera.Parameters.FLASH_MODE_OFF
camera!!.parameters = params
}
override fun release() {
camera!!.release()
camera = null
}
}

View File

@ -1,37 +0,0 @@
package com.simplemobiletools.flashlight.helpers
import android.annotation.TargetApi
import android.content.Context
import android.hardware.camera2.CameraManager
import android.os.Build
import android.os.Handler
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.flashlight.models.Events
import org.greenrobot.eventbus.EventBus
internal class MarshmallowCamera constructor(val context: Context) {
private val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
private var cameraId: String? = null
init {
try {
cameraId = manager.cameraIdList[0] ?: "0"
} catch (e: Exception) {
context.showErrorToast(e)
}
}
@TargetApi(Build.VERSION_CODES.M)
fun toggleMarshmallowFlashlight(enable: Boolean) {
try {
manager.setTorchMode(cameraId!!, enable)
} catch (e: Exception) {
context.showErrorToast(e)
val mainRunnable = Runnable {
EventBus.getDefault().post(Events.CameraUnavailable())
}
Handler(context.mainLooper).post(mainRunnable)
}
}
}

View File

@ -0,0 +1,94 @@
package com.simplemobiletools.flashlight.helpers
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Build
import android.os.Handler
import androidx.annotation.RequiresApi
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.helpers.isTiramisuPlus
import com.simplemobiletools.flashlight.extensions.config
import com.simplemobiletools.flashlight.models.Events
import org.greenrobot.eventbus.EventBus
@RequiresApi(Build.VERSION_CODES.M)
internal class MarshmallowPlusCameraFlash(
private val context: Context,
private var cameraTorchListener: CameraTorchListener? = null,
) : CameraFlash {
private val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
private var cameraId: String? = null
private val torchCallback = object : CameraManager.TorchCallback() {
override fun onTorchModeChanged(cameraId: String, enabled: Boolean) {
cameraTorchListener?.onTorchEnabled(enabled)
}
}
init {
try {
cameraId = manager.cameraIdList[0] ?: "0"
} catch (e: Exception) {
context.showErrorToast(e)
}
}
override fun toggleFlashlight(enable: Boolean) {
try {
if (supportsBrightnessControl() && enable) {
val brightnessLevel = getCurrentBrightnessLevel()
changeTorchBrightness(brightnessLevel)
} else {
manager.setTorchMode(cameraId!!, enable)
}
} catch (e: Exception) {
context.showErrorToast(e)
val mainRunnable = Runnable {
EventBus.getDefault().post(Events.CameraUnavailable())
}
Handler(context.mainLooper).post(mainRunnable)
}
}
override fun changeTorchBrightness(level: Int) {
if (isTiramisuPlus()) {
manager.turnOnTorchWithStrengthLevel(cameraId!!, level)
}
}
override fun getMaximumBrightnessLevel(): Int {
return if (isTiramisuPlus()) {
val characteristics = manager.getCameraCharacteristics(cameraId!!)
characteristics.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL) ?: MIN_BRIGHTNESS_LEVEL
} else {
MIN_BRIGHTNESS_LEVEL
}
}
override fun supportsBrightnessControl(): Boolean {
val maxBrightnessLevel = getMaximumBrightnessLevel()
return maxBrightnessLevel > MIN_BRIGHTNESS_LEVEL
}
override fun getCurrentBrightnessLevel(): Int {
var brightnessLevel = context.config.brightnessLevel
if (brightnessLevel == DEFAULT_BRIGHTNESS_LEVEL) {
brightnessLevel = getMaximumBrightnessLevel()
}
return brightnessLevel
}
override fun initialize() {
manager.registerTorchCallback(torchCallback, Handler(context.mainLooper))
}
override fun unregisterListeners() {
manager.unregisterTorchCallback(torchCallback)
}
override fun release() {
cameraTorchListener = null
}
}

View File

@ -1,20 +1,17 @@
package com.simplemobiletools.flashlight.helpers package com.simplemobiletools.flashlight.helpers
import android.content.Context import android.content.Context
import android.graphics.SurfaceTexture
import android.hardware.Camera
import android.os.Handler import android.os.Handler
import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.helpers.isMarshmallowPlus import com.simplemobiletools.commons.helpers.isMarshmallowPlus
import com.simplemobiletools.commons.helpers.isNougatPlus
import com.simplemobiletools.flashlight.R import com.simplemobiletools.flashlight.R
import com.simplemobiletools.flashlight.extensions.config import com.simplemobiletools.flashlight.extensions.config
import com.simplemobiletools.flashlight.extensions.updateWidgets import com.simplemobiletools.flashlight.extensions.updateWidgets
import com.simplemobiletools.flashlight.models.Events import com.simplemobiletools.flashlight.models.Events
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
class MyCameraImpl(val context: Context) { class MyCameraImpl private constructor(val context: Context, private var cameraTorchListener: CameraTorchListener? = null) {
var stroboFrequency = 1000L var stroboFrequency = 1000L
companion object { companion object {
@ -23,15 +20,12 @@ class MyCameraImpl(val context: Context) {
private var u = 200L // The length of one dit (Time unit) private var u = 200L // The length of one dit (Time unit)
private val SOS = arrayListOf(u, u, u, u, u, u * 3, u * 3, u, u * 3, u, u * 3, u * 3, u, u, u, u, u, u * 7) private val SOS = arrayListOf(u, u, u, u, u, u * 3, u * 3, u, u * 3, u, u * 3, u * 3, u, u, u, u, u, u * 7)
private var camera: Camera? = null
private var params: Camera.Parameters? = null
private var isMarshmallow = false
private var shouldEnableFlashlight = false private var shouldEnableFlashlight = false
private var shouldEnableStroboscope = false private var shouldEnableStroboscope = false
private var shouldEnableSOS = false private var shouldEnableSOS = false
private var isStroboSOS = false // are we sending SOS, or casual stroboscope? private var isStroboSOS = false // are we sending SOS, or casual stroboscope?
private var marshmallowCamera: MarshmallowCamera? = null private var cameraFlash: CameraFlash? = null
@Volatile @Volatile
private var shouldStroboscopeStop = false private var shouldStroboscopeStop = false
@ -42,11 +36,10 @@ class MyCameraImpl(val context: Context) {
@Volatile @Volatile
private var isSOSRunning = false private var isSOSRunning = false
fun newInstance(context: Context) = MyCameraImpl(context) fun newInstance(context: Context, cameraTorchListener: CameraTorchListener? = null) = MyCameraImpl(context, cameraTorchListener)
} }
init { init {
isMarshmallow = isMarshmallowPlus()
handleCameraSetup() handleCameraSetup()
stroboFrequency = context.config.stroboscopeFrequency stroboFrequency = context.config.stroboscopeFrequency
} }
@ -57,6 +50,8 @@ class MyCameraImpl(val context: Context) {
} }
fun toggleStroboscope(): Boolean { fun toggleStroboscope(): Boolean {
handleCameraSetup()
if (isSOSRunning) { if (isSOSRunning) {
stopSOS() stopSOS()
shouldEnableStroboscope = true shouldEnableStroboscope = true
@ -68,6 +63,8 @@ class MyCameraImpl(val context: Context) {
disableFlashlight() disableFlashlight()
} }
cameraFlash!!.unregisterListeners()
if (!tryInitCamera()) { if (!tryInitCamera()) {
return false return false
} }
@ -87,6 +84,8 @@ class MyCameraImpl(val context: Context) {
} }
fun toggleSOS(): Boolean { fun toggleSOS(): Boolean {
handleCameraSetup()
if (isStroboscopeRunning) { if (isStroboscopeRunning) {
stopStroboscope() stopStroboscope()
shouldEnableSOS = true shouldEnableSOS = true
@ -106,6 +105,8 @@ class MyCameraImpl(val context: Context) {
disableFlashlight() disableFlashlight()
} }
cameraFlash!!.unregisterListeners()
return if (isSOSRunning) { return if (isSOSRunning) {
stopSOS() stopSOS()
false false
@ -121,58 +122,26 @@ class MyCameraImpl(val context: Context) {
} }
private fun tryInitCamera(): Boolean { private fun tryInitCamera(): Boolean {
if (!isNougatPlus()) { handleCameraSetup()
if (camera == null) { if (cameraFlash == null) {
initCamera() context.toast(R.string.camera_error)
} return false
if (camera == null) {
context.toast(R.string.camera_error)
return false
}
} }
return true return true
} }
fun handleCameraSetup() { fun handleCameraSetup() {
if (isMarshmallow) {
setupMarshmallowCamera()
} else {
setupCamera()
}
}
private fun setupMarshmallowCamera() {
if (marshmallowCamera == null) {
marshmallowCamera = MarshmallowCamera(context)
}
}
private fun setupCamera() {
if (isMarshmallow) {
return
}
if (camera == null) {
initCamera()
}
}
private fun initCamera() {
try { try {
camera = Camera.open() if (cameraFlash == null) {
params = camera!!.parameters cameraFlash = if (isMarshmallowPlus()) MarshmallowPlusCameraFlash(context, cameraTorchListener) else LollipopCameraFlash()
params!!.flashMode = Camera.Parameters.FLASH_MODE_OFF }
camera!!.parameters = params
} catch (e: Exception) { } catch (e: Exception) {
EventBus.getDefault().post(Events.CameraUnavailable()) EventBus.getDefault().post(Events.CameraUnavailable())
} }
} }
private fun checkFlashlight() { private fun checkFlashlight() {
if (camera == null) { handleCameraSetup()
handleCameraSetup()
}
if (isFlashlightOn) { if (isFlashlightOn) {
enableFlashlight() enableFlashlight()
@ -188,25 +157,12 @@ class MyCameraImpl(val context: Context) {
return return
} }
if (isMarshmallow) { try {
toggleMarshmallowFlashlight(true) cameraFlash!!.initialize()
} else { cameraFlash!!.toggleFlashlight(true)
try { } catch (e: Exception) {
if (camera == null || params == null || camera!!.parameters == null) { context.showErrorToast(e)
return disableFlashlight()
}
} catch (e: Exception) {
return
}
params!!.flashMode = Camera.Parameters.FLASH_MODE_TORCH
camera!!.parameters = params
try {
camera!!.startPreview()
} catch (e: Exception) {
context.showErrorToast(e)
disableFlashlight()
}
} }
val mainRunnable = Runnable { stateChanged(true) } val mainRunnable = Runnable { stateChanged(true) }
@ -218,22 +174,13 @@ class MyCameraImpl(val context: Context) {
return return
} }
if (isMarshmallow) { try {
toggleMarshmallowFlashlight(false) cameraFlash!!.toggleFlashlight(false)
} else { } catch (e: Exception) {
try { context.showErrorToast(e)
if (camera == null || params == null || camera!!.parameters == null) { disableFlashlight()
return
}
} catch (e: Exception) {
return
}
params!!.flashMode = Camera.Parameters.FLASH_MODE_OFF
camera!!.parameters = params
} }
stateChanged(false) stateChanged(false)
releaseCamera()
} }
private fun stateChanged(isEnabled: Boolean) { private fun stateChanged(isEnabled: Boolean) {
@ -242,17 +189,16 @@ class MyCameraImpl(val context: Context) {
context.updateWidgets(isEnabled) context.updateWidgets(isEnabled)
} }
private fun toggleMarshmallowFlashlight(enable: Boolean) {
marshmallowCamera!!.toggleMarshmallowFlashlight(enable)
}
fun releaseCamera() { fun releaseCamera() {
cameraFlash?.unregisterListeners()
if (isFlashlightOn) { if (isFlashlightOn) {
disableFlashlight() disableFlashlight()
} }
camera?.release() cameraFlash?.release()
camera = null cameraFlash = null
cameraTorchListener = null
isFlashlightOn = false isFlashlightOn = false
shouldStroboscopeStop = true shouldStroboscopeStop = true
@ -271,56 +217,28 @@ class MyCameraImpl(val context: Context) {
} }
var sosIndex = 0 var sosIndex = 0
if (isNougatPlus()) { handleCameraSetup()
while (!shouldStroboscopeStop) { while (!shouldStroboscopeStop) {
try {
marshmallowCamera!!.toggleMarshmallowFlashlight(true)
val onDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency
Thread.sleep(onDuration)
marshmallowCamera!!.toggleMarshmallowFlashlight(false)
val offDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency
Thread.sleep(offDuration)
} catch (e: Exception) {
shouldStroboscopeStop = true
}
}
} else {
if (camera == null) {
initCamera()
}
try { try {
val torchOn = camera!!.parameters ?: return@Runnable cameraFlash!!.toggleFlashlight(true)
val torchOff = camera!!.parameters val onDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency
torchOn.flashMode = Camera.Parameters.FLASH_MODE_TORCH Thread.sleep(onDuration)
torchOff.flashMode = Camera.Parameters.FLASH_MODE_OFF cameraFlash!!.toggleFlashlight(false)
val offDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency
val dummy = SurfaceTexture(1) Thread.sleep(offDuration)
camera!!.setPreviewTexture(dummy) } catch (e: Exception) {
camera!!.startPreview()
while (!shouldStroboscopeStop) {
camera!!.parameters = torchOn
val onDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency
Thread.sleep(onDuration)
camera!!.parameters = torchOff
val offDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency
Thread.sleep(offDuration)
}
if (camera != null) {
camera!!.parameters = torchOff
if (!shouldEnableFlashlight || isMarshmallow) {
camera!!.release()
camera = null
}
}
} catch (e: RuntimeException) {
shouldStroboscopeStop = true shouldStroboscopeStop = true
} }
} }
// disable flash immediately if stroboscope is stopped and normal flash mode is disabled
if (shouldStroboscopeStop && !shouldEnableFlashlight) {
handleCameraSetup()
cameraFlash!!.toggleFlashlight(false)
cameraFlash!!.release()
cameraFlash = null
}
shouldStroboscopeStop = false shouldStroboscopeStop = false
if (isStroboSOS) { if (isStroboSOS) {
isSOSRunning = false isSOSRunning = false
@ -343,4 +261,20 @@ class MyCameraImpl(val context: Context) {
} }
} }
} }
fun getMaximumBrightnessLevel(): Int {
return cameraFlash!!.getMaximumBrightnessLevel()
}
fun getCurrentBrightnessLevel(): Int {
return cameraFlash!!.getCurrentBrightnessLevel()
}
fun supportsBrightnessControl(): Boolean {
return cameraFlash!!.supportsBrightnessControl()
}
fun updateBrightnessLevel(level: Int) {
cameraFlash!!.changeTorchBrightness(level)
}
} }

View File

@ -105,6 +105,20 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stroboscope_btn" /> app:layout_constraintTop_toBottomOf="@+id/stroboscope_btn" />
<com.simplemobiletools.commons.views.MySeekBar
android:id="@+id/brightness_bar"
android:layout_width="@dimen/seekbar_width"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_margin"
android:paddingTop="@dimen/medium_margin"
android:paddingBottom="@dimen/medium_margin"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stroboscope_btn" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>