Skip to content

Commit 6856327

Browse files
committed
Separate data class of MediaItemDetails
1 parent d3bc2cd commit 6856327

3 files changed

Lines changed: 158 additions & 156 deletions

File tree

app/src/main/java/app/grapheneos/camera/ui/composable/components/MediaInfoDialog.kt

Lines changed: 2 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
package app.grapheneos.camera.ui.composable.components
22

3-
import android.annotation.SuppressLint
4-
import android.content.Context
5-
import android.media.MediaMetadataRetriever
6-
import android.net.Uri
7-
import android.provider.MediaStore.MediaColumns
8-
import android.provider.OpenableColumns
93
import androidx.compose.foundation.background
104
import androidx.compose.foundation.layout.Box
115
import androidx.compose.foundation.layout.Column
12-
import androidx.compose.foundation.layout.fillMaxHeight
6+
137
import androidx.compose.foundation.layout.padding
148
import androidx.compose.foundation.rememberScrollState
159
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -18,9 +12,6 @@ import androidx.compose.material3.ProvideTextStyle
1812
import androidx.compose.material3.Text
1913
import androidx.compose.material3.TextButton
2014
import androidx.compose.runtime.Composable
21-
import androidx.compose.runtime.MutableState
22-
import androidx.compose.runtime.mutableStateOf
23-
import androidx.compose.runtime.saveable.Saver
2415
import androidx.compose.ui.Alignment
2516
import androidx.compose.ui.Modifier
2617
import androidx.compose.ui.graphics.Color
@@ -30,153 +21,9 @@ import androidx.compose.ui.text.font.FontWeight
3021
import androidx.compose.ui.unit.dp
3122
import androidx.compose.ui.unit.sp
3223
import androidx.compose.ui.window.Dialog
33-
import androidxc.exifinterface.media.ExifInterface
34-
import app.grapheneos.camera.CapturedItem
35-
import app.grapheneos.camera.ITEM_TYPE_VIDEO
3624
import app.grapheneos.camera.R
25+
import app.grapheneos.camera.ui.composable.data.MediaItemDetails
3726
import app.grapheneos.camera.ui.theme.AppColor
38-
import app.grapheneos.camera.util.storageLocationToUiString
39-
import java.text.SimpleDateFormat
40-
import java.util.Date
41-
import java.util.Locale
42-
import java.util.TimeZone
43-
44-
class MediaItemDetails private constructor(
45-
val filePath: String?,
46-
val fileName: String?,
47-
val size: String?,
48-
var dateAdded: String?,
49-
var dateModified: String?,
50-
) {
51-
52-
companion object {
53-
54-
55-
val Saver = Saver<MutableState<MediaItemDetails?>, Array<String?>>(
56-
save = {
57-
val item = it.value ?: return@Saver arrayOf()
58-
arrayOf(item.filePath, item.fileName, item.size, item.dateAdded, item.dateModified)
59-
},
60-
restore = {
61-
if (it.isEmpty()) return@Saver null
62-
mutableStateOf(MediaItemDetails(it[0], it[1], it[2], it[3], it[4]))
63-
},
64-
)
65-
66-
fun forCapturedItem(context: Context, item: CapturedItem) : MediaItemDetails {
67-
68-
var relativePath: String? = null
69-
var fileName: String? = null
70-
var size: String? = null
71-
72-
var dateAdded: String? = null
73-
var dateModified: String? = null
74-
75-
val projection = arrayOf(MediaColumns.RELATIVE_PATH, OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
76-
77-
context.contentResolver.query(item.uri, projection, null,null)?.use {
78-
if (it.moveToFirst()) {
79-
fileName = it.getString(1)
80-
81-
if (fileName == null) {
82-
throw Exception("File name not found for file")
83-
}
84-
85-
relativePath = getRelativePath(context, item.uri, it.getString(0), fileName!!)
86-
size = String.format(Locale.ROOT, "%.2f MB", (it.getLong(2) / (1000f * 1000f)))
87-
}
88-
}
89-
90-
if (item.type == ITEM_TYPE_VIDEO) {
91-
MediaMetadataRetriever().use {
92-
it.setDataSource(context, item.uri)
93-
dateAdded = convertTimeForVideo(it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)!!)
94-
dateModified = dateAdded
95-
96-
}
97-
} else {
98-
context.contentResolver.openInputStream(item.uri)?.use { stream ->
99-
val eInterface = ExifInterface(stream)
100-
101-
val offset = eInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME)
102-
103-
if (eInterface.hasAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)) {
104-
dateAdded = convertTimeForPhoto(
105-
eInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)!!,
106-
offset
107-
)
108-
}
109-
110-
if (eInterface.hasAttribute(ExifInterface.TAG_DATETIME)) {
111-
dateModified = convertTimeForPhoto(
112-
eInterface.getAttribute(ExifInterface.TAG_DATETIME)!!,
113-
offset
114-
)
115-
}
116-
}
117-
}
118-
119-
return MediaItemDetails(
120-
relativePath,
121-
fileName,
122-
size,
123-
dateAdded,
124-
dateModified,
125-
)
126-
}
127-
128-
@SuppressLint("SimpleDateFormat")
129-
fun convertTime(time: Long, showTimeZone: Boolean = true): String {
130-
val date = Date(time)
131-
val format = SimpleDateFormat(
132-
if (showTimeZone) {
133-
"yyyy-MM-dd HH:mm:ss z"
134-
} else {
135-
"yyyy-MM-dd HH:mm:ss"
136-
}
137-
)
138-
format.timeZone = TimeZone.getDefault()
139-
return format.format(date)
140-
}
141-
142-
private fun convertTimeForVideo(time: String): String {
143-
val dateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS'Z'", Locale.US)
144-
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
145-
val parsedDate = dateFormat.parse(time)
146-
return convertTime(parsedDate?.time ?: 0)
147-
}
148-
149-
private fun convertTimeForPhoto(time: String, offset: String? = null): String {
150-
val timestamp = if (offset != null) {
151-
"$time $offset"
152-
} else {
153-
time
154-
}
155-
156-
val dateFormat = SimpleDateFormat(
157-
if (offset == null) {
158-
"yyyy:MM:dd HH:mm:ss"
159-
} else {
160-
"yyyy:MM:dd HH:mm:ss Z"
161-
}, Locale.US
162-
)
163-
164-
if (offset == null) {
165-
dateFormat.timeZone = TimeZone.getDefault()
166-
}
167-
val parsedDate = dateFormat.parse(timestamp)
168-
return convertTime(parsedDate?.time ?: 0, offset != null)
169-
}
170-
171-
private fun getRelativePath(ctx: Context, uri: Uri, path: String?, fileName: String): String {
172-
if (path == null) {
173-
return storageLocationToUiString(ctx, uri.toString())
174-
}
175-
176-
return "${ctx.getString(R.string.main_storage)}/$path$fileName"
177-
}
178-
}
179-
}
18027

