From 9922157ae29c647cb9bdb5a5dba3da45c8399b3b Mon Sep 17 00:00:00 2001 From: WooJin Kong Date: Tue, 27 May 2025 00:02:16 +0900 Subject: [PATCH 1/2] assignment 09 sample --- app/src/main/AndroidManifest.xml | 5 + .../bcsd_android_2025_1/MainActivity.kt | 189 +++++++++++++++++- .../adapter/MusicAdapter.kt | 58 ++++++ .../bcsd_android_2025_1/model/MusicData.kt | 8 + .../bcsd_android_2025_1/utils/OpenSettings.kt | 20 ++ .../bcsd_android_2025_1/utils/Utils.kt | 62 ++++++ app/src/main/res/drawable/ic_no_album_art.xml | 10 + app/src/main/res/layout/activity_main.xml | 34 +++- app/src/main/res/layout/item_music.xml | 53 +++++ app/src/main/res/values/strings.xml | 7 + 10 files changed, 440 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/example/bcsd_android_2025_1/adapter/MusicAdapter.kt create mode 100644 app/src/main/java/com/example/bcsd_android_2025_1/model/MusicData.kt create mode 100644 app/src/main/java/com/example/bcsd_android_2025_1/utils/OpenSettings.kt create mode 100644 app/src/main/java/com/example/bcsd_android_2025_1/utils/Utils.kt create mode 100644 app/src/main/res/drawable/ic_no_album_art.xml create mode 100644 app/src/main/res/layout/item_music.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c80941..8ec36c9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,11 @@ + + + + when (isGranted) { + true -> getAudioFile() + else -> { + when ( + shouldShowRequestPermissionRationale( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) Manifest.permission.READ_MEDIA_AUDIO + else Manifest.permission.READ_EXTERNAL_STORAGE + ) + ) { + true -> permissionDialog(true) + else -> permissionDialog(false) + } + } + } + } + + private val openSettings = + registerForActivityResult(OpenSettings()) { + initView() + hidePermissionSettingsButton() + } + + private val dataList = mutableListOf() + private val musicAdapter = MusicAdapter() + private lateinit var recyclerViewMusicList: RecyclerView + private lateinit var textViewEmpty: TextView + private lateinit var textViewPermissionNeeded: TextView + private lateinit var buttonPermissionSettings: Button + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + recyclerViewMusicList = findViewById(R.id.recycler_view_music_list) + textViewEmpty = findViewById(R.id.text_view_empty) + textViewPermissionNeeded = findViewById(R.id.text_view_permission_needed) + buttonPermissionSettings = findViewById(R.id.button_permission_settings) + val buttonPermissionSettings: Button = findViewById(R.id.button_permission_settings) + + initView() + + buttonPermissionSettings.setOnClickListener { + /* + val intent = Intent() + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", packageName, null) + intent.setData(uri) + startActivity(intent) + */ + openSettings.launch(Unit) + } + } + + private fun checkPermission() { + requestPermission.launch( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) Manifest.permission.READ_MEDIA_AUDIO + else Manifest.permission.READ_EXTERNAL_STORAGE + ) + } + + private fun permissionDialog(isDeniedOnce: Boolean) { + when (isDeniedOnce) { + true -> { + val builder = AlertDialog.Builder(this) + builder.setTitle(getString(R.string.dialog_permission_title)) + .setMessage(getString(R.string.dialog_permission_messsage)) + .setPositiveButton(getString(R.string.dialog_permission_ok)) { _, _ -> + checkPermission() + } + .setNegativeButton(getString(R.string.dialog_permission_cancel)) { dialog, _ -> + dialog.dismiss() + showPermissionSettingsButton() + } + .setCancelable(false) + builder.show() + } + + else -> showPermissionSettingsButton() + } + } + + private fun showPermissionSettingsButton() { + recyclerViewMusicList.visibility = View.GONE + textViewEmpty.visibility = View.GONE + textViewPermissionNeeded.visibility = View.VISIBLE + buttonPermissionSettings.visibility = View.VISIBLE + } + + private fun hidePermissionSettingsButton() { + recyclerViewMusicList.visibility = View.VISIBLE + textViewPermissionNeeded.visibility = View.GONE + buttonPermissionSettings.visibility = View.GONE + } + + private fun getAudioFile() { + val projection = arrayOf( + MediaStore.Audio.Media._ID, + MediaStore.Audio.Media.ALBUM_ID, + MediaStore.Audio.Media.TITLE, + MediaStore.Audio.Media.ARTIST, + MediaStore.Audio.Media.DURATION + ) + + val sortOrder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + "${MediaStore.Files.FileColumns.ALBUM}, ${MediaStore.Files.FileColumns.ARTIST}, CAST(${MediaStore.Files.FileColumns.CD_TRACK_NUMBER} AS INTEGER)" + } else { + " ${MediaStore.Audio.AlbumColumns.ALBUM}, ${MediaStore.Audio.AlbumColumns.ARTIST}, CAST(${MediaStore.Audio.AudioColumns.TRACK} AS INTEGER)" + } + val cursor = this.contentResolver.query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + projection, + MediaStore.Audio.Media.IS_MUSIC, + null, + sortOrder + ) + + cursor?.use { + val idColumn = cursor.getColumnIndex(MediaStore.Audio.Media._ID) + val albumIdColumn = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID) + val titleColumn = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE) + val artistColumn = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST) + val durationColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn) + val albumId = cursor.getLong(albumIdColumn) + val title = cursor.getString(titleColumn) + val artist = cursor.getString(artistColumn) + val duration = cursor.getLong(durationColumn) + + val albumUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + "${MediaStore.Audio.Media.EXTERNAL_CONTENT_URI}/$id" + } else { + "content://media/external/audio/albumart/$albumId" + } + + dataList.add(MusicData(title, artist, duration, albumUri)) + musicAdapter.notifyItemInserted(musicAdapter.itemCount) + } + } + checkIsMusicEmpty() + } + + private fun checkIsMusicEmpty() { + if (musicAdapter.itemCount != 0) { + textViewEmpty.visibility = View.GONE + } else { + textViewEmpty.visibility = View.VISIBLE + } + } + + private fun initView() { + val dividerItemDecoration = DividerItemDecoration( + recyclerViewMusicList.context, + LinearLayoutManager(this).orientation + ) + + musicAdapter.dataList = dataList + + musicAdapter.setOnClickListener { + + } + + recyclerViewMusicList.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + adapter = musicAdapter + addItemDecoration(dividerItemDecoration) + } + + checkPermission() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/adapter/MusicAdapter.kt b/app/src/main/java/com/example/bcsd_android_2025_1/adapter/MusicAdapter.kt new file mode 100644 index 0000000..f91f212 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/adapter/MusicAdapter.kt @@ -0,0 +1,58 @@ +package com.example.bcsd_android_2025_1.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.net.toUri +import androidx.recyclerview.widget.RecyclerView +import com.example.bcsd_android_2025_1.R +import com.example.bcsd_android_2025_1.model.MusicData +import com.example.bcsd_android_2025_1.utils.getAlbumArt +import com.example.bcsd_android_2025_1.utils.toDuration + +class MusicAdapter : RecyclerView.Adapter() { + var dataList = mutableListOf() + lateinit var onClickListener: OnClickListener + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_music, parent, false)) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + with(holder) { + val item = dataList[position] + titleTextView.text = item.title + artistTextView.text = item.artist + durationTextView.text = item.duration.toDuration() + + val albumArt = item.albumUri.toUri().getAlbumArt(itemView.context, itemView.resources) + albumArtImage.setImageDrawable(albumArt) + + itemView.setOnClickListener { + onClickListener.onClick(item) + } + } + } + + override fun getItemCount(): Int = dataList.size + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val titleTextView: TextView = itemView.findViewById(R.id.title_text_view) + val artistTextView: TextView = itemView.findViewById(R.id.artist_text_view) + val durationTextView: TextView = itemView.findViewById(R.id.duration_text_view) + val albumArtImage: ImageView = itemView.findViewById(R.id.album_art_image) + } + + interface OnClickListener { + fun onClick(music: MusicData) + } + + inline fun setOnClickListener(crossinline item: (MusicData) -> Unit) { + this.onClickListener = object : OnClickListener { + override fun onClick(music: MusicData) { + item(music) + } + } + } +} diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/model/MusicData.kt b/app/src/main/java/com/example/bcsd_android_2025_1/model/MusicData.kt new file mode 100644 index 0000000..b1a955f --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/model/MusicData.kt @@ -0,0 +1,8 @@ +package com.example.bcsd_android_2025_1.model + +data class MusicData( + val title: String, + val artist: String, + val duration: Long, + val albumUri: String +) diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/utils/OpenSettings.kt b/app/src/main/java/com/example/bcsd_android_2025_1/utils/OpenSettings.kt new file mode 100644 index 0000000..5f82f74 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/utils/OpenSettings.kt @@ -0,0 +1,20 @@ +package com.example.bcsd_android_2025_1.utils + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.activity.result.contract.ActivityResultContract + +class OpenSettings : ActivityResultContract() { + override fun createIntent(context: Context, input: Unit): Intent { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri: Uri = Uri.fromParts("package", context.packageName, null) + intent.data = uri + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): Int { + return resultCode + } +} diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/utils/Utils.kt b/app/src/main/java/com/example/bcsd_android_2025_1/utils/Utils.kt new file mode 100644 index 0000000..6d14e37 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/utils/Utils.kt @@ -0,0 +1,62 @@ +package com.example.bcsd_android_2025_1.utils + +import android.content.Context +import android.content.res.Resources +import android.graphics.BitmapFactory +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.util.Size +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.toDrawable +import com.example.bcsd_android_2025_1.R +import java.io.FileNotFoundException +import java.io.InputStream +import java.util.Locale +import java.util.concurrent.TimeUnit + +fun Long.toDuration(): String { + val hours = TimeUnit.MILLISECONDS.toHours(this) + val minutes = + TimeUnit.MILLISECONDS.toMinutes(this) - TimeUnit.HOURS.toMinutes(hours) + val seconds = + TimeUnit.MILLISECONDS.toSeconds(this) - TimeUnit.MINUTES.toSeconds(minutes) + + val duration = if (hours.toInt() != 0) { + String.format(Locale.ROOT, "%02d:%02d:%02d", hours, minutes, seconds) + } else { + String.format(Locale.ROOT, "%02d:%02d", minutes, seconds) + } + return duration +} + +fun Uri.getAlbumArt( + context: Context, + resources: Resources, + width: Int = 500, + height: Int = 500 +): Drawable? { + var inputStream: InputStream? = null + val albumArt: Drawable? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + context.contentResolver.loadThumbnail(this, Size(500, 500), null) + .toDrawable(resources) + } catch (e: FileNotFoundException) { + ResourcesCompat.getDrawable(resources, R.drawable.ic_no_album_art, null) + } + } else { + try { + inputStream = context.contentResolver.openInputStream(this) + val option = BitmapFactory.Options() + option.outWidth = width + option.outHeight = height + option.inSampleSize = 2 + BitmapFactory.decodeStream(inputStream, null, option)?.toDrawable(resources) + } catch (e: FileNotFoundException) { + ResourcesCompat.getDrawable(resources, R.drawable.ic_no_album_art, null) + } + } + inputStream?.close() + + return albumArt +} diff --git a/app/src/main/res/drawable/ic_no_album_art.xml b/app/src/main/res/drawable/ic_no_album_art.xml new file mode 100644 index 0000000..f132710 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_album_art.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 311f3cb..44456b6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -2,18 +2,48 @@ + + + + +