diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml new file mode 100644 index 0000000..113d138 --- /dev/null +++ b/.github/workflows/build-apk.yml @@ -0,0 +1,63 @@ +name: build-apk + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: Build IR apk + runs-on: ubuntu-latest + needs: [unit-test] + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + cache: 'gradle' + + - name: Load Google Service file + env: + DATA: ${{ secrets.GOOGLE_SERVICES_JSON }} + run: echo $DATA | base64 -di > app/google-services.json + + - name: Setup Local properties + env: + STORE_FILE: ${{ secrets.STORE_FILE }} + IR_STORE_FILE: ${{ secrets.IR_STORE_FILE }} + run: mkdir keys && + echo $STORE_FILE | base64 -di > keys/store_key.jks && + echo $IR_STORE_FILE | base64 -di > keys/ir_store_key.jks && + echo $'STORE_FILE=${{ github.workspace }}/keys/store_key.jks\n + STORE_PASSWORD=${{ secrets.STORE_PASSWORD }}\n + KEY_ALIAS=${{ secrets.KEY_ALIAS }}\n + KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}\n + IR_STORE_FILE=${{ github.workspace }}/keys/ir_store_key.jks\n + IR_STORE_PASSWORD=${{ secrets.IR_STORE_PASSWORD }}\n + IR_KEY_ALIAS=${{ secrets.IR_KEY_ALIAS }}\n + IR_KEY_PASSWORD=${{ secrets.IR_KEY_PASSWORD }}\n' > ./local.properties + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build apk + run: ./gradlew assembleInternalRelease + + - name: Rename apk + run: mv ./app/build/outputs/apk/internalRelease/app-internalRelease.apk ./app/build/outputs/apk/internalRelease/zen-music-ir-build-${{ github.run_number }}.apk + + - name: Upload apk + uses: actions/upload-artifact@v4 + with: + name: zen-music-ir-build-${{ github.run_number }}.apk + path: app/build/outputs/apk/internalRelease/ diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 0000000..d146357 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,52 @@ +name: unit-test + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +jobs: + test: + name: Perform Unit Testing + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + cache: 'gradle' + + - name: Load Google Service file + env: + DATA: ${{ secrets.GOOGLE_SERVICES_JSON }} + run: echo $DATA | base64 -di > app/google-services.json + + - name: Setup Local properties + env: + STORE_FILE: ${{ secrets.STORE_FILE }} + IR_STORE_FILE: ${{ secrets.IR_STORE_FILE }} + run: mkdir keys && + echo $STORE_FILE | base64 -di > keys/store_key.jks && + echo $IR_STORE_FILE | base64 -di > keys/ir_store_key.jks && + echo $'STORE_FILE=${{ github.workspace }}/keys/store_key.jks\n + STORE_PASSWORD=${{ secrets.STORE_PASSWORD }}\n + KEY_ALIAS=${{ secrets.KEY_ALIAS }}\n + KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}\n + IR_STORE_FILE=${{ github.workspace }}/keys/ir_store_key.jks\n + IR_STORE_PASSWORD=${{ secrets.IR_STORE_PASSWORD }}\n + IR_KEY_ALIAS=${{ secrets.IR_KEY_ALIAS }}\n + IR_KEY_PASSWORD=${{ secrets.IR_KEY_PASSWORD }}\n' > ./local.properties + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run unit tests + run: ./gradlew testDebug \ No newline at end of file diff --git a/app/src/main/java/com/github/pakka_papad/data/music/SongExtractor.kt b/app/src/main/java/com/github/pakka_papad/data/music/SongExtractor.kt index 9342258..7bd9824 100644 --- a/app/src/main/java/com/github/pakka_papad/data/music/SongExtractor.kt +++ b/app/src/main/java/com/github/pakka_papad/data/music/SongExtractor.kt @@ -1,11 +1,14 @@ package com.github.pakka_papad.data.music +import android.Manifest import android.content.ContentUris import android.content.Context +import android.content.pm.PackageManager import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Build import android.provider.MediaStore +import androidx.core.content.ContextCompat import com.github.pakka_papad.data.ZenCrashReporter import com.github.pakka_papad.data.daos.AlbumArtistDao import com.github.pakka_papad.data.daos.AlbumDao @@ -78,6 +81,17 @@ class SongExtractor( } } + private fun checkReadStoragePermission(): Boolean { + return ContextCompat.checkSelfPermission( + context, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_AUDIO + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + ) == PackageManager.PERMISSION_GRANTED + } + private val projection = arrayOf( MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, @@ -90,6 +104,7 @@ class SongExtractor( ) fun resolveSong(location: String): Song? { + if (!checkReadStoragePermission()) return null val selection = MediaStore.Audio.Media.DATA + " LIKE ?" val selectionArgs = arrayOf(location) val cursor = context.contentResolver.query( @@ -136,6 +151,7 @@ class SongExtractor( } suspend fun extract(folderPath: String? = null): List { + if (!checkReadStoragePermission()) return emptyList() val selection = MediaStore.Audio.Media.DATA + " LIKE ?" val selectionArgs = folderPath?.let { arrayOf("$it%") @@ -190,6 +206,7 @@ class SongExtractor( } fun extractMini(folderPath: String? = null): List { + if (!checkReadStoragePermission()) return emptyList() val projectionForMini = arrayOf( MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, @@ -240,6 +257,7 @@ class SongExtractor( val scanStatus = _scanStatus.receiveAsFlow() fun scanForMusic() { + if (!checkReadStoragePermission()) return scope.launch { _scanStatus.send(ScanStatus.ScanStarted) val blacklistedSongLocations = blacklistDao diff --git a/app/src/main/java/com/github/pakka_papad/home/HomeFragment.kt b/app/src/main/java/com/github/pakka_papad/home/HomeFragment.kt index 2965997..a0d7d2b 100644 --- a/app/src/main/java/com/github/pakka_papad/home/HomeFragment.kt +++ b/app/src/main/java/com/github/pakka_papad/home/HomeFragment.kt @@ -1,8 +1,12 @@ package com.github.pakka_papad.home +import android.Manifest import android.app.PendingIntent import android.content.Intent +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -34,8 +38,10 @@ import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetValue +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.material3.surfaceColorAtElevation @@ -64,6 +70,7 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.navigation.NavController import androidx.navigation.fragment.findNavController import com.github.pakka_papad.Constants +import com.github.pakka_papad.R import com.github.pakka_papad.Screens import com.github.pakka_papad.components.BottomSheet import com.github.pakka_papad.components.Snackbar @@ -74,6 +81,9 @@ import com.github.pakka_papad.nowplaying.PlayerHelper import com.github.pakka_papad.nowplaying.Queue import com.github.pakka_papad.player.ZenBroadcastReceiver import com.github.pakka_papad.ui.theme.ZenTheme +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.systemuicontroller.rememberSystemUiController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay @@ -93,7 +103,7 @@ class HomeFragment : Fragment() { @OptIn( ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class, - ExperimentalMaterialApi::class + ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class ) override fun onCreateView( inflater: LayoutInflater, @@ -188,6 +198,32 @@ class HomeFragment : Fragment() { snackbarHostState.showSnackbar(message) } + val readStoragePermissionState = + rememberPermissionState( + permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_AUDIO + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + ) + + LaunchedEffect(key1 = readStoragePermissionState) { + if (readStoragePermissionState.status.isGranted) return@LaunchedEffect + val snackbarResult = snackbarHostState.showSnackbar( + context.getString(R.string.grant_access_to_read_storage), + context.getString(R.string.settings), + true, + SnackbarDuration.Indefinite + ) + if (snackbarResult != SnackbarResult.ActionPerformed) return@LaunchedEffect + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null) + ).apply { + startActivity(this) + } + } + val bottomBarYOffset by remember { derivedStateOf { val progress = if (swipeableState.progress.from == 0) {