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
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 권한입니다.


<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand Down
179 changes: 175 additions & 4 deletions app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,185 @@
package com.example.bcsd_android_2025_1

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.provider.MediaStore
import android.provider.Settings
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.io.File

data class MusicItem(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별도의 data class로 분리 바랍니다

val id: Long,
val title: String,
val artist: String,
val duration: String,
val albumId: Long,
val data: String
)

class MainActivity : AppCompatActivity() {

private lateinit var recyclerView: RecyclerView
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RecyclerView를 굳이 lateinit으로 선언할 이유가 없어 보입니다.

private lateinit var musicAdapter: MusicAdapter
private val musicList = mutableListOf<MusicItem>()


private val musicPermission: String
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_AUDIO
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}

private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
loadMusicFiles()
Toast.makeText(this, "allow", Toast.LENGTH_SHORT).show()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strings.xml 사용이 필요합니다.

} else {
showPermissionDeniedDialog()
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

initViews()
checkAndRequestPermission()
}

private fun initViews() {
recyclerView = findViewById(R.id.recycler_view_music)
musicAdapter = MusicAdapter(musicList)
recyclerView.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = musicAdapter
}
}

private fun checkAndRequestPermission() {
if (ContextCompat.checkSelfPermission(this, musicPermission) == PackageManager.PERMISSION_GRANTED) {
loadMusicFiles()
} else {
showPermissionRequestDialog()
}
}

private fun showPermissionRequestDialog() {
AlertDialog.Builder(this)
.setTitle("음악 및 오디오 접근 권한")
.setMessage("음악 파일을 불러오기 위해 권한이 필요합니다.")
.setPositiveButton("Allow") { _, _ ->
requestPermissionLauncher.launch(musicPermission)
}
.setNegativeButton("Don't allow") { _, _ ->
showPermissionDeniedDialog()
}
.setCancelable(false)
.show()
}

private fun showPermissionDeniedDialog() {
AlertDialog.Builder(this)
.setTitle("권한 필요!")
.setMessage("음악 파일을 표시하려면 권한이 필요합니다.\n\nAllow permission to show files")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strings.xml 써주세요.
기본적으로 사용자에게 보이는 메시지나 텍스트는 strings.xml을 사용하여 유지보수가 편리하게 만드는게 맞습니다.

.setPositiveButton("Open Settings") { _, _ ->
openAppSettings()
}
.setNegativeButton("Cancel") { _, _ ->
finish()
}
.setCancelable(false)
.show()
}

private fun openAppSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent)
}

private fun loadMusicFiles() {

val file = File("/sdcard/Music/Lil_Tecca_Dark_Thoughts.mp3")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped storage 도입 후, /sdcard에 직접 접근이 불가합니다.
따라서 이 코드는 동작하지 않습니다.

if (file.exists()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파일이 존재하는지 확인해서 미디어 스캐닝을 시도하려고 하신 것 같은데, 이렇게 되면 파일이 존재할 때 미디어 스캐닝이 동작합니다.
또한, 위에서 말한것 처럼, /sdcard에 대한 접근이 불가하기 때문에, 작동하지 않습니다.

미디어 스캐닝을 시도하시려면 미디어 스캐닝을 시도하는 버튼을 만드시는게 좋아보입니다.

미디어 스캐닝을 앱 실행 시 매번 실행하면 되는거 아니냐고 생각하실 수 있는데, 미디어 스캐닝 자체가 꽤 무거운 작업이라 필요할 때만 실행해야 합니다.

MediaScannerConnection.scanFile(this, arrayOf(file.absolutePath), null, null)
}

if (file.exists()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마찬가지입니다

val uri = Uri.fromFile(file)
sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri))
}

musicList.clear()

val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.DATA
)

val selection = "${MediaStore.Audio.Media.IS_MUSIC} = 1"
val sortOrder = "${MediaStore.Audio.Media.TITLE} ASC"

val cursor: Cursor? = contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
null,
sortOrder
)

cursor?.use {
val idColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val titleColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
val artistColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
val durationColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
val albumIdColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
val dataColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)

while (it.moveToNext()) {
val id = it.getLong(idColumn)
val title = it.getString(titleColumn) ?: "Unknown Title"
val artist = it.getString(artistColumn) ?: "Unknown Artist"
val duration = formatDuration(it.getLong(durationColumn))
val albumId = it.getLong(albumIdColumn)
val data = it.getString(dataColumn) ?: ""

musicList.add(MusicItem(id, title, artist, duration, albumId, data))
}
}

musicAdapter.notifyDataSetChanged()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notifyDataSetChanged의 경우, adapter 내의 데이터를 한꺼번에 업데이트 하는 코드입니다.
musicList.add 밑어 musicAdapter.notifyItemInserted(musicList.lastIndex)를 호출하는게 더 좋을 것 같습니다

}

private fun formatDuration(durationMs: Long): String {
val minutes = (durationMs / 1000) / 60
val seconds = (durationMs / 1000) % 60
return String.format("%02d:%02d", minutes, seconds)
}

override fun onResume() {
super.onResume()
if (ContextCompat.checkSelfPermission(this, musicPermission) == PackageManager.PERMISSION_GRANTED && musicList.isEmpty()) {
loadMusicFiles()
}
}
}
}
55 changes: 55 additions & 0 deletions app/src/main/java/com/example/bcsd_android_2025_1/MusicAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.example.bcsd_android_2025_1

import android.content.ContentUris
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions

class MusicAdapter(
private val musicList: List<MusicItem>
) : RecyclerView.Adapter<MusicAdapter.MusicViewHolder>() {

inner class MusicViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val albumArt: ImageView = itemView.findViewById(R.id.iv_album_art)
val title: TextView = itemView.findViewById(R.id.tv_title)
val artist: TextView = itemView.findViewById(R.id.tv_artist)
val duration: TextView = itemView.findViewById(R.id.tv_duration)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MusicViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.activity_music, parent, false)
return MusicViewHolder(view)
}

override fun onBindViewHolder(holder: MusicViewHolder, position: Int) {
val musicItem = musicList[position]

holder.title.text = musicItem.title
holder.artist.text = musicItem.artist
holder.duration.text = musicItem.duration

val albumArtUri = ContentUris.withAppendedId(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Uri.parse("content://media/external/audio/albumart"),
musicItem.albumId
)

Glide.with(holder.itemView.context)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glide를 썼는데, dependency에 추가가 안되어 있네요

.load(albumArtUri)
.apply(
RequestOptions()
.placeholder(R.drawable.ic_music_note)
.error(R.drawable.ic_music_note)
.centerCrop()
)
.into(holder.albumArt)
}

override fun getItemCount(): Int = musicList.size
}
7 changes: 7 additions & 0 deletions app/src/main/res/drawable/album_art_background.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/surface_color" />
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="#E0E0E0" />
</shape>
7 changes: 7 additions & 0 deletions app/src/main/res/drawable/ic_music_note.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/surface_color" />
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="#E0E0E0" />
</shape>
59 changes: 59 additions & 0 deletions app/src/main/res/layout/activity_music.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recyclerview의 아이템에 사용하는 xml 파일은 activity라는 이름을 가져선 안됩니다.
일반적으로 item_music과 같은 이름을 사용합니다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical">

<ImageView
android:id="@+id/iv_album_art"
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@drawable/ic_music_note"
android:scaleType="centerCrop"
android:background="@drawable/album_art_background" />

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp">

<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="title"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/primary_text"
android:maxLines="1"
android:ellipsize="end" />

<TextView
android:id="@+id/tv_artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="artist"
android:textSize="14sp"
android:textColor="@color/secondary_text"
android:layout_marginTop="2dp"
android:maxLines="1"
android:ellipsize="end" />

</LinearLayout>

<TextView
android:id="@+id/tv_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textSize="12sp"
android:textColor="@color/secondary_text"
android:layout_marginStart="8dp" />

</LinearLayout>
33 changes: 33 additions & 0 deletions app/src/main/res/layout/activity_permission.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activity_permission이 아니라 activity_main에 있어야 하는 내용입니다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_color"
tools:context=".MainActivity">

<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/primary_color"
android:elevation="4dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="음악"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold" />

</androidx.appcompat.widget.Toolbar>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_music"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
tools:listitem="@layout/activity_music" />

</LinearLayout>
11 changes: 11 additions & 0 deletions app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,15 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="green">#4CAF50</color>
<color name="t_200">#03DAC5</color>
<color name="t_700">#018786</color>
<color name="p_500">#6200EE</color>
<color name="p_700">#3700B3</color>
<color name="citrus">#FF9800</color>
<color name="secondary_text">#757575</color>
<color name="primary_text">#212121</color>
<color name="surface_color">#F5F5F5</color>
<color name="background_color">#FFFFFF</color>
<color name="primary_color">#6200EE</color>
</resources>
Loading