Skip to content

Commit 7478bf0

Browse files
committed
Allow selecting desired capture resolution
1 parent f5e7db9 commit 7478bf0

5 files changed

Lines changed: 172 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
@@ -56,6 +59,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
5659
import com.google.zxing.BarcodeFormat
5760
import java.util.concurrent.ExecutionException
5861
import java.util.concurrent.Executors
62+
import kotlin.math.log
5963

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

120124
const val WAIT_FOR_FOCUS_LOCK = "wait_for_focus_lock"
121125

126+
const val CAPTURE_RESOLUTION = "capture_resolution"
127+
122128
// const val IMAGE_FILE_FORMAT = "image_quality"
123129
// const val VIDEO_FILE_FORMAT = "video_quality"
124130
}
@@ -179,6 +185,14 @@ class CamConfig(private val mActivity: MainActivity) {
179185

180186
const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_BACK
181187

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

723737
if (isVideoMode) {
724738
mActivity.settingsDialog.reloadQualities()
739+
} else {
740+
mActivity.settingsDialog.reloadResolutions()
725741
}
726742

727743
if (lensFacing == CameraSelector.LENS_FACING_FRONT) {
@@ -923,6 +939,27 @@ class CamConfig(private val mActivity: MainActivity) {
923939
}
924940
}
925941

942+
var captureResolution: Size?
943+
get() {
944+
val value = commonPref.getString(SettingValues.Key.CAPTURE_RESOLUTION, null)
945+
if (value.isNullOrEmpty()) return null
946+
return try {
947+
val parts = value.split("x")
948+
Size(parts[0].toInt(), parts[1].toInt())
949+
} catch (e: Exception) {
950+
null
951+
}
952+
}
953+
set(value) {
954+
commonPref.edit {
955+
if (value == null) {
956+
remove(SettingValues.Key.CAPTURE_RESOLUTION)
957+
} else {
958+
putString(SettingValues.Key.CAPTURE_RESOLUTION, "${value.width}x${value.height}")
959+
}
960+
}
961+
}
962+
926963
var selectHighestResolution: Boolean
927964
get() {
928965
return commonPref.getBoolean(
@@ -989,13 +1026,37 @@ class CamConfig(private val mActivity: MainActivity) {
9891026
} else {
9901027
AspectRatio.RATIO_16_9
9911028
}
1029+
// Clear capture resolution since available resolutions depend on aspect ratio
1030+
captureResolution = null
9921031
startCamera(true)
9931032
}
9941033

9951034
private fun getCurrentCameraInfo() : CameraInfo {
9961035
return cameraProvider!!.getCameraInfo(cameraSelector)
9971036
}
9981037

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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ 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()
71+
private var aspectRatioForResolutions: Int? = null;
6872

6973
var mScrollView: ScrollView
7074
var mScrollViewContent: View
@@ -83,6 +87,7 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
8387
private var selfIlluminationSetting: View
8488
private var videoQualitySetting: View
8589
private var timerSetting: View
90+
private var captureResolutionSetting: View
8691

8792
var settingsFrame: View
8893

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

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

@@ -329,6 +350,7 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
329350
selfIlluminationSetting = binding.selfIlluminationSetting
330351
videoQualitySetting = binding.videoQualitySetting
331352
timerSetting = binding.timerSetting
353+
captureResolutionSetting = binding.captureResolutionSetting
332354

333355
includeAudioToggle = binding.includeAudioSwitch
334356
includeAudioToggle.setOnCheckedChangeListener { _, _ ->
@@ -466,8 +488,55 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) :
466488
} else {
467489
View.VISIBLE
468490
}
491+
492+
captureResolutionSetting.visibility = if (camConfig.isVideoMode) {
493+
View.GONE
494+
} else {
495+
View.VISIBLE
496+
}
497+
}
498+
499+
private fun resolutionToIndex(size: android.util.Size?): Int {
500+
if (size == null) return 0 // Highest resolution
501+
return availableResolutions.indexOfFirst { it.width == size.width && it.height == size.height }
502+
}
503+
504+
private fun indexToResolution(index: Int): android.util.Size? {
505+
return if (index >= 0 && index < availableResolutions.size) {
506+
availableResolutions[index]
507+
} else {
508+
null
509+
}
469510
}
470511

512+
fun reloadResolutions() {
513+
if (aspectRatioForResolutions !== null && aspectRatioForResolutions?.equals(camConfig.aspectRatio) == true) {
514+
// Use cached data
515+
return;
516+
}
517+
518+
availableResolutions = camConfig.getAvailableImageResolutions()
519+
520+
val titles = mutableListOf<String>()
521+
availableResolutions.forEach { size ->
522+
titles.add("${size.width}x${size.height}")
523+
}
524+
525+
captureResolutionAdapter = ArrayAdapter<String>(
526+
mActivity,
527+
android.R.layout.simple_spinner_item,
528+
titles
529+
)
530+
531+
captureResolutionAdapter.setDropDownViewResource(
532+
android.R.layout.simple_spinner_dropdown_item
533+
)
534+
535+
captureResolutionSpinner.adapter = captureResolutionAdapter
536+
captureResolutionSpinner.setSelection(resolutionToIndex(camConfig.captureResolution))
537+
538+
aspectRatioForResolutions = camConfig.aspectRatio;
539+
}
471540

472541
fun updateFocusTimeout(selectedOption: String) {
473542

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/capture_resolution_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/capture_resolution_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)