diff --git a/.gitignore b/.gitignore index 6ca2a82..83dd92d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ *.iml *.jks *.aab +*.aar *.base64 *.json +*/libs .gradle /local.properties .idea diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2a054fa..50fe2f5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { alias(libs.plugins.gradle.ktlint) } -val properties = Properties() +val properties: Properties = Properties() val propertiesFile = rootProject.file("local.properties") if (propertiesFile.exists()) { @@ -116,6 +116,7 @@ android { } dependencies { + // implementation(fileTree("dir" to "./libs", "include" to arrayOf("*.aar"))) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/app/src/main/java/com/infbyte/amuzeo/models/AmuzeoSideEffect.kt b/app/src/main/java/com/infbyte/amuzeo/models/AmuzeoSideEffect.kt index b866954..86d4c07 100644 --- a/app/src/main/java/com/infbyte/amuzeo/models/AmuzeoSideEffect.kt +++ b/app/src/main/java/com/infbyte/amuzeo/models/AmuzeoSideEffect.kt @@ -2,4 +2,5 @@ package com.infbyte.amuzeo.models data class AmuzeoSideEffect( val showSplash: Boolean = true, + val showAppSettingsDialog: Boolean = false, ) diff --git a/app/src/main/java/com/infbyte/amuzeo/presentation/ui/activities/MainActivity.kt b/app/src/main/java/com/infbyte/amuzeo/presentation/ui/activities/MainActivity.kt index afdfc56..84c285c 100644 --- a/app/src/main/java/com/infbyte/amuzeo/presentation/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/infbyte/amuzeo/presentation/ui/activities/MainActivity.kt @@ -25,6 +25,8 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.google.android.gms.ads.MobileAds import com.infbyte.amuze.ads.GoogleMobileAdsConsentManager +import com.infbyte.amuze.contracts.AppSettingsContract +import com.infbyte.amuze.ui.dialogs.AppSettingsRedirectDialog import com.infbyte.amuze.ui.screens.AboutScreen import com.infbyte.amuze.ui.screens.LoadingScreen import com.infbyte.amuze.ui.screens.NoMediaAvailableScreen @@ -38,6 +40,7 @@ import com.infbyte.amuzeo.presentation.ui.screens.VideoScreen import com.infbyte.amuzeo.presentation.ui.screens.VideosScreen import com.infbyte.amuzeo.presentation.viewmodels.VideosViewModel import com.infbyte.amuzeo.utils.AmuzeoPermissions.isReadPermissionGranted +import com.infbyte.amuzeo.utils.AmuzeoPermissions.showReqPermRationale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel @@ -60,6 +63,14 @@ class MainActivity : ComponentActivity() { } } + private val appSettingsLauncher = + registerForActivityResult(AppSettingsContract()) { + videosViewModel.setReadPermGranted(it) + if (it) { + videosViewModel.init(this) + } + } + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) @@ -104,6 +115,17 @@ class MainActivity : ComponentActivity() { return@Surface } + if (videosViewModel.sideEffect.showAppSettingsDialog) { + AppSettingsRedirectDialog( + stringResource(R.string.amuzeo_perm_rationale), + onAccept = { + videosViewModel.hideAppSettingsRedirectDialog() + appSettingsLauncher.launch(packageName) + }, + onDismiss = { videosViewModel.hideAppSettingsRedirectDialog() }, + ) + } + initialScreen = when { !videosViewModel.state.isReadPermGranted -> { @@ -143,7 +165,13 @@ class MainActivity : ComponentActivity() { NoMediaPermissionScreen( appIcon = R.drawable.amuzeo_intro, action = R.string.amuzeo_watch, - onStartAction = { launchPermRequest() }, + onStartAction = { + if (!showReqPermRationale()) { + videosViewModel.showAppSettingsRedirectDialog() + return@NoMediaPermissionScreen + } + launchPermRequest() + }, onExit = { onExit() }, aboutApp = { navController.navigate(Screens.ABOUT) }, ) diff --git a/app/src/main/java/com/infbyte/amuzeo/presentation/viewmodels/VideosViewModel.kt b/app/src/main/java/com/infbyte/amuzeo/presentation/viewmodels/VideosViewModel.kt index 9a46065..5112609 100644 --- a/app/src/main/java/com/infbyte/amuzeo/presentation/viewmodels/VideosViewModel.kt +++ b/app/src/main/java/com/infbyte/amuzeo/presentation/viewmodels/VideosViewModel.kt @@ -297,4 +297,12 @@ class VideosViewModel( state = state.copy(searchQuery = query) } } + + fun showAppSettingsRedirectDialog() { + sideEffect = sideEffect.copy(showAppSettingsDialog = true) + } + + fun hideAppSettingsRedirectDialog() { + sideEffect = sideEffect.copy(showAppSettingsDialog = false) + } } diff --git a/app/src/main/java/com/infbyte/amuzeo/repo/VideosRepo.kt b/app/src/main/java/com/infbyte/amuzeo/repo/VideosRepo.kt index 7bd695d..768b4d0 100644 --- a/app/src/main/java/com/infbyte/amuzeo/repo/VideosRepo.kt +++ b/app/src/main/java/com/infbyte/amuzeo/repo/VideosRepo.kt @@ -99,17 +99,21 @@ class VideosRepo(private val context: Context) { val fileId = videoPath.fileId() - contentIds.add(fileId) - - _videos += - Video( - item = item, - folder = extractFolderName(path), - fileId = fileId, - thumbnail = context.createVideoThumbnail(Path(path), Size(640, 480)), - ) + val thumbnail = context.createVideoThumbnail(videoUri, Size(640, 480)) + + if (thumbnail != null) { + contentIds.add(fileId) + + _videos += + Video( + item = item, + folder = extractFolderName(path), + fileId = fileId, + thumbnail = thumbnail, + ) - _folderPaths.add(extractFolderPath(path)) + _folderPaths.add(extractFolderPath(path)) + } } query.close() } diff --git a/app/src/main/java/com/infbyte/amuzeo/utils/AmuzeoPermissions.kt b/app/src/main/java/com/infbyte/amuzeo/utils/AmuzeoPermissions.kt index c87a275..0595b0d 100644 --- a/app/src/main/java/com/infbyte/amuzeo/utils/AmuzeoPermissions.kt +++ b/app/src/main/java/com/infbyte/amuzeo/utils/AmuzeoPermissions.kt @@ -1,6 +1,7 @@ package com.infbyte.amuzeo.utils import android.Manifest +import android.app.Activity import android.content.Context import android.content.pm.PackageManager import android.os.Build @@ -17,4 +18,13 @@ object AmuzeoPermissions { .checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED } + + fun Activity.showReqPermRationale(): Boolean = + shouldShowRequestPermissionRationale( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_VIDEO + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + }, + ) } diff --git a/app/src/main/java/com/infbyte/amuzeo/utils/VideoUtils.kt b/app/src/main/java/com/infbyte/amuzeo/utils/VideoUtils.kt index 2409bc6..57a06f8 100644 --- a/app/src/main/java/com/infbyte/amuzeo/utils/VideoUtils.kt +++ b/app/src/main/java/com/infbyte/amuzeo/utils/VideoUtils.kt @@ -5,13 +5,12 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever.OPTION_PREVIOUS_SYNC -import android.media.ThumbnailUtils import android.net.Uri import android.os.Build +import android.util.Log import android.util.Size import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap -import java.nio.file.Path import kotlin.math.max typealias VideoDuration = Long @@ -25,68 +24,58 @@ fun Context.getVideoDuration(uri: Uri?): VideoDuration { return duration } -fun Context.createVideoThumbnail( - path: Path, - size: Size, -): ImageBitmap { - val file = path.toFile() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return ThumbnailUtils.createVideoThumbnail( - file, - size, - null, - ).asImageBitmap() - } - return createVideoThumbnail(Uri.fromFile(file), size) -} - fun Context.createVideoThumbnail( uri: Uri, size: Size, -): ImageBitmap { - val metaRetriever = MediaMetadataRetriever() - metaRetriever.setDataSource(this, uri) - val thumbnailsBytes = metaRetriever.embeddedPicture +): ImageBitmap? { + return try { + val metaRetriever = MediaMetadataRetriever() + metaRetriever.setDataSource(this, uri) + val thumbnailsBytes = metaRetriever.embeddedPicture - if (thumbnailsBytes != null) { - return BitmapFactory.decodeByteArray(thumbnailsBytes, 0, thumbnailsBytes.size).asImageBitmap() - } + if (thumbnailsBytes != null) { + return BitmapFactory.decodeByteArray(thumbnailsBytes, 0, thumbnailsBytes.size).asImageBitmap() + } - val width = - metaRetriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH, - )?.toFloat() ?: size.width.toFloat() - val height = - metaRetriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT, - )?.toFloat() ?: size.height.toFloat() + val width = + metaRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH, + )?.toFloat() ?: size.width.toFloat() + val height = + metaRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT, + )?.toFloat() ?: size.height.toFloat() - val widthRatio = size.width.toFloat() / width - val heightRatio = size.height.toFloat() / height + val widthRatio = size.width.toFloat() / width + val heightRatio = size.height.toFloat() / height - val ratio = max(widthRatio, heightRatio) + val ratio = max(widthRatio, heightRatio) - if (ratio > 1) { - val reqWidth = width * ratio - val reqHeight = height * ratio + if (ratio > 1) { + val reqWidth = width * ratio + val reqHeight = height * ratio - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val frame = - metaRetriever.getScaledFrameAtTime( - -1, - OPTION_PREVIOUS_SYNC, - reqWidth.toInt(), - reqHeight.toInt(), - ) - metaRetriever.release() - return frame?.asImageBitmap()!! + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val frame = + metaRetriever.getScaledFrameAtTime( + -1, + OPTION_PREVIOUS_SYNC, + reqWidth.toInt(), + reqHeight.toInt(), + ) + metaRetriever.release() + return frame?.asImageBitmap()!! + } } - } - // Should be scaled according to requested size - val frame = metaRetriever.frameAtTime - metaRetriever.release() - return frame?.asImageBitmap()!! + // Should be scaled according to requested size + val frame = metaRetriever.frameAtTime + metaRetriever.release() + frame?.asImageBitmap() + } catch (e: Exception) { + Log.e("Video Thumbnail", "Failed to create thumbnail with exception: $e") + null + } } fun VideoDuration.format(): String { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08d8065..0833044 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,4 +17,5 @@ Apply Cancel Preparing your videos... + To play video, Amuzeo requires Video permisson.