diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..97f0a8e
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..9027b72
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ 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..73708be 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,169 @@
package com.example.bcsd_android_2025_1
-import android.os.Bundle
-import androidx.activity.enableEdgeToEdge
+import android.Manifest
+import android.content.*
+import android.net.Uri
+import android.os.*
+import android.provider.MediaStore
+import android.view.View
+import android.widget.Button
+import android.widget.TextView
+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
+
+class MainActivity : AppCompatActivity(), MusicAdapter.OnItemClickListener {
+
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var permissionMessage: TextView
+ private lateinit var openSettingsButton: Button
+ private lateinit var requestPermissionButton: Button
+ private lateinit var musicAdapter: MusicAdapter
+ private val musicList = mutableListOf()
+
+ private var musicService: MusicService? = null
+ private var isBound = false
+
+ private val permissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { granted ->
+ if (granted) showMusicList()
+ else showPermissionUI()
+ }
+
+ private val connection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ val binder = service as MusicService.MusicBinder
+ musicService = binder.getService()
+ isBound = true
+ }
+ override fun onServiceDisconnected(name: ComponentName?) {
+ musicService = null
+ isBound = false
+ }
+ }
-class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+
+ recyclerView = findViewById(R.id.MusicRecyclerView)
+ permissionMessage = findViewById(R.id.text1)
+ openSettingsButton = findViewById(R.id.OpenButton)
+ requestPermissionButton = findViewById(R.id.RequestButton)
+
+ musicAdapter = MusicAdapter(musicList, this)
+ recyclerView.adapter = musicAdapter
+ recyclerView.layoutManager = LinearLayoutManager(this)
+
+ recyclerView.visibility = View.GONE
+ permissionMessage.visibility = View.GONE
+ openSettingsButton.visibility = View.GONE
+ requestPermissionButton.visibility = View.GONE
+
+ if (hasPermission()) showMusicList()
+ else showPermissionUI()
+
+ requestPermissionButton.setOnClickListener { requestPermission() }
+ openSettingsButton.setOnClickListener {
+ val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ val uri = Uri.fromParts("package", packageName, null)
+ intent.data = uri
+ startActivity(intent)
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ val intent = Intent(this, MusicService::class.java)
+ bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ if (isBound) {
+ unbindService(connection)
+ isBound = false
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (hasPermission()) showMusicList()
+ }
+
+ private fun hasPermission(): 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 showPermissionUI() {
+ recyclerView.visibility = View.GONE
+ permissionMessage.visibility = View.VISIBLE
+ openSettingsButton.visibility = View.VISIBLE
+ requestPermissionButton.visibility = View.VISIBLE
+ }
+
+ private fun showMusicList() {
+ recyclerView.visibility = View.VISIBLE
+ permissionMessage.visibility = View.GONE
+ openSettingsButton.visibility = View.GONE
+ requestPermissionButton.visibility = View.GONE
+ loadMusicList()
+ }
+
+ private fun loadMusicList() {
+ musicList.clear()
+ 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,
+ MediaStore.Audio.Media.TITLE + " ASC"
+ )
+ cursor?.use {
+ val idIdx = it.getColumnIndex(MediaStore.Audio.Media._ID)
+ val titleIdx = it.getColumnIndex(MediaStore.Audio.Media.TITLE)
+ val artistIdx = it.getColumnIndex(MediaStore.Audio.Media.ARTIST)
+ val durationIdx = it.getColumnIndex(MediaStore.Audio.Media.DURATION)
+ while (it.moveToNext()) {
+ val id = it.getLong(idIdx)
+ val title = it.getString(titleIdx) ?: "Unknown"
+ val artist = it.getString(artistIdx) ?: "Unknown"
+ val duration = it.getLong(durationIdx)
+ val contentUri = ContentUris.withAppendedId(uri, id)
+ musicList.add(MusicData(title, artist, duration, contentUri))
+ }
+ }
+ musicAdapter.notifyDataSetChanged()
+ }
+
+ override fun onItemClick(music: MusicData) {
+ val intent = Intent(this, MusicService::class.java).apply {
+ putExtra("music_uri", music.uri.toString())
+ putExtra("music_title", music.title)
+ }
+ ContextCompat.startForegroundService(this, intent)
+ if (isBound) musicService?.playMusic(music.uri, music.title)
}
-}
\ No newline at end of file
+}
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..314800a
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicAdapter.kt
@@ -0,0 +1,48 @@
+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 listener: OnItemClickListener
+) : RecyclerView.Adapter() {
+
+ interface OnItemClickListener {
+ fun onItemClick(music: MusicData)
+ }
+
+ class MusicViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val titleText: TextView = view.findViewById(R.id.textTitle)
+ val artistText: TextView = view.findViewById(R.id.textArtist)
+ val durationText: TextView = view.findViewById(R.id.textDuration)
+ }
+
+ 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 getItemCount() = musicList.size
+
+ override fun onBindViewHolder(holder: MusicViewHolder, position: Int) {
+ val music = musicList[position]
+ holder.titleText.text = music.title
+ holder.artistText.text = music.artist
+ holder.durationText.text = formatDuration(music.duration)
+ holder.itemView.setOnClickListener {
+ listener.onItemClick(music)
+ }
+ }
+
+ private fun formatDuration(durationMs: Long): String {
+ val totalSec = durationMs / 1000
+ val min = totalSec / 60
+ val sec = totalSec % 60
+ return String.format("%02d:%02d", min, sec)
+ }
+}
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MusicItem.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MusicItem.kt
new file mode 100644
index 0000000..a455656
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicItem.kt
@@ -0,0 +1,10 @@
+package com.example.bcsd_android_2025_1
+
+import android.net.Uri
+
+data class MusicData(
+ val title: String,
+ val artist: String,
+ val duration: Long,
+ val uri: Uri
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MusicService.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MusicService.kt
new file mode 100644
index 0000000..6b0afcd
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicService.kt
@@ -0,0 +1,63 @@
+package com.example.bcsd_android_2025_1
+
+import android.app.*
+import android.content.Intent
+import android.media.MediaPlayer
+import android.net.Uri
+import android.os.Binder
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+
+class MusicService : Service() {
+
+ private val binder = MusicBinder()
+ private var mediaPlayer: MediaPlayer? = null
+
+ inner class MusicBinder : Binder() {
+ fun getService(): MusicService = this@MusicService
+ }
+
+ override fun onBind(intent: Intent?): IBinder = binder
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val uriString = intent?.getStringExtra("music_uri")
+ val title = intent?.getStringExtra("music_title") ?: "재생 중"
+ if (uriString != null) {
+ playMusic(Uri.parse(uriString), title)
+ }
+ return START_NOT_STICKY
+ }
+
+ fun playMusic(uri: Uri, title: String) {
+ mediaPlayer?.release()
+ mediaPlayer = MediaPlayer.create(this, uri).apply {
+ setOnCompletionListener {
+ stopSelf()
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ }
+ start()
+ }
+ showNotification(title)
+ }
+
+ private fun showNotification(title: String) {
+ val notifIntent = Intent(this, MainActivity::class.java)
+ val pendingIntent = PendingIntent.getActivity(
+ this, 0, notifIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ val notification = NotificationCompat.Builder(this, "music_channel")
+ .setContentTitle("음악 재생")
+ .setContentText(title)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .build()
+ startForeground(1, notification)
+ }
+
+ override fun onDestroy() {
+ mediaPlayer?.release()
+ mediaPlayer = null
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ super.onDestroy()
+ }
+}
diff --git a/app/src/main/res/drawable/ic_logo_google.png b/app/src/main/res/drawable/ic_logo_google.png
new file mode 100644
index 0000000..f39297a
Binary files /dev/null and b/app/src/main/res/drawable/ic_logo_google.png differ
diff --git a/app/src/main/res/drawable/ic_logo_kakao.png b/app/src/main/res/drawable/ic_logo_kakao.png
new file mode 100644
index 0000000..712c565
Binary files /dev/null and b/app/src/main/res/drawable/ic_logo_kakao.png differ
diff --git a/app/src/main/res/drawable/ic_logo_naver.png b/app/src/main/res/drawable/ic_logo_naver.png
new file mode 100644
index 0000000..ac4036e
Binary files /dev/null and b/app/src/main/res/drawable/ic_logo_naver.png differ
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 311f3cb..c3dc41d 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,19 +1,62 @@
-
+ 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..75468dc
--- /dev/null
+++ b/app/src/main/res/layout/item_music.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c6c4daf..ad76f1e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,10 @@
- BCSD_Android_2025-1
-
\ No newline at end of file
+ MusicPlayer
+ 재생 중: 없음
+ 권한이 필요합니다.
+ 권한 요청
+ 설정 열기
+ 제목
+ 아티스트
+ 00:00
+