Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7e8c1d0
Manual mode in BottomTabLayout
alexceend Aug 1, 2026
a31065f
ISO button in manual mode
alexceend Aug 3, 2026
7ffdfd0
Added onClickListener to ISO button
alexceend Aug 3, 2026
3ca0c53
Crash FIX, now initializes before (ISO button)
alexceend Aug 3, 2026
be93774
Created xml reource for pressed button
alexceend Aug 3, 2026
1cb20f5
Created Layout for iso slider
alexceend Aug 3, 2026
c15402f
ISO bar now visible onclick
alexceend Aug 3, 2026
d4f675e
Beautify iso slider and added more values
alexceend Aug 3, 2026
d4cd9f8
ISO now uses min & max and if given -1 value it returns to AE mode ON…
alexceend Aug 3, 2026
b7df8b1
AE returns to ON when MANUAL mode is not selected and changed setIso …
alexceend Aug 3, 2026
eb8e262
FIX: Now closing ISO slidebar won't remove the selected value
alexceend Aug 3, 2026
f2e8f5a
Made shutter speed functionality
alexceend Aug 3, 2026
efd0733
[FIX] ISO button and Shutter button don't overlap now
alexceend Aug 4, 2026
a20bf8c
Improved scalability of manual mode, now there's a list of buttons an…
alexceend Aug 4, 2026
e453807
Centered manual mode buttons
alexceend Aug 4, 2026
5897c33
Fixed AE mode not working on normal modes
alexceend Aug 4, 2026
9af0304
[FIX] Camera mode started with AE_MODE_OFF
alexceend Aug 4, 2026
31a71c2
Disabled onTouch feature in MANUAL mode because it made flickerings
alexceend Aug 4, 2026
f8ec733
Exposure value is now long
alexceend Aug 4, 2026
c81fdc2
Adapt ShutterSpeedBar values to hardware and software limitations
alexceend Aug 4, 2026
2a51c1c
Remove max property from <app.grapheneos.camera.ui.seekbar.ShutterSpe…
alexceend Aug 4, 2026
c359cdf
Fix crash on launch, variables were not initialized
alexceend Aug 4, 2026
4bc55d6
Delayed refresh shutter values so that Camera2 gets initialized first
alexceend Aug 4, 2026
16185ce
Standarize, rename variables, refactor to improve mantainability
alexceend Aug 4, 2026
22da600
Rename shutter speed to Exposure time because its more accurate
alexceend Aug 4, 2026
35c9842
Remove IDE-specific changes
alexceend Aug 4, 2026
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
79 changes: 78 additions & 1 deletion app/src/main/java/app/grapheneos/camera/CamConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ import com.google.zxing.BarcodeFormat
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import kotlin.concurrent.thread
import androidx.camera.camera2.interop.Camera2CameraControl
import androidx.camera.camera2.interop.CaptureRequestOptions
import android.hardware.camera2.CaptureRequest
import android.util.Range

// note that enum constant name is used as a name of a SharedPreferences instance
enum class CameraMode(val extensionMode: Int, val uiName: Int) {
Expand All @@ -76,6 +80,7 @@ enum class CameraMode(val extensionMode: Int, val uiName: Int) {
HDR(ExtensionMode.HDR, R.string.hdr_mode),
CAMERA(ExtensionMode.NONE, R.string.camera),
VIDEO(ExtensionMode.NONE, R.string.video),
MANUAL(ExtensionMode.NONE, R.string.manual_mode)
}

@SuppressLint("UnsafeOptInUsageError")
Expand Down Expand Up @@ -274,6 +279,9 @@ class CamConfig(private val mActivity: MainActivity) {

var lastCapturedItem: CapturedItem? = null

var manualIsoValue: Int? = null
var manualExposureTimeValue: Long? = null

init {
if (mActivity !is SecureActivity) {
CapturedItems.init(mActivity, this)
Expand Down Expand Up @@ -313,6 +321,9 @@ class CamConfig(private val mActivity: MainActivity) {
var isQRMode = false
private set

var isManualMode = false
private set

val isFlashAvailable: Boolean
get() = camera?.cameraInfo?.hasFlashUnit() ?: false

Expand Down Expand Up @@ -1016,6 +1027,46 @@ class CamConfig(private val mActivity: MainActivity) {
}
}

@androidx.annotation.OptIn(androidx.camera.camera2.interop.ExperimentalCamera2Interop::class)
fun getIsoRange(): Range<Int>? {
val cameraInfo = camera?.cameraInfo ?: return null
return Camera2CameraInfo.from(cameraInfo)
.getCameraCharacteristic(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE)
}

@androidx.annotation.OptIn(androidx.camera.camera2.interop.ExperimentalCamera2Interop::class)
fun getShutterRange(): Range<Long>? {
val cameraInfo = camera?.cameraInfo ?: return null
return Camera2CameraInfo.from(cameraInfo)
.getCameraCharacteristic(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE)
}

/**
* Applies manual ISO and Exposure Time settings to the camera hardware.
* If camera is not in manual mode, it restores Auto Exposure (AE) mode.
*/
@androidx.annotation.OptIn(androidx.camera.camera2.interop.ExperimentalCamera2Interop::class)
fun applyManualSettings() {
val cameraControl = camera?.cameraControl ?: return
val camera2CameraControl = Camera2CameraControl.from(cameraControl)
val builder = CaptureRequestOptions.Builder()

if (!mActivity.camConfig.isManualMode) {
builder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
} else {
builder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
manualIsoValue?.let {
builder.setCaptureRequestOption(CaptureRequest.SENSOR_SENSITIVITY, it)
}
manualExposureTimeValue?.let {
builder.setCaptureRequestOption(CaptureRequest.SENSOR_EXPOSURE_TIME, it)
}
}

camera2CameraControl.setCaptureRequestOptions(builder.build())
}


fun toggleAspectRatio() {
aspectRatio = if (aspectRatio == AspectRatio.RATIO_16_9) {
AspectRatio.RATIO_4_3
Expand Down Expand Up @@ -1786,6 +1837,8 @@ class CamConfig(private val mActivity: MainActivity) {

camera?.cameraInfo?.exposureState?.let { mActivity.exposureBar.setExposureConfig(it) }

applyManualSettings()

mActivity.settingsDialog.torchToggle.isChecked = false

// Focus camera on touch/tap
Expand Down Expand Up @@ -1860,7 +1913,7 @@ class CamConfig(private val mActivity: MainActivity) {
private fun availableModes(): Set<CameraMode> {
return CameraMode.entries.filter {
when (it) {
CameraMode.CAMERA, CameraMode.VIDEO -> true
CameraMode.CAMERA, CameraMode.VIDEO, CameraMode.MANUAL -> true
CameraMode.QR_SCAN -> mActivity !is SecureMainActivity
else -> {
check(it.extensionMode != ExtensionMode.NONE)
Expand Down Expand Up @@ -2013,6 +2066,8 @@ class CamConfig(private val mActivity: MainActivity) {

isVideoMode = mode == CameraMode.VIDEO

isManualMode = mode == CameraMode.MANUAL

if (isQRMode) {
mActivity.qrOverlay.visibility = View.VISIBLE
mActivity.thirdOption.visibility = View.INVISIBLE
Expand All @@ -2036,6 +2091,13 @@ class CamConfig(private val mActivity: MainActivity) {
mActivity.setCaptureButtonIcon(R.drawable.torch_off_button, R.string.turn_torch_on)

mActivity.micOffIcon.visibility = View.GONE
mActivity.isoButton.visibility = View.GONE
mActivity.isoButton.isSelected = false
mActivity.isoSliderContainer.visibility = View.GONE
//mActivity.updateIsoButtonUI()
manualIsoValue = null
manualExposureTimeValue = null
applyManualSettings()
} else {
mActivity.qrOverlay.visibility = View.INVISIBLE
mActivity.thirdOption.visibility = View.VISIBLE
Expand All @@ -2052,6 +2114,21 @@ class CamConfig(private val mActivity: MainActivity) {
mActivity.setCaptureButtonIcon(R.drawable.camera_shutter, R.string.capture)
mActivity.micOffIcon.visibility = View.GONE
}

mActivity.isoButton.visibility = if (isManualMode) View.VISIBLE else View.GONE
mActivity.exposureTimeButton.visibility = if (isManualMode) View.VISIBLE else View.GONE
if (!isManualMode) {
mActivity.deselectAllManualControls()
manualIsoValue = null
manualExposureTimeValue = null
applyManualSettings()
mActivity.startFocusTimer()
}else{
manualIsoValue = mActivity.isoSeekBar.getCurrentIsoValue()
manualExposureTimeValue = mActivity.exposureTimeBar.getCurrentExposureTimeValue()
applyManualSettings()
mActivity.cancelFocusTimer()
}
}

mActivity.updateSelfTimerBadge()
Expand Down
119 changes: 117 additions & 2 deletions app/src/main/java/app/grapheneos/camera/ui/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.ImageDecoder
import android.graphics.Point
import android.graphics.Rect
Expand Down Expand Up @@ -87,6 +88,8 @@ import app.grapheneos.camera.ui.QROverlay
import app.grapheneos.camera.ui.QRToggle
import app.grapheneos.camera.ui.SettingsDialog
import app.grapheneos.camera.ui.seekbar.ExposureBar
import app.grapheneos.camera.ui.seekbar.IsoBar
import app.grapheneos.camera.ui.seekbar.ExposureTimeBar
import app.grapheneos.camera.ui.seekbar.ZoomBar
import app.grapheneos.camera.ui.showIgnoringShortEdgeMode
import app.grapheneos.camera.util.CameraControl
Expand Down Expand Up @@ -229,8 +232,15 @@ open class MainActivity : AppCompatActivity(),
previewView.height / 2.0f, QROverlay.RATIO
)

// In manual mode, we only want to focus (AF), not change exposure (AE)
val flags = if (camConfig.isManualMode) {
FocusMeteringAction.FLAG_AF
} else {
FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE or FocusMeteringAction.FLAG_AWB
}

camConfig.camera?.cameraControl?.startFocusAndMetering(
FocusMeteringAction.Builder(autoFocusPoint).disableAutoCancel().build()
FocusMeteringAction.Builder(autoFocusPoint, flags).disableAutoCancel().build()
)

startFocusTimer()
Expand All @@ -248,6 +258,41 @@ open class MainActivity : AppCompatActivity(),
}

lateinit var micOffIcon: ImageView
lateinit var isoButton: TextView

lateinit var isoSeekBar: IsoBar
lateinit var isoSliderContainer: View
lateinit var isoValueText: TextView

lateinit var exposureTimeButton: TextView
lateinit var exposureTimeBar: ExposureTimeBar
lateinit var exposureTimeSliderContainer: View
lateinit var exposureTimeValueText: TextView

private val manualControlPairs by lazy {
listOf(
isoButton to isoSliderContainer,
exposureTimeButton to exposureTimeSliderContainer
)
}


fun updateManualButtonsUI(){
val buttons : List<TextView> = listOf(isoButton, exposureTimeButton)
for(b in buttons){
updateManualButtonUI(b)
}
}

private fun updateManualButtonUI(button: TextView) {
if (button.isSelected) {
button.setBackgroundResource(R.drawable.manual_button_pressed_bg)
button.setTextColor(Color.BLACK)
} else {
button.setBackgroundResource(R.drawable.manual_button_bg)
button.setTextColor(Color.WHITE)
}
}

private var shouldRestartRecording = false

Expand Down Expand Up @@ -617,6 +662,23 @@ open class MainActivity : AppCompatActivity(),
camConfig = CamConfig(this)
cameraControl = CameraControl(camConfig)
mainOverlay = binding.mainOverlay
isoButton = binding.isoButton

isoSliderContainer = binding.isoSliderContainer
isoValueText = binding.isoValueText
exposureTimeButton = binding.exposureTimeButton
exposureTimeValueText = binding.exposureTimeValueText

isoSeekBar = binding.isoSeekbar
exposureTimeBar = binding.exposureTimeSeekbar

isoSeekBar.setMainActivity(this)
exposureTimeBar.setMainActivity(this)

exposureTimeSliderContainer = binding.exposureTimeSliderContainer



imageCapturer = ImageCapturer(this)
videoCapturer = VideoCapturer(this)
thirdOption = binding.thirdOption
Expand Down Expand Up @@ -645,6 +707,8 @@ open class MainActivity : AppCompatActivity(),
if (state == StreamState.STREAMING) {
mainOverlay.visibility = View.INVISIBLE
camConfig.reloadSettings()
isoSeekBar.refreshIsoValues()
exposureTimeBar.refreshExposureTimeValues()
if (!camConfig.isQRMode) {
previewGrid.visibility = View.VISIBLE
if (!settingsDialog.isShowing) {
Expand Down Expand Up @@ -779,6 +843,38 @@ open class MainActivity : AppCompatActivity(),
}
}

isoButton.setOnClickListener {
val targetState = !it.isSelected
deselectAllManualControls()
it.isSelected = targetState

if (it.isSelected) {
isoSliderContainer.visibility = View.VISIBLE
if (camConfig.manualIsoValue == null) {
camConfig.manualIsoValue = isoSeekBar.getCurrentIsoValue()
}
} else {
isoSliderContainer.visibility = View.GONE
}
camConfig.applyManualSettings()
updateManualButtonsUI()
}

exposureTimeButton.setOnClickListener {
val targetState = !it.isSelected
deselectAllManualControls()
it.isSelected = targetState

if (it.isSelected) {
exposureTimeSliderContainer.visibility = View.VISIBLE
camConfig.manualExposureTimeValue = exposureTimeBar.getCurrentExposureTimeValue()
} else {
exposureTimeSliderContainer.visibility = View.GONE
}
camConfig.applyManualSettings()
updateManualButtonsUI()
}

cancelButtonView = binding.cancelButton

zoomBar = binding.zoomBar
Expand Down Expand Up @@ -1233,13 +1329,23 @@ open class MainActivity : AppCompatActivity(),
if (camConfig.isQRMode)
return false

if(camConfig.isManualMode)
return false

val x = event.x
val y = event.y

val autoFocusPoint = previewView.meteringPointFactory.createPoint(x, y)
animateFocusRing(x, y)

val focusBuilder = FocusMeteringAction.Builder(autoFocusPoint)
// In manual mode, we only want to focus (AF), not change exposure (AE)
val flags = if (camConfig.isManualMode) {
FocusMeteringAction.FLAG_AF
} else {
FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE or FocusMeteringAction.FLAG_AWB
}

val focusBuilder = FocusMeteringAction.Builder(autoFocusPoint, flags)

if (!camConfig.isVideoMode) {
camConfig.mPlayer.playFocusStartSound()
Expand Down Expand Up @@ -1805,6 +1911,15 @@ open class MainActivity : AppCompatActivity(),
application.resetPreventScreenFromSleeping()
}

fun deselectAllManualControls() {
manualControlPairs.forEach { (button, container) ->
button.isSelected = false
container.visibility = View.GONE
}
updateManualButtonsUI()
}


@Volatile var isStarted = false

override fun onStart() {
Expand Down
Loading