18128
@Composable
18229
fun MediaInfoDialog(
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package app.grapheneos.camera.ui.composable.data
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.media.MediaMetadataRetriever
6+
import android.net.Uri
7+
import android.provider.MediaStore.MediaColumns
8+
import android.provider.OpenableColumns
9+
import androidx.compose.runtime.MutableState
10+
import androidx.compose.runtime.mutableStateOf
11+
import androidx.compose.runtime.saveable.Saver
12+
import androidxc.exifinterface.media.ExifInterface
13+
import app.grapheneos.camera.CapturedItem
14+
import app.grapheneos.camera.ITEM_TYPE_VIDEO
15+
import app.grapheneos.camera.R
16+
import app.grapheneos.camera.util.storageLocationToUiString
17+
import java.text.SimpleDateFormat
18+
import java.util.Date
19+
import java.util.Locale
20+
import java.util.TimeZone
21+
22+
data class MediaItemDetails(
23+
val filePath: String?,
24+
val fileName: String?,
25+
val size: String?,
26+
var dateAdded: String?,
27+
var dateModified: String?,
28+
) {
29+
30+
companion object {
31+
val Saver = Saver<MutableState<MediaItemDetails?>, Array<String?>>(
32+
save = {
33+
val item = it.value ?: return@Saver arrayOf()
34+
arrayOf(item.filePath, item.fileName, item.size, item.dateAdded, item.dateModified)
35+
},
36+
restore = {
37+
if (it.isEmpty()) return@Saver null
38+
mutableStateOf(MediaItemDetails(it[0], it[1], it[2], it[3], it[4]))
39+
},
40+
)
41+
42+
fun forCapturedItem(context: Context, item: CapturedItem) : MediaItemDetails {
43+
44+
var relativePath: String? = null
45+
var fileName: String? = null
46+
var size: String? = null
47+
48+
var dateAdded: String? = null
49+
var dateModified: String? = null
50+
51+
val projection = arrayOf(MediaColumns.RELATIVE_PATH, OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
52+
53+
context.contentResolver.query(item.uri, projection, null,null)?.use {
54+
if (it.moveToFirst()) {
55+
fileName = it.getString(1)
56+
57+
if (fileName == null) {
58+
throw Exception("File name not found for file")
59+
}
60+
61+
relativePath = getRelativePath(context, item.uri, it.getString(0), fileName!!)
62+
size = String.format(Locale.ROOT, "%.2f MB", (it.getLong(2) / (1000f * 1000f)))
63+
}
64+
}
65+
66+
if (item.type == ITEM_TYPE_VIDEO) {
67+
MediaMetadataRetriever().use {
68+
it.setDataSource(context, item.uri)
69+
dateAdded = convertTimeForVideo(it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)!!)
70+
dateModified = dateAdded
71+
72+
}
73+
} else {
74+
context.contentResolver.openInputStream(item.uri)?.use { stream ->
75+
val eInterface = ExifInterface(stream)
76+
77+
val offset = eInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME)
78+
79+
if (eInterface.hasAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)) {
80+
dateAdded = convertTimeForPhoto(
81+
eInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)!!,
82+
offset
83+
)
84+
}
85+
86+
if (eInterface.hasAttribute(ExifInterface.TAG_DATETIME)) {
87+
dateModified = convertTimeForPhoto(
88+
eInterface.getAttribute(ExifInterface.TAG_DATETIME)!!,
89+
offset
90+
)
91+
}
92+
}
93+
}
94+
95+
return MediaItemDetails(
96+
relativePath,
97+
fileName,
98+
size,
99+
dateAdded,
100+
dateModified,
101+
)
102+
}
103+
104+
@SuppressLint("SimpleDateFormat")
105+
fun convertTime(time: Long, showTimeZone: Boolean = true): String {
106+
val date = Date(time)
107+
val format = SimpleDateFormat(
108+
if (showTimeZone) {
109+
"yyyy-MM-dd HH:mm:ss z"
110+
} else {
111+
"yyyy-MM-dd HH:mm:ss"
112+
}
113+
)
114+
format.timeZone = TimeZone.getDefault()
115+
return format.format(date)
116+
}
117+
118+
private fun convertTimeForVideo(time: String): String {
119+
val dateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS'Z'", Locale.US)
120+
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
121+
val parsedDate = dateFormat.parse(time)
122+
return convertTime(parsedDate?.time ?: 0)
123+
}
124+
125+
private fun convertTimeForPhoto(time: String, offset: String? = null): String {
126+
val timestamp = if (offset != null) {
127+
"$time $offset"
128+
} else {
129+
time
130+
}
131+
132+
val dateFormat = SimpleDateFormat(
133+
if (offset == null) {
134+
"yyyy:MM:dd HH:mm:ss"
135+
} else {
136+
"yyyy:MM:dd HH:mm:ss Z"
137+
}, Locale.US
138+
)
139+
140+
if (offset == null) {
141+
dateFormat.timeZone = TimeZone.getDefault()
142+
}
143+
val parsedDate = dateFormat.parse(timestamp)
144+
return convertTime(parsedDate?.time ?: 0, offset != null)
145+
}
146+
147+
private fun getRelativePath(ctx: Context, uri: Uri, path: String?, fileName: String): String {
148+
if (path == null) {
149+
return storageLocationToUiString(ctx, uri.toString())
150+
}
151+
152+
return "${ctx.getString(R.string.main_storage)}/$path$fileName"
153+
}
154+
}
155+
}

app/src/main/java/app/grapheneos/camera/ui/composable/screen/GalleryScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ import app.grapheneos.camera.ui.composable.components.FileDeletionDialog
4444
import app.grapheneos.camera.ui.composable.components.GalleryImage
4545
import app.grapheneos.camera.ui.composable.components.GalleryTopBar
4646
import app.grapheneos.camera.ui.composable.components.MediaInfoDialog
47-
import app.grapheneos.camera.ui.composable.components.MediaItemDetails
4847
import app.grapheneos.camera.ui.theme.AppColor
4948
import kotlinx.coroutines.launch
5049
import me.saket.telephoto.zoomable.rememberZoomableState
5150
import me.saket.telephoto.zoomable.zoomable
5251

5352
import app.grapheneos.camera.ITEM_TYPE_IMAGE
5453
import app.grapheneos.camera.ITEM_TYPE_VIDEO
54+
import app.grapheneos.camera.ui.composable.data.MediaItemDetails
5555

5656
private const val TAG = "GalleryScreen"
5757

0 commit comments

Comments
 (0)