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
15 changes: 12 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
package="com.example.bcsd_android_2025_1">

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

<application
android:allowBackup="true"
Expand All @@ -17,10 +23,13 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<service
android:name=".MusicPlayerService"
android:exported="false"
android:foregroundServiceType="mediaPlayback" />

</application>
</manifest>
187 changes: 182 additions & 5 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,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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

애플리케이션 재실행 시 permissionDeniedCount가 초기화되기 때문에, 다이얼로그를 띄울 수 없어도 permissionDeniedCount가 0이라 띄우려고 시도를 하게 됩니다.

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<Music>()
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)
}
}
}
}
10 changes: 10 additions & 0 deletions app/src/main/java/com/example/bcsd_android_2025_1/Music.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 Music(
val title: String,
val artist: String,
val duration: Long,
val uri: Uri
)
43 changes: 43 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,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<Music>,
private val onItemClick: (Music) -> Unit
) : RecyclerView.Adapter<MusicAdapter.MusicViewHolder>() {

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Long의 확장함수로 작성할 수 있을 것 같습니다

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)
}
}
Loading