Skip to content

Commit de58a76

Browse files
committed
Allow selecting capture resolutions
1 parent f5e7db9 commit de58a76

5 files changed

Lines changed: 164 additions & 1 deletion

File tree

app/src/main/java/app/grapheneos/camera/CamConfig.kt

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package app.grapheneos.camera
33
import android.annotation.SuppressLint
44
import android.content.Context
55
import android.content.SharedPreferences
6+
import android.graphics.ImageFormat
7+
import android.hardware.camera2.CameraCharacteristics
68
import android.net.Uri
79
import android.os.Build
810
import android.provider.MediaStore
@@ -32,6 +34,7 @@ import androidx.camera.core.featuregroup.GroupableFeature
3234
import androidx.camera.core.resolutionselector.AspectRatioStrategy
3335
import androidx.camera.core.resolutionselector.ResolutionSelector
3436
import androidx.camera.core.resolutionselector.ResolutionStrategy
37+
import androidx.camera.camera2.interop.Camera2CameraInfo
3538
import androidx.camera.extensions.ExtensionMode
3639
import androidx.camera.extensions.ExtensionsManager
3740
import androidx.camera.lifecycle.ProcessCameraProvider
@@ -119,6 +122,8 @@ class CamConfig(private val mActivity: MainActivity) {
119122

120123
const val WAIT_FOR_FOCUS_LOCK = "wait_for_focus_lock"
121124

125+
const val CAPTURE_RESOLUTION = "capture_resolution"
126+
122127
// const val IMAGE_FILE_FORMAT = "image_quality"
123128
// const val VIDEO_FILE_FORMAT = "video_quality"
124129
}
@@ -179,6 +184,14 @@ class CamConfig(private val mActivity: MainActivity) {
179184

180185
const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_BACK
181186

187+
fun aspectRatioToWidthHeight(aspectRatio: Int): Pair<Int, Int> {
188+
return when (aspectRatio) {
189+
AspectRatio.RATIO_16_9 -> Pair(16, 9)
190+
AspectRatio.RATIO_4_3 -> Pair(4, 3)
191+
else -> throw IllegalArgumentException("Unknown aspect ratio: $aspectRatio")
192+
}
193+
}
194+
182195
val commonFormats = arrayOf(
183196
BarcodeFormat.AZTEC,
184197
BarcodeFormat.QR_CODE,
@@ -722,6 +735,8 @@ class CamConfig(private val mActivity: MainActivity) {
722735

723736
if (isVideoMode) {
724737
mActivity.settingsDialog.reloadQualities()
738+
} else {
739+
mActivity.settingsDialog.reloadResolutions()
725740
}
726741

727742
if (lensFacing == CameraSelector.LENS_FACING_FRONT) {
@@ -923,6 +938,27 @@ class CamConfig(private val mActivity: MainActivity) {
923938
}
924939
}
925940

941+
var captureResolution: Size?
942+
get() {
943+
val value = commonPref.getString(SettingValues.Key.CAPTURE_RESOLUTION, null)
944+
if (value.isNullOrEmpty()) return null
945+
return try {
946+
val parts = value.split("x")
947+
Size(parts[0].toInt(), parts[1].toInt())
948+
} catch (e: Exception) {
949+
null
950+
}
951+
}
952+
set(value) {
953+
commonPref.edit {
954+
if (value == null) {
955+
remove(SettingValues.Key.CAPTURE_RESOLUTION)
956+
} else {
957+
putString(SettingValues.Key.CAPTURE_RESOLUTION, "${value.width}x${value.height}")
958+
}
959+
}
960+
}
961+
926962
var selectHighestResolution: Boolean
927963
get() {
928964
return commonPref.getBoolean(
@@ -989,13 +1025,38 @@ class CamConfig(private val mActivity: MainActivity) {
9891025
} else {
9901026
AspectRatio.RATIO_16_9
9911027
}
1028+
// Clear capture resolution since available resolutions depend on aspect ratio
1029+
captureResolution = null
9921030
startCamera(true)
9931031
}
9941032

9951033
private fun getCurrentCameraInfo() : CameraInfo {
9961034
return cameraProvider!!.getCameraInfo(cameraSelector)
9971035
}
9981036

1037+
@SuppressLint("UnsafeOptInUsageError")
1038+
fun getAvailableImageResolutions(): List<Size> {
1039+
val cameraInfo = camera?.cameraInfo ?: return emptyList()
1040+
val camera2Info = Camera2CameraInfo.from(cameraInfo)
1041+
val characteristics = camera2Info.getCameraCharacteristic(
1042+
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
1043+
) ?: return emptyList()
1044+
1045+
val sizes = characteristics.getOutputSizes(ImageFormat.JPEG) ?: return emptyList()
1046+
1047+
// Filter by current aspect ratio with 2% tolerance
1048+
val targetRatio = when (aspectRatio) {
1049+
AspectRatio.RATIO_16_9 -> 16.0 / 9.0
1050+
AspectRatio.RATIO_4_3 -> 4.0 / 3.0
1051+
else -> 4.0 / 3.0
1052+
}
1053+
1054+
return sizes.filter { size ->
1055+
val ratio = size.width.toDouble() / size.height.toDouble()
1056+
kotlin.math.abs(ratio - targetRatio) / targetRatio < 0.02
1057+
}.sortedByDescending { it.width * it.height }
1058+
}
1059+
9991060
fun toggleCameraSelector() {
10001061

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

1293+
captureResolution?.let { size ->
1294+
resolutionSelectorBuilder.setResolutionStrategy(
1295+
ResolutionStrategy(size, ResolutionStrategy.FALLBACK_RULE_NONE)
1296+
)
1297+
}
1298+
12321299
it.setResolutionSelector(resolutionSelectorBuilder.build())
12331300

12341301
it.setFlashMode(flashMode)

app/src/main/java/app/grapheneos/camera/capturer/ImageSaver.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package app.grapheneos.camera.capturer
33
import android.annotation.SuppressLint
44
import android.content.ContentValues
55
import android.content.Context
6+
import android.graphics.Bitmap
67
import android.graphics.ImageDecoder
78
import android.graphics.ImageFormat
89
import android.graphics.Rect
@@ -38,7 +39,6 @@ import java.text.SimpleDateFormat
3839
import java.util.Date
3940
import java.util.Locale
4041
import java.util.concurrent.Executors
41-
import java.util.concurrent.atomic.AtomicBoolean
4242

4343
// see com.android.externalstorage.ExternalStorageProvider and
4444
// com.android.internal.content.FileSystemProvider

app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
6565
private lateinit var vQAdapter: ArrayAdapter<String>
6666
private var focusTimeoutSpinner: Spinner
6767
private var timerSpinner: Spinner
68+
private var captureResolutionSpinner: Spinner
69+
private lateinit var captureResolutionAdapter: ArrayAdapter<String>
70+
private var availableResolutions: List<android.util.Size> = emptyList()
6871

6972
var mScrollView: ScrollView
7073
var mScrollViewContent: View
@@ -83,6 +86,7 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
8386
private var selfIlluminationSetting: View
8487
private var videoQualitySetting: View
8588
private var timerSetting: View
89+
private var captureResolutionSetting: View
8690

8791
var settingsFrame: View
8892

@@ -321,6 +325,22 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
321325
override fun onNothingSelected(p0: AdapterView<*>?) {}
322326
}
323327

328+
captureResolutionSpinner = binding.longestEdgeLimitSpinner
329+
captureResolutionSpinner.onItemSelectedListener =
330+
object : AdapterView.OnItemSelectedListener {
331+
override fun onItemSelected(
332+
p0: AdapterView<*>?,
333+
p1: View?,
334+
position: Int,
335+
p3: Long
336+
) {
337+
camConfig.captureResolution = indexToResolution(position)
338+
camConfig.startCamera(true)
339+
}
340+
341+
override fun onNothingSelected(p0: AdapterView<*>?) {}
342+
}
343+
324344
mScrollView = binding.settingsScrollview
325345
mScrollViewContent = binding.settingsScrollviewContent
326346

@@ -329,6 +349,7 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
329349
selfIlluminationSetting = binding.selfIlluminationSetting
330350
videoQualitySetting = binding.videoQualitySetting
331351
timerSetting = binding.timerSetting
352+
captureResolutionSetting = binding.longestEdgeLimitSetting
332353

333354
includeAudioToggle = binding.includeAudioSwitch
334355
includeAudioToggle.setOnCheckedChangeListener { _, _ ->
@@ -466,8 +487,48 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
466487
} else {
467488
View.VISIBLE
468489
}
490+
491+
captureResolutionSetting.visibility = if (camConfig.isVideoMode) {
492+
View.GONE
493+
} else {
494+
View.VISIBLE
495+
}
469496
}
470497

498+
private fun resolutionToIndex(size: android.util.Size?): Int {
499+
if (size == null) return 0 // Highest resolution
500+
return availableResolutions.indexOfFirst { it.width == size.width && it.height == size.height }
501+
}
502+
503+
private fun indexToResolution(index: Int): android.util.Size? {
504+
return if (index >= 0 && index < availableResolutions.size) {
505+
availableResolutions[index]
506+
} else {
507+
null
508+
}
509+
}
510+
511+
fun reloadResolutions() {
512+
availableResolutions = camConfig.getAvailableImageResolutions()
513+
514+
val titles = mutableListOf<String>()
515+
availableResolutions.forEach { size ->
516+
titles.add("${size.width}x${size.height}")
517+
}
518+
519+
captureResolutionAdapter = ArrayAdapter<String>(
520+
mActivity,
521+
android.R.layout.simple_spinner_item,
522+
titles
523+
)
524+
525+
captureResolutionAdapter.setDropDownViewResource(
526+
android.R.layout.simple_spinner_dropdown_item
527+
)
528+
529+
captureResolutionSpinner.adapter = captureResolutionAdapter
530+
captureResolutionSpinner.setSelection(resolutionToIndex(camConfig.captureResolution))
531+
}
471532

472533
fun updateFocusTimeout(selectedOption: String) {
473534

app/src/main/res/layout/settings.xml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,40 @@
382382
</FrameLayout>
383383
</LinearLayout>
384384

385+
<LinearLayout
386+
android:id="@+id/longest_edge_limit_setting"
387+
android:layout_width="match_parent"
388+
android:layout_height="@dimen/settings_dialog_menu_item_height"
389+
android:paddingVertical="@dimen/settings_dialog_menu_item_vertical"
390+
android:paddingHorizontal="@dimen/settings_dialog_menu_item_horizontal"
391+
android:layout_gravity="end"
392+
android:gravity="center_vertical"
393+
android:orientation="horizontal">
394+
395+
<TextView
396+
android:layout_height="wrap_content"
397+
android:layout_width="wrap_content"
398+
android:layout_gravity="center_vertical"
399+
android:text="@string/capture_resolution"/>
400+
401+
<FrameLayout
402+
android:layout_height="wrap_content"
403+
android:padding="0dp"
404+
android:layout_margin="0dp"
405+
android:layout_width="match_parent">
406+
407+
<Spinner
408+
android:id="@+id/longest_edge_limit_spinner"
409+
android:layout_width="wrap_content"
410+
android:layout_height="wrap_content"
411+
android:checked="true"
412+
android:padding="0dp"
413+
android:layout_margin="0dp"
414+
android:layout_gravity="end"/>
415+
416+
</FrameLayout>
417+
</LinearLayout>
418+
385419
<!-- Extra padding for the bottom of the list -->
386420
<View
387421
android:layout_width="match_parent"

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
<string name="self_illumination">Self Illumination</string>
4444
<string name="focus_timeout">Focus Timeout</string>
4545
<string name="timer">Timer</string>
46+
<string name="capture_resolution">Capture Resolution</string>
4647
<string name="cancel_timer">Cancel Timer</string>
4748
<string name="video_capture_label">Record Video</string>
4849

0 commit comments

Comments
 (0)