diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4c80941..64509f3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,12 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.bcsd_android_2025_1">
+
+
+
+
+
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt
index 3ffa0eb..1889f3b 100644
--- a/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt
@@ -1,14 +1,191 @@
package com.example.bcsd_android_2025_1
-import android.os.Bundle
-import androidx.activity.enableEdgeToEdge
+import android.Manifest
+import android.content.*
+import android.content.pm.PackageManager
+import android.os.*
+import android.provider.MediaStore
+import android.provider.Settings
+import android.view.View
+import android.widget.*
+import androidx.activity.result.contract.ActivityResultContracts
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 android.net.Uri
+import androidx.core.app.ActivityCompat
class MainActivity : AppCompatActivity() {
+
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var permissionLayout: View
+ private lateinit var btnRequestPermission: Button
+ private lateinit var playerLayout: View
+ private lateinit var currentTitle: TextView
+ private lateinit var currentArtist: TextView
+ private lateinit var btnPlayPause: ImageButton
+ private var permissionDeniedCount = 0
+
+
+ private val nowPlayingReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val title = intent?.getStringExtra("title") ?: return
+ val artist = intent.getStringExtra("artist") ?: return
+ val isPlaying = intent.getBooleanExtra("isPlaying", false)
+
+ currentTitle.text = title
+ currentArtist.text = artist
+ btnPlayPause.setImageResource(
+ if (isPlaying) android.R.drawable.ic_media_pause
+ else android.R.drawable.ic_media_play
+ )
+ playerLayout.visibility = View.VISIBLE
+ }
+ }
+
+ private val permissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ if (isGranted) {
+ loadMusicFiles()
+ } else {
+ permissionDeniedCount++
+ if (permissionDeniedCount < 2) {
+ requestPermission()
+ } else {
+ showPermissionLayout()
+ }
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ 100
+ )
+ }
+
+ recyclerView = findViewById(R.id.music_recycler_view)
+ permissionLayout = findViewById(R.id.permission_layout)
+ btnRequestPermission = findViewById(R.id.btn_request_permission)
+ playerLayout = findViewById(R.id.player_layout)
+ currentTitle = findViewById(R.id.text_current_title)
+ currentArtist = findViewById(R.id.text_current_artist)
+ btnPlayPause = findViewById(R.id.button_play_pause)
+
+ btnRequestPermission.setOnClickListener {
+ if (permissionDeniedCount < 2) {
+ requestPermission()
+ } else {
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.fromParts("package", packageName, null)
+ startActivity(intent)
+ }
+ }
+
+ btnPlayPause.setOnClickListener {
+ val intent = Intent("com.example.bcsd_android_2025_1.ACTION_TOGGLE_PLAY")
+ sendBroadcast(intent)
+ }
+
+ if (isPermissionGranted()) {
+ loadMusicFiles()
+ } else {
+ showPermissionLayout()
+ requestPermission()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ registerReceiver(
+ nowPlayingReceiver,
+ IntentFilter("com.example.bcsd_android_2025_1.NOW_PLAYING"),
+ RECEIVER_NOT_EXPORTED
+ )
+
+ sendBroadcast(Intent("com.example.bcsd_android_2025_1.REQUEST_NOW_PLAYING"))
+ }
+
+ override fun onPause() {
+ super.onPause()
+ unregisterReceiver(nowPlayingReceiver)
+ }
+
+ private fun isPermissionGranted(): Boolean {
+ val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Manifest.permission.READ_MEDIA_AUDIO
+ } else {
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ }
+ return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun requestPermission() {
+ val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Manifest.permission.READ_MEDIA_AUDIO
+ } else {
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ }
+ permissionLauncher.launch(permission)
+ }
+
+ private fun showPermissionLayout() {
+ recyclerView.visibility = View.GONE
+ permissionLayout.visibility = View.VISIBLE
+ }
+
+ private fun loadMusicFiles() {
+ recyclerView.visibility = View.VISIBLE
+ permissionLayout.visibility = View.GONE
+
+ val musicList = mutableListOf()
+ val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ val projection = arrayOf(
+ MediaStore.Audio.Media._ID,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.ARTIST,
+ MediaStore.Audio.Media.DURATION
+ )
+
+ val cursor = contentResolver.query(
+ uri,
+ projection,
+ MediaStore.Audio.Media.IS_MUSIC + "!= 0",
+ null,
+ null
+ )
+
+ cursor?.use {
+ val idIdx = it.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
+ val titleIdx = it.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
+ val artistIdx = it.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
+ val durationIdx = it.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
+
+ while (it.moveToNext()) {
+ val id = it.getLong(idIdx)
+ val contentUri = ContentUris.withAppendedId(uri, id)
+ val title = it.getString(titleIdx) ?: "Unknown"
+ val artist = it.getString(artistIdx) ?: "Unknown"
+ val duration = it.getLong(durationIdx)
+ musicList.add(Music(title, artist, duration, contentUri))
+ }
+ }
+
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ recyclerView.adapter = MusicAdapter(musicList) { selectedMusic ->
+ val serviceIntent = Intent(this, MusicPlayerService::class.java).apply {
+ putExtra(MusicPlayerService.EXTRA_MUSIC_URI, selectedMusic.uri)
+ putExtra(MusicPlayerService.EXTRA_TITLE, selectedMusic.title)
+ putExtra(MusicPlayerService.EXTRA_ARTIST, selectedMusic.artist)
+ }
+ startForegroundService(serviceIntent)
+ }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/Music.kt b/app/src/main/java/com/example/bcsd_android_2025_1/Music.kt
new file mode 100644
index 0000000..829b5d1
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/Music.kt
@@ -0,0 +1,10 @@
+package com.example.bcsd_android_2025_1
+
+import android.net.Uri
+
+data class Music(
+ val title: String,
+ val artist: String,
+ val duration: Long,
+ val uri: Uri
+)
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MusicAdapter.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MusicAdapter.kt
new file mode 100644
index 0000000..4f3a793
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicAdapter.kt
@@ -0,0 +1,43 @@
+package com.example.bcsd_android_2025_1
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+
+class MusicAdapter(
+ private val musicList: List,
+ private val onItemClick: (Music) -> Unit
+) : RecyclerView.Adapter() {
+
+ class MusicViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val title: TextView = view.findViewById(R.id.title_text)
+ val artist: TextView = view.findViewById(R.id.artist_text)
+ val duration: TextView = view.findViewById(R.id.duration_text)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MusicViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_music, parent, false)
+ return MusicViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: MusicViewHolder, position: Int) {
+ val music = musicList[position]
+ holder.title.text = music.title
+ holder.artist.text = music.artist
+ holder.duration.text = formatDuration(music.duration)
+ holder.itemView.setOnClickListener { onItemClick(music) }
+ }
+
+ override fun getItemCount(): Int = musicList.size
+
+ private fun formatDuration(durationMs: Long): String {
+ val seconds = durationMs / 1000 % 60
+ val minutes = durationMs / (1000 * 60) % 60
+ val hours = durationMs / (1000 * 60 * 60)
+ return if (hours > 0) "%d:%02d:%02d".format(hours, minutes, seconds)
+ else "%02d:%02d".format(minutes, seconds)
+ }
+}
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MusicPlayerService.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MusicPlayerService.kt
new file mode 100644
index 0000000..23f5317
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicPlayerService.kt
@@ -0,0 +1,142 @@
+package com.example.bcsd_android_2025_1
+
+import android.app.*
+import android.content.*
+import android.media.MediaPlayer
+import android.net.Uri
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import android.os.Build
+
+
+class MusicPlayerService : Service() {
+
+ private var mediaPlayer: MediaPlayer? = null
+ private var currentTitle: String = ""
+ private var currentArtist: String = ""
+
+ companion object {
+ const val CHANNEL_ID = "MusicPlayerChannel"
+ const val EXTRA_MUSIC_URI = "music_uri"
+ const val EXTRA_TITLE = "music_title"
+ const val EXTRA_ARTIST = "music_artist"
+ }
+
+ private val commandReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ when (intent?.action) {
+ "com.example.bcsd_android_2025_1.ACTION_TOGGLE_PLAY" -> {
+ if (mediaPlayer?.isPlaying == true) {
+ mediaPlayer?.pause()
+ } else {
+ mediaPlayer?.start()
+ }
+ broadcastNowPlaying()
+ }
+ "com.example.bcsd_android_2025_1.REQUEST_NOW_PLAYING" -> {
+ broadcastNowPlaying()
+ }
+ }
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ val filter = IntentFilter().apply {
+ addAction("com.example.bcsd_android_2025_1.ACTION_TOGGLE_PLAY")
+ addAction("com.example.bcsd_android_2025_1.REQUEST_NOW_PLAYING")
+ }
+ registerReceiver(commandReceiver, filter)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ intent?.let {
+ val musicUri = it.getParcelableExtra(EXTRA_MUSIC_URI)
+ currentTitle = it.getStringExtra(EXTRA_TITLE) ?: "Unknown"
+ currentArtist = it.getStringExtra(EXTRA_ARTIST) ?: "Unknown"
+
+ musicUri?.let { uri ->
+ playMusic(uri)
+ }
+ }
+ return START_STICKY
+ }
+
+ private fun playMusic(uri: Uri) {
+ mediaPlayer?.release()
+ try {
+ val fd = contentResolver.openFileDescriptor(uri, "r")?.fileDescriptor
+ if (fd != null) {
+ mediaPlayer = MediaPlayer().apply {
+ setDataSource(fd)
+ prepare()
+ start()
+ }
+ showNotification()
+ broadcastNowPlaying()
+ } else {
+ stopSelf()
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ stopSelf()
+ }
+ }
+
+
+ private fun showNotification() {
+ val openAppIntent = Intent(this, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ openAppIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val notification = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(currentTitle)
+ .setContentText(currentArtist)
+ .setSmallIcon(android.R.drawable.ic_media_play)
+ .setOngoing(true)
+ .setContentIntent(pendingIntent)
+ .build()
+
+ startForeground(1, notification)
+ }
+
+ private fun broadcastNowPlaying() {
+ val isPlaying = mediaPlayer?.isPlaying == true
+ val intent = Intent("com.example.bcsd_android_2025_1.NOW_PLAYING").apply {
+ putExtra("title", currentTitle)
+ putExtra("artist", currentArtist)
+ putExtra("isPlaying", isPlaying)
+ }
+ sendBroadcast(intent)
+ }
+
+ override fun onDestroy() {
+ mediaPlayer?.release()
+ unregisterReceiver(commandReceiver)
+ super.onDestroy()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ private fun createNotificationChannel() {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "음악 재생 채널",
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "음악 재생 상태를 보여주는 채널"
+ }
+
+ val manager = getSystemService(NotificationManager::class.java)
+ manager?.createNotificationChannel(channel)
+ }
+
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 311f3cb..064a884 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,19 +1,83 @@
-
+ android:layout_height="match_parent">
-
+
+
+
+
+
+
+
+
+
+ app:layout_constraintEnd_toEndOf="parent">
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_music.xml b/app/src/main/res/layout/item_music.xml
new file mode 100644
index 0000000..64d83fb
--- /dev/null
+++ b/app/src/main/res/layout/item_music.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index c8524cd..bbc5fd8 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -2,4 +2,5 @@
#FF000000
#FFFFFFFF
+ #8E28FF
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c6c4daf..dbfb37c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,8 @@
- BCSD_Android_2025-1
+ My Appliction
+ 파일을 보려면 권한을 허용해야 합니다.
+ 설정으로 이동
+ 제목 없음
+ 가수 없음
+ 재생 또는 일시정지
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 5f61f69..00f5b00 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,8 +1,7 @@
-
-