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
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
val fragment_version = "1.8.9"
implementation("androidx.fragment:fragment-ktx:$fragment_version")
}
10 changes: 9 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -20,6 +23,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MusicService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaPlayback" />
</application>

</manifest>
168 changes: 166 additions & 2 deletions app/src/main/java/com/example/android_25_2/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,184 @@
package com.example.android_25_2

import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.os.IBinder
import android.provider.MediaStore
import android.provider.Settings
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {
private var isBound = false
private var musicService: MusicService? = null
private lateinit var player: ConstraintLayout
private lateinit var tvTitle: TextView
private lateinit var tvArtist: TextView

private val serviceConnection = object : ServiceConnection{
override fun onServiceConnected(name: ComponentName?, service: IBinder?){
val binder = service as MusicService.MusicBinder
musicService = binder.getService()
isBound = true

musicService?.setOnMusicChangeListener { music ->
updatePlayer(music)
}
updatePlayer(musicService?.currentMusic)
}

override fun onServiceDisconnected(p0: ComponentName?) {
musicService = null
isBound = false
}
}
fun updatePlayer(music: MusicItem?) {
if (music != null) {
player.visibility = ConstraintLayout.VISIBLE
tvTitle.text = music.displayName
tvArtist.text = music.artist
} else {
player.visibility = ConstraintLayout.GONE
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}

player = findViewById(R.id.player)
tvTitle = findViewById(R.id.player_title)
tvArtist = findViewById(R.id.player_artist)

permissionCheck()
createNotificationChannel()

val musicList = mutableListOf<MusicItem>()

val projection = arrayOf(
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media._ID
)

applicationContext.contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
"",
arrayOf(),
""
)?.use { cursor ->
val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
val artistsColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
while (cursor.moveToNext()){
val displayName = cursor.getString(displayNameColumn)
val duration = cursor.getInt(durationColumn)
val artists = cursor.getString(artistsColumn)
val id = cursor.getString(idColumn)
val uri = Uri.withAppendedPath(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id.toString()
)
musicList.add(MusicItem(displayName, duration, artists, uri))
}
}

val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
val musicAdapter = MusicAdapter(musicList) { music ->
playMusicWithService(music)
}
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = musicAdapter
}

private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
val channel = NotificationChannel(
"music_channel",
"Music Playback",
NotificationManager.IMPORTANCE_LOW
)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}

private fun playMusicWithService(music: MusicItem) {
val intent = Intent(this, MusicService::class.java).apply {
action = "ACTION_PLAY"
putExtra("MUSIC_TITLE", music.displayName)
putExtra("MUSIC_ARTIST", music.artist)
putExtra("MUSIC_URI", music.uri.toString())
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
bindService(Intent(this, MusicService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
}

private fun permissionCheck() {
val mediaPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_AUDIO
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
if (ContextCompat.checkSelfPermission(this, mediaPermission)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this, arrayOf(mediaPermission), 1000
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 if 문이
if (ContextCompat.checkSelfPermission(this, mediaPermission) != PackageManager.PERMISSION_GRANTED)
안에 있으면 안됩니다.

이유는 mediaPermission 이 거절 되어있을 경우에만 POST_NOTIFICATIONS 검사를 하게 됩니다.
그러니 media 권한은 있는데 notification 권한이 없어도 정상 작동하게 됩니다.

if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.POST_NOTIFICATIONS
), 1000
)
}
}

if(!ActivityCompat.shouldShowRequestPermissionRationale(this, mediaPermission)) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse("package:${packageName}"))
startActivity(intent)
}
if(!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse("package:${packageName}"))
startActivity(intent)
}
}
}
override fun onDestroy() {
super.onDestroy()
if (isBound) {
unbindService(serviceConnection)
isBound = false
}
}
}
49 changes: 49 additions & 0 deletions app/src/main/java/com/example/android_25_2/MusicAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.example.android_25_2

import android.content.Context
import android.media.MediaPlayer
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.constraintlayout.motion.widget.MotionScene.Transition.TransitionOnClick
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder

class MusicAdapter(private val musicList: List<MusicItem>, private val onClick: (MusicItem) -> Unit): RecyclerView.Adapter<ViewHolder>(){
private var mediaPlayer: MediaPlayer? = null

class MusicViewHolder(view: View) : RecyclerView.ViewHolder(view){
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item, parent, false)

return MusicViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val music = musicList[position]
val displayName: TextView = holder.itemView.findViewById(R.id.text_display_name)
val artists: TextView = holder.itemView.findViewById(R.id.text_artist)
val duration: TextView = holder.itemView.findViewById(R.id.text_duration)
displayName.text = music.displayName
artists.text = music.artist
duration.text = music.duration.toString()

holder.itemView.setOnClickListener{
mediaPlayer?.stop()
mediaPlayer?.release()
onClick(music)

mediaPlayer = MediaPlayer().apply {
Copy link
Collaborator

Choose a reason for hiding this comment

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

앞에서 onClick(music) 을 통해 music 을 실행하고 있습니다.

여기서 한번 더 MediaPlayer 를 만들고 start() 를 한다면
노래가 2번 실행됩니다.
삭제되어야 하는게 맞습니다.

setDataSource(holder.itemView.context, music.uri)
prepare()
start()
}
}
}

override fun getItemCount() = musicList.size
}
10 changes: 10 additions & 0 deletions app/src/main/java/com/example/android_25_2/MusicItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.android_25_2

import android.net.Uri

data class MusicItem (
val displayName: String,
val duration: Int,
val artist: String,
val uri: Uri
)
114 changes: 114 additions & 0 deletions app/src/main/java/com/example/android_25_2/MusicService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.example.android_25_2

import android.app.Notification
import android.app.PendingIntent
import android.app.Service
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 var mediaPlayer: MediaPlayer? = null
var currentMusic: MusicItem? = null
private val binder = MusicBinder()
private var musicChangeListener: ((MusicItem?) -> Unit)? = null

inner class MusicBinder : Binder() {
fun getService(): MusicService = this@MusicService
}

fun setOnMusicChangeListener(listener: (MusicItem?) -> Unit) {
musicChangeListener = listener
}

override fun onStartCommand(intent: Intent?, flags: Int, startID: Int): Int{
when (intent?.action) {
"ACTION_PLAY" -> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

이런 부분도 상수 관리가 좋겠네요.

val title = intent.getStringExtra("MUSIC_TITLE")?: ""
val artist = intent.getStringExtra("MUSIC_ARTIST")?: ""
val uriString = intent.getStringExtra("MUSIC_URI")?: ""
Copy link
Collaborator

Choose a reason for hiding this comment

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

MUSIC_TITLE, MUSIC_ARTIST, MUSIC_URI 는 상수 관리를 한다면 좋을 거 같습니다.

val uri = Uri.parse(uriString)

val music = MusicItem(title, 0, artist, uri)
playMusic(music)
}
"ACTION_STOP" -> {
stopMusic()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
return START_STICKY
}

private fun playMusic(music: MusicItem) {
try {
mediaPlayer?.stop()
mediaPlayer?.release()

mediaPlayer = MediaPlayer().apply {
setDataSource(applicationContext, music.uri)
prepare()
start()
}

currentMusic = music
musicChangeListener?.invoke(music)
startForeground(1, createNotification(music))

} catch (e: Exception) {
e.printStackTrace()
}
}

private fun stopMusic() {
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
currentMusic = null
musicChangeListener?.invoke(null)
}

private fun createNotification(music: MusicItem): Notification {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)

val stopIntent = Intent(this, MusicService::class.java).apply {
action = "ACTION_STOP"
}
val stopPendingIntent = PendingIntent.getService(
this,
1,
stopIntent,
PendingIntent.FLAG_IMMUTABLE
)

return NotificationCompat.Builder(this, "music_channel")
.setContentTitle(music.displayName)
.setContentText(music.artist)
.setSmallIcon(android.R.drawable.ic_media_play)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent)
.setOngoing(true)
.build()
}

override fun onBind(intent: Intent?): IBinder {
return binder
}

override fun onDestroy() {
super.onDestroy()
stopMusic()
}
}
Loading