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
12 changes: 12 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,18 @@ dependencies {
implementation(libs.coil.video)
implementation(libs.coil.network)

// CameraX
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.video)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extensions)

// Media3 ExoPlayer for video playback in camera preview
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)

// Networking and Data
// Networking
implementation(libs.retrofit)
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">

<uses-feature
android:name="android.hardware.camera"
android:required="false" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
Expand All @@ -20,6 +24,10 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />


<queries>
Expand Down Expand Up @@ -200,6 +208,13 @@
android:theme="@style/SaveAppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden" />

<activity
android:name=".features.media.camera.CameraActivity"
android:exported="false"
android:screenOrientation="portrait"
android:taskAffinity=""
android:theme="@style/SaveAppTheme.NoActionBar" />

<activity
android:name=".features.onboarding.SpaceSetupActivity"
android:exported="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module

val passcodeModule = module {

single {
AppConfig(
passcodeLength = 6,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import net.opendasharchive.openarchive.features.main.ui.SaveNavGraph
import net.opendasharchive.openarchive.features.media.AddMediaType
import net.opendasharchive.openarchive.features.media.MediaLaunchers
import net.opendasharchive.openarchive.features.media.Picker
import net.opendasharchive.openarchive.features.media.camera.CameraConfig
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber

Expand Down Expand Up @@ -150,8 +151,17 @@ class HomeActivity: FragmentActivity() {
// insert that logic here (e.g., check Prefs.addMediaHint).
when (mediaType) {
AddMediaType.CAMERA -> {
// Launch the camera using Picker.
Picker.takePhotoModern(this, mediaLaunchers.modernCameraLauncher)
// Use custom camera with photo and video support
val cameraConfig = CameraConfig(
allowVideoCapture = true,
allowPhotoCapture = true,
allowMultipleCapture = false, // Single capture for main screen
enablePreview = true,
showFlashToggle = true,
showGridToggle = true,
showCameraSwitch = true
)
Picker.launchCustomCamera(this, mediaLaunchers.customCameraLauncher, cameraConfig)
}
AddMediaType.GALLERY -> {
// Launch the gallery/image picker.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import net.opendasharchive.openarchive.features.media.MediaLaunchers
import net.opendasharchive.openarchive.features.media.Picker
import net.opendasharchive.openarchive.features.media.PreviewActivity
import net.opendasharchive.openarchive.features.media.camera.CameraConfig
import net.opendasharchive.openarchive.features.onboarding.Onboarding23Activity
import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity
import net.opendasharchive.openarchive.features.onboarding.StartDestination
Expand Down Expand Up @@ -840,10 +841,17 @@
if (Prefs.addMediaHint) {
when (mediaType) {
AddMediaType.CAMERA -> {
//permissionManager.checkCameraPermission {
//Picker.takePhoto(this@MainActivity, mediaLaunchers.cameraLauncher)
Picker.takePhotoModern(this@MainActivity, mediaLaunchers.modernCameraLauncher)
//}
// Use custom camera instead of system camera
val cameraConfig = CameraConfig(
allowVideoCapture = true,
allowPhotoCapture = true,
allowMultipleCapture = false,
enablePreview = true,
showFlashToggle = true,
showGridToggle = true,
showCameraSwitch = true
)
Picker.launchCustomCamera(this@MainActivity, mediaLaunchers.customCameraLauncher, cameraConfig)

Check warning

Code scanning / detekt

Reports lines with exceeded length Warning

Exceeded max line length (120)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Argument should be on a separate line (unless all arguments can fit a single line)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Argument should be on a separate line (unless all arguments can fit a single line)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Argument should be on a separate line (unless all arguments can fit a single line)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Missing newline before ")"
}

AddMediaType.GALLERY -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ data class MediaLaunchers(
val galleryLauncher: ActivityResultLauncher<PickVisualMediaRequest>, // Changed
val filePickerLauncher: ActivityResultLauncher<Intent>,
val cameraLauncher: ActivityResultLauncher<Intent>,
val modernCameraLauncher: ActivityResultLauncher<Uri>
val modernCameraLauncher: ActivityResultLauncher<Uri>,
val customCameraLauncher: ActivityResultLauncher<Intent>
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import net.opendasharchive.openarchive.features.media.camera.CameraActivity
import net.opendasharchive.openarchive.features.media.camera.CameraConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.opendasharchive.openarchive.R
Expand Down Expand Up @@ -129,11 +131,48 @@
}
}

// Custom camera launcher
val customCameraLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val capturedUris = result.data?.getStringArrayListExtra(CameraActivity.EXTRA_CAPTURED_URIS)
if (!capturedUris.isNullOrEmpty()) {
val uris = capturedUris.map { Uri.parse(it) }
val snackbar = showProgressSnackBar(activity, root, activity.getString(R.string.importing_media))

activity.lifecycleScope.launch(Dispatchers.IO) {
try {
// Import the captured media with proof generation enabled
// This ensures proper mimetype detection and Media object setup
val media = import(activity, project(), uris, true)

activity.lifecycleScope.launch(Dispatchers.Main) {
snackbar.dismiss()
completed(media)
}
} catch (e: Exception) {

Check warning

Code scanning / detekt

The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled. Warning

The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.
AppLogger.e("Error processing camera captures", e)
activity.lifecycleScope.launch(Dispatchers.Main) {
snackbar.dismiss()
Toast.makeText(activity, "Failed to process captures", Toast.LENGTH_SHORT).show()
}
}
}
} else {
AppLogger.w("No captures returned from custom camera")
}
} else {
AppLogger.w("Custom camera capture cancelled or failed")
}
}

return MediaLaunchers(
galleryLauncher = galleryLauncher,
filePickerLauncher = filePickerLauncher,
cameraLauncher = legacyCameraLauncher,
modernCameraLauncher = modernCameraLauncher
modernCameraLauncher = modernCameraLauncher,
customCameraLauncher = customCameraLauncher
)
}

Expand All @@ -155,6 +194,15 @@
type = "application/*"
}

/**
* Launch custom camera with configuration options.
* Supports both photo and video capture with preview functionality.
*/
fun launchCustomCamera(activity: Activity, launcher: ActivityResultLauncher<Intent>, config: CameraConfig = CameraConfig()) {

Check warning

Code scanning / detekt

Reports lines with exceeded length Warning

Exceeded max line length (120)

Check warning

Code scanning / detekt

Detects mis-aligned parameter lists Warning

Parameter should start on a newline

Check warning

Code scanning / detekt

Detects mis-aligned parameter lists Warning

Parameter should start on a newline

Check warning

Code scanning / detekt

Detects mis-aligned parameter lists Warning

Parameter should start on a newline

Check warning

Code scanning / detekt

Detects mis-aligned parameter lists Warning

Missing newline before ")"
val intent = CameraActivity.createIntent(activity, config)
launcher.launch(intent)
}

/**
* Modern camera photo capture using TakePicture contract.
* This is the recommended approach for new implementations.
Expand Down Expand Up @@ -257,7 +305,8 @@
}

media.originalFilePath = Uri.fromFile(file).toString()
media.mimeType = Utility.getMimeType(context, uri) ?: ""
// Enhanced mime type detection for file URIs
media.mimeType = getMimeTypeWithFallback(context, uri, file?.path)
media.createDate = createDate
media.updateDate = media.createDate
media.sStatus = Media.Status.Local
Expand Down Expand Up @@ -315,4 +364,43 @@
bar.show()
return bar
}

Check warning

Code scanning / detekt

Detects trailing spaces Warning

Trailing space(s)
/**
* Enhanced mime type detection that falls back to file extension detection
* for file URIs where ContentResolver might not have mime type info.
*/
private fun getMimeTypeWithFallback(context: Context, uri: Uri, filePath: String?): String {

Check warning

Code scanning / detekt

Prefer splitting up complex methods into smaller, easier to test methods. Warning

The function getMimeTypeWithFallback appears to be too complex based on Cyclomatic Complexity (complexity: 19). Defined complexity threshold for methods is set to '15'
// First try the standard way
val standardMimeType = Utility.getMimeType(context, uri)
if (!standardMimeType.isNullOrEmpty()) {
return standardMimeType
}

Check warning

Code scanning / detekt

Detects trailing spaces Warning

Trailing space(s)
// Fallback to file extension detection
val extension = when {
filePath != null -> File(filePath).extension
uri.path != null -> File(uri.path!!).extension
else -> null
}

Check warning

Code scanning / detekt

Detects trailing spaces Warning

Trailing space(s)
return when (extension?.lowercase()) {
"jpg", "jpeg" -> "image/jpeg"
"png" -> "image/png"
"gif" -> "image/gif"
"webp" -> "image/webp"
"mp4" -> "video/mp4"
"mov" -> "video/quicktime"
"avi" -> "video/x-msvideo"
"mkv" -> "video/x-matroska"
"webm" -> "video/webm"
"mp3" -> "audio/mpeg"
"wav" -> "audio/wav"
"ogg" -> "audio/ogg"
"m4a" -> "audio/mp4"
else -> {
AppLogger.w("Unknown file extension '$extension' for URI: $uri")
"application/octet-stream" // Generic binary type
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import net.opendasharchive.openarchive.util.extensions.hide
import net.opendasharchive.openarchive.util.extensions.show
import net.opendasharchive.openarchive.util.extensions.toggle
import net.opendasharchive.openarchive.features.media.camera.CameraConfig

class PreviewActivity : BaseActivity(), View.OnClickListener, PreviewAdapter.Listener {

Expand Down Expand Up @@ -120,8 +121,17 @@
}

R.id.action_upload_camera -> {
//Picker.takePhoto(this@PreviewActivity, mediaLaunchers.cameraLauncher)
Picker.takePhotoModern(this@PreviewActivity, mediaLaunchers.modernCameraLauncher)
// Use custom camera with photo and video support
val cameraConfig = CameraConfig(
allowVideoCapture = true,
allowPhotoCapture = true,
allowMultipleCapture = false, // Allow adding multiple items
enablePreview = true,
showFlashToggle = true,
showGridToggle = true,
showCameraSwitch = true
)
Picker.launchCustomCamera(this@PreviewActivity, mediaLaunchers.customCameraLauncher, cameraConfig)

Check warning

Code scanning / detekt

Reports lines with exceeded length Warning

Exceeded max line length (120)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Argument should be on a separate line (unless all arguments can fit a single line)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Argument should be on a separate line (unless all arguments can fit a single line)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Argument should be on a separate line (unless all arguments can fit a single line)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Missing newline before ")"
}

R.id.action_upload_files -> {
Expand All @@ -145,10 +155,17 @@
val modalBottomSheet = ContentPickerFragment { action ->
when (action) {
AddMediaType.CAMERA -> {
// permissionManager.checkCameraPermission {
Picker.takePhotoModern(this@PreviewActivity, mediaLaunchers.modernCameraLauncher)
// }

// Use custom camera with photo and video support
val cameraConfig = CameraConfig(
allowVideoCapture = true,
allowPhotoCapture = true,
allowMultipleCapture = true, // Allow adding multiple items in preview
enablePreview = true,
showFlashToggle = true,
showGridToggle = true,
showCameraSwitch = true
)
Picker.launchCustomCamera(this@PreviewActivity, mediaLaunchers.customCameraLauncher, cameraConfig)

Check warning

Code scanning / detekt

Reports lines with exceeded length Warning

Exceeded max line length (120)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Argument should be on a separate line (unless all arguments can fit a single line)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Argument should be on a separate line (unless all arguments can fit a single line)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Argument should be on a separate line (unless all arguments can fit a single line)

Check warning

Code scanning / detekt

Reports incorrect argument list wrapping Warning

Missing newline before ")"
}
AddMediaType.FILES -> Picker.pickFiles(mediaLaunchers.filePickerLauncher)
AddMediaType.GALLERY -> onClick(mBinding.btAddMore)
Expand Down
Loading