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
1 change: 1 addition & 0 deletions .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

167 changes: 161 additions & 6 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,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<MusicData>()

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)
}
}
}
48 changes: 48 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,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<MusicData>,
private val listener: OnItemClickListener
) : RecyclerView.Adapter<MusicAdapter.MusicViewHolder>() {

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)
}
}
10 changes: 10 additions & 0 deletions app/src/main/java/com/example/bcsd_android_2025_1/MusicItem.kt
Original file line number Diff line number Diff line change
@@ -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
)
63 changes: 63 additions & 0 deletions app/src/main/java/com/example/bcsd_android_2025_1/MusicService.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading