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
67 changes: 67 additions & 0 deletions app/src/main/java/app/grapheneos/camera/CamConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package app.grapheneos.camera
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
Expand Down Expand Up @@ -32,6 +34,7 @@ import androidx.camera.core.featuregroup.GroupableFeature
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.extensions.ExtensionMode
import androidx.camera.extensions.ExtensionsManager
import androidx.camera.lifecycle.ProcessCameraProvider
Expand All @@ -56,6 +59,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.zxing.BarcodeFormat
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import kotlin.math.log

// note that enum constant name is used as a name of a SharedPreferences instance
enum class CameraMode(val extensionMode: Int, val uiName: Int) {
Expand Down Expand Up @@ -119,6 +123,8 @@ class CamConfig(private val mActivity: MainActivity) {

const val WAIT_FOR_FOCUS_LOCK = "wait_for_focus_lock"

const val CAPTURE_RESOLUTION = "capture_resolution"

// const val IMAGE_FILE_FORMAT = "image_quality"
// const val VIDEO_FILE_FORMAT = "video_quality"
}
Expand Down Expand Up @@ -179,6 +185,14 @@ class CamConfig(private val mActivity: MainActivity) {

const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_BACK

fun aspectRatioToWidthHeight(aspectRatio: Int): Pair<Int, Int> {
return when (aspectRatio) {
AspectRatio.RATIO_16_9 -> Pair(16, 9)
AspectRatio.RATIO_4_3 -> Pair(4, 3)
else -> throw IllegalArgumentException("Unknown aspect ratio: $aspectRatio")
}
}

val commonFormats = arrayOf(
BarcodeFormat.AZTEC,
BarcodeFormat.QR_CODE,
Expand Down Expand Up @@ -722,6 +736,8 @@ class CamConfig(private val mActivity: MainActivity) {

if (isVideoMode) {
mActivity.settingsDialog.reloadQualities()
} else {
mActivity.settingsDialog.reloadResolutions()
}

if (lensFacing == CameraSelector.LENS_FACING_FRONT) {
Expand Down Expand Up @@ -923,6 +939,27 @@ class CamConfig(private val mActivity: MainActivity) {
}
}

var captureResolution: Size?
get() {
val value = commonPref.getString(SettingValues.Key.CAPTURE_RESOLUTION, null)
if (value.isNullOrEmpty()) return null
return try {
val parts = value.split("x")
Size(parts[0].toInt(), parts[1].toInt())
} catch (e: Exception) {
null
}
}
set(value) {
commonPref.edit {
if (value == null) {
remove(SettingValues.Key.CAPTURE_RESOLUTION)
} else {
putString(SettingValues.Key.CAPTURE_RESOLUTION, "${value.width}x${value.height}")
}
}
}

var selectHighestResolution: Boolean
get() {
return commonPref.getBoolean(
Expand Down Expand Up @@ -989,13 +1026,37 @@ class CamConfig(private val mActivity: MainActivity) {
} else {
AspectRatio.RATIO_16_9
}
// Clear capture resolution since available resolutions depend on aspect ratio
captureResolution = null
startCamera(true)
}

private fun getCurrentCameraInfo() : CameraInfo {
return cameraProvider!!.getCameraInfo(cameraSelector)
}

fun getAvailableImageResolutions(): List<Size> {
val cameraInfo = camera?.cameraInfo ?: return emptyList()
val camera2Info = Camera2CameraInfo.from(cameraInfo)
val characteristics = camera2Info.getCameraCharacteristic(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
) ?: return emptyList()

val sizes = characteristics.getOutputSizes(ImageFormat.JPEG) ?: return emptyList()

// Filter by current aspect ratio with 2% tolerance
val targetRatio = when (aspectRatio) {
AspectRatio.RATIO_16_9 -> 16.0 / 9.0
AspectRatio.RATIO_4_3 -> 4.0 / 3.0
else -> 4.0 / 3.0
}

return sizes.filter { size ->
val ratio = size.width.toDouble() / size.height.toDouble()
kotlin.math.abs(ratio - targetRatio) / targetRatio < 0.02
}.sortedByDescending { it.width * it.height }
}

fun toggleCameraSelector() {

// Manually switch to the opposite lens facing
Expand Down Expand Up @@ -1229,6 +1290,12 @@ class CamConfig(private val mActivity: MainActivity) {
resolutionSelectorBuilder.setAllowedResolutionMode(ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE)
}

captureResolution?.let { size ->
resolutionSelectorBuilder.setResolutionStrategy(
ResolutionStrategy(size, ResolutionStrategy.FALLBACK_RULE_NONE)
)
}

it.setResolutionSelector(resolutionSelectorBuilder.build())

it.setFlashMode(flashMode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.grapheneos.camera.capturer
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.graphics.ImageFormat
import android.graphics.Rect
Expand Down Expand Up @@ -38,7 +39,6 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean

// see com.android.externalstorage.ExternalStorageProvider and
// com.android.internal.content.FileSystemProvider
Expand Down
69 changes: 69 additions & 0 deletions app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
private lateinit var vQAdapter: ArrayAdapter<String>
private var focusTimeoutSpinner: Spinner
private var timerSpinner: Spinner
private var captureResolutionSpinner: Spinner
private lateinit var captureResolutionAdapter: ArrayAdapter<String>
private var availableResolutions: List<android.util.Size> = emptyList()
private var aspectRatioForResolutions: Int? = null;

var mScrollView: ScrollView
var mScrollViewContent: View
Expand All @@ -83,6 +87,7 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
private var selfIlluminationSetting: View
private var videoQualitySetting: View
private var timerSetting: View
private var captureResolutionSetting: View

var settingsFrame: View

Expand Down Expand Up @@ -321,6 +326,22 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
override fun onNothingSelected(p0: AdapterView<*>?) {}
}

captureResolutionSpinner = binding.captureResolutionSpinner
captureResolutionSpinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
p0: AdapterView<*>?,
p1: View?,
position: Int,
p3: Long
) {
camConfig.captureResolution = indexToResolution(position)
camConfig.startCamera(true)
}

override fun onNothingSelected(p0: AdapterView<*>?) {}
}

mScrollView = binding.settingsScrollview
mScrollViewContent = binding.settingsScrollviewContent

Expand All @@ -329,6 +350,7 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
selfIlluminationSetting = binding.selfIlluminationSetting
videoQualitySetting = binding.videoQualitySetting
timerSetting = binding.timerSetting
captureResolutionSetting = binding.captureResolutionSetting

includeAudioToggle = binding.includeAudioSwitch
includeAudioToggle.setOnCheckedChangeListener { _, _ ->
Expand Down Expand Up @@ -466,8 +488,55 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
} else {
View.VISIBLE
}

captureResolutionSetting.visibility = if (camConfig.isVideoMode) {
View.GONE
} else {
View.VISIBLE
}
}

private fun resolutionToIndex(size: android.util.Size?): Int {
if (size == null) return 0 // Highest resolution
return availableResolutions.indexOfFirst { it.width == size.width && it.height == size.height }
}

private fun indexToResolution(index: Int): android.util.Size? {
return if (index >= 0 && index < availableResolutions.size) {
availableResolutions[index]
} else {
null
}
}

fun reloadResolutions() {
if (aspectRatioForResolutions !== null && aspectRatioForResolutions?.equals(camConfig.aspectRatio) == true) {
// Use cached data
return;
}

availableResolutions = camConfig.getAvailableImageResolutions()

val titles = mutableListOf<String>()
availableResolutions.forEach { size ->
titles.add("${size.width}x${size.height}")
}

captureResolutionAdapter = ArrayAdapter<String>(
mActivity,
android.R.layout.simple_spinner_item,
titles
)

captureResolutionAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
)

captureResolutionSpinner.adapter = captureResolutionAdapter
captureResolutionSpinner.setSelection(resolutionToIndex(camConfig.captureResolution))

aspectRatioForResolutions = camConfig.aspectRatio;
}

fun updateFocusTimeout(selectedOption: String) {

Expand Down
34 changes: 34 additions & 0 deletions app/src/main/res/layout/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,40 @@
</FrameLayout>
</LinearLayout>

<LinearLayout
android:id="@+id/capture_resolution_setting"
android:layout_width="match_parent"
android:layout_height="@dimen/settings_dialog_menu_item_height"
android:paddingVertical="@dimen/settings_dialog_menu_item_vertical"
android:paddingHorizontal="@dimen/settings_dialog_menu_item_horizontal"
android:layout_gravity="end"
android:gravity="center_vertical"
android:orientation="horizontal">

<TextView
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/capture_resolution"/>

<FrameLayout
android:layout_height="wrap_content"
android:padding="0dp"
android:layout_margin="0dp"
android:layout_width="match_parent">

<Spinner
android:id="@+id/capture_resolution_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:padding="0dp"
android:layout_margin="0dp"
android:layout_gravity="end"/>

</FrameLayout>
</LinearLayout>

<!-- Extra padding for the bottom of the list -->
<View
android:layout_width="match_parent"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<string name="self_illumination">Self Illumination</string>
<string name="focus_timeout">Focus Timeout</string>
<string name="timer">Timer</string>
<string name="capture_resolution">Capture Resolution</string>
<string name="cancel_timer">Cancel Timer</string>
<string name="video_capture_label">Record Video</string>

Expand Down