diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..c4e4683
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+BCSD_Android_2025-1
\ No newline at end of file
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/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..639c779
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..c224ad5
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ 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..02a7102
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ 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..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5791375..3ec5dfe 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -36,7 +36,6 @@ android {
}
dependencies {
-
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4c80941..8dd65ea 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -21,6 +33,10 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/ListData.kt b/app/src/main/java/com/example/bcsd_android_2025_1/ListData.kt
new file mode 100644
index 0000000..8e523da
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/ListData.kt
@@ -0,0 +1,6 @@
+package com.example.bcsd_android_2025_1
+
+import android.net.Uri
+
+data class ListData(val musicUri: Uri, val albumImgUri: Long, val title:String, val name:String, val time:Long)
+
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/LocalChangeReceiver.kt b/app/src/main/java/com/example/bcsd_android_2025_1/LocalChangeReceiver.kt
new file mode 100644
index 0000000..3ee2dcf
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/LocalChangeReceiver.kt
@@ -0,0 +1,16 @@
+package com.example.bcsd_android_2025_1
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+class LocalChangeReceiver: BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val title = intent?.getStringExtra(MusicService.TITLE_KEY) ?: R.string.default_title
+
+ val toMainIntent = Intent(MainActivity.MAIN_RECEIVER_ACTION)
+ toMainIntent.putExtra(MusicService.TITLE_KEY, title)
+ toMainIntent.setPackage("com.example.bcsd_android_2025_1")
+ context?.sendBroadcast(toMainIntent)
+ }
+}
\ 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..2484e9a 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,252 @@
package com.example.bcsd_android_2025_1
+import android.Manifest
+import android.content.BroadcastReceiver
+import android.content.ContentUris
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
-import androidx.activity.enableEdgeToEdge
+import android.provider.MediaStore
+import android.provider.Settings
+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.RecyclerView
+import android.widget.Toast
class MainActivity : AppCompatActivity() {
+ companion object {
+ const val BATTERY_LEVEL = "level"
+ const val BATTERY_SCALE = "scale"
+ const val MAIN_RECEIVER_ACTION = "action"
+ }
+
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var permissionTextView: TextView
+ private lateinit var permissionButton: Button
+ private lateinit var serviceIntent : Intent
+ private lateinit var playingMusicTextView: TextView
+
+ private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+ when(isGranted) {
+ true->{
+ recyclerView.visibility = View.VISIBLE
+ permissionButton.visibility = View.GONE
+ permissionTextView.visibility = View.GONE
+ loadMusics()
+ }
+ else->{
+ when(shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE))
+ {
+ true->permissionDialog(true)
+ else->{
+ permissionDialog(false)
+ }
+ }
+ }
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+
+
+ serviceIntent = Intent(this,MusicService::class.java)///////
+
+ recyclerView = findViewById(R.id.recycler_view)
+ permissionTextView = findViewById(R.id.need_permission_textview)
+ permissionButton = findViewById(R.id.need_permission_button)
+ playingMusicTextView = findViewById(R.id.playing_music_textview)
+
+ recyclerView.visibility = View.GONE
+ permissionButton.visibility = View.GONE
+ permissionTextView.visibility = View.GONE
+
+ requestPermissionLaunch()
+
+ permissionButton.setOnClickListener{
+ val intent = Intent()
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ val uri = Uri.fromParts("package", packageName, null)
+ intent.setData(uri)
+ startActivity(intent)
+ }
+ }
+
+ private val mainReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val title = intent?.getStringExtra(MusicService.TITLE_KEY) ?: R.string.default_title
+ playingMusicTextView.text = getString(R.string.playing_music_view, title)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ val batteryFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+ registerReceiver(batteryCheckReceiver, batteryFilter)
+
+ val intentfilter = IntentFilter(MAIN_RECEIVER_ACTION)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(mainReceiver, intentfilter, RECEIVER_NOT_EXPORTED)
+ } else {
+ registerReceiver(mainReceiver, intentfilter)
+ }
+
+ if(permissionGranted()){
+ recyclerView.visibility = View.VISIBLE
+ permissionButton.visibility = View.GONE
+ permissionTextView.visibility = View.GONE
+ loadMusics()
+ }else{
+ recyclerView.visibility = View.GONE
+ permissionButton.visibility = View.VISIBLE
+ permissionTextView.visibility = View.VISIBLE
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ unregisterReceiver(batteryCheckReceiver)
+ unregisterReceiver(mainReceiver)
+ }
+
+ private fun permissionGranted():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 requestPermissionLaunch(){
+ val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Manifest.permission.READ_MEDIA_AUDIO
+ } else {
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ }
+
+ if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
+ recyclerView.visibility = View.VISIBLE
+ permissionButton.visibility = View.GONE
+ permissionTextView.visibility = View.GONE
+ loadMusics()
+ } else {
+ requestPermission.launch(permission)
+ }
}
-}
\ No newline at end of file
+
+ private fun permissionDialog(requestDialog : Boolean){
+ val builder = androidx.appcompat.app.AlertDialog.Builder(this)
+ builder.setTitle(R.string.permission_title_dialog)
+ builder.setMessage(R.string.permission_message_dialog)
+ builder.setPositiveButton(R.string.ok){dialog, _->
+ dialog.dismiss()
+ if(!requestDialog){
+ val intent = Intent()
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ val uri = Uri.fromParts("package", packageName, null)
+ intent.setData(uri)
+ startActivity(intent)
+ }
+ }
+ builder.setNegativeButton(R.string.no){dialog, _->
+ dialog.dismiss()
+ }
+ builder.show()
+ }
+
+
+ private fun loadMusics(){
+ val list = ArrayList()
+
+ val projection = arrayOf(
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.ARTIST,
+ MediaStore.Audio.Media.DURATION,
+ MediaStore.Audio.Media._ID,
+ MediaStore.Audio.Media.ALBUM_ID)
+
+ contentResolver.query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ null,
+ null,
+ null
+ )?.use{ cursor->
+ val idIndex = cursor.getColumnIndex(MediaStore.Audio.Media._ID)
+ val titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
+ val nameIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)
+ val timeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION)
+ val albumIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID)
+ while (cursor.moveToNext()){
+ val id = cursor.getLong(idIndex)
+ val title = cursor.getString(titleIndex)?:getString(R.string.default_title)
+ val name = cursor.getString(nameIndex)?:getString(R.string.default_name)
+ val time = cursor.getLong(timeIndex)
+ val albumId = cursor.getLong(albumIdIndex)
+ val musicUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
+
+ list.add(ListData(musicUri, albumId, title, name, time))
+ }
+ }
+
+ recyclerView.adapter = RecyclerViewAdapter(list, object:RecyclerViewAdapter.OnItemClickListener{
+ override fun itemClick(item:ListData){
+ playMusic(item)
+ }
+ })
+ }
+
+ private fun playMusic(item:ListData) {
+ serviceIntent.putExtra(MusicService.MUSIC_URI_KEY, item.musicUri.toString())
+ serviceIntent.putExtra(MusicService.MUSIC_TITLE_KEY, item.title)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(serviceIntent)
+ } else {
+ startService(serviceIntent)
+ }
+ }
+
+ private val batteryCheckReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val level = intent?.getIntExtra(BATTERY_LEVEL, -1) ?: -1
+ val scale = intent?.getIntExtra(BATTERY_SCALE, -1) ?: -1
+
+ if (level != -1 && scale != -1) {
+ val batteryPercent = (level / scale.toFloat()) * 100
+
+ if (batteryPercent <= 20f) {
+ Toast.makeText(context, R.string.battery_alert_message, Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+ }
+
+ fun serviceStart(view : View){
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(serviceIntent)
+ }
+ else{
+ startService(serviceIntent)
+ }
+ }
+
+ fun serviceStop(view : View){
+ stopService(serviceIntent)
+ }
+}
+
+
+
+
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..48ad754
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicService.kt
@@ -0,0 +1,92 @@
+package com.example.bcsd_android_2025_1
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.media.MediaPlayer
+import android.net.Uri
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+
+class MusicService : Service() {
+ companion object{
+ const val CHANNEL_ID = "notification_music"
+ const val CHANNEL_NAME = "music_channel"
+ const val NOTIFICATION_ID = 1111
+ const val TITLE_KEY = "title"
+ const val MUSIC_URI_KEY = "musicUri"
+ const val MUSIC_TITLE_KEY = "musicTitle"
+ const val TITLE_INTENT_ACTION = "MUSIC_PLAYING"
+ }
+
+ private var mediaPlayer: MediaPlayer? = null
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ createNotificationChannel()
+
+ val musicUriString = intent?.getStringExtra(MUSIC_URI_KEY)
+ val title = intent?.getStringExtra(MUSIC_TITLE_KEY) ?: getString(R.string.default_title)
+
+ if (musicUriString != null) {
+ val uri = Uri.parse(musicUriString)
+
+ mediaPlayer?.release()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForeground(NOTIFICATION_ID, buildNotification(title), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
+ } else {
+ startForeground(NOTIFICATION_ID, buildNotification(title))
+ }
+
+ mediaPlayer = MediaPlayer().apply {
+ setOnPreparedListener {
+ it.start()
+ }
+ setDataSource(applicationContext, uri)
+ prepareAsync()
+ }
+
+ val intentTitle = Intent(this, LocalChangeReceiver::class.java)
+ intentTitle.action = TITLE_INTENT_ACTION
+ intentTitle.putExtra(TITLE_KEY, title)
+ sendBroadcast(intentTitle)
+ }
+
+ return START_STICKY
+ }
+
+ private fun buildNotification(title: String): Notification {
+ val intent = Intent(this, MainActivity::class.java)
+ val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+
+ return NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(getString(R.string.notification_playing_music))
+ .setContentText(title)
+ .setSmallIcon(android.R.drawable.ic_media_play)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .setAutoCancel(false)
+ .build()
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH)
+ val manager = getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mediaPlayer?.release()
+ mediaPlayer = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/RecyclerViewAdapter.kt b/app/src/main/java/com/example/bcsd_android_2025_1/RecyclerViewAdapter.kt
new file mode 100644
index 0000000..f58e4db
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/RecyclerViewAdapter.kt
@@ -0,0 +1,72 @@
+package com.example.bcsd_android_2025_1
+
+import android.graphics.BitmapFactory
+import android.media.MediaMetadataRetriever
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+
+class RecyclerViewAdapter(private val items:MutableList, private val itemClickListener: OnItemClickListener): RecyclerView.Adapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val inflatedView = LayoutInflater.from(parent.context).inflate(R.layout.item_recyclerview, parent, false)
+ return ViewHolder(inflatedView)
+ }
+
+ interface OnItemClickListener{
+ fun itemClick(item:ListData)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = items[position]
+ holder.bind(item, position)
+ }
+
+ override fun getItemCount(): Int = items.size
+
+ inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
+ fun bind(item: ListData, position: Int) {
+ val imageItem: ImageView = view.findViewById(R.id.imageview_item)
+ val titleItem: TextView = view.findViewById(R.id.title_textview_item)
+ val nameItem: TextView = view.findViewById(R.id.name_textview_item)
+ val timeItem: TextView = view.findViewById(R.id.time_textview_item)
+ titleItem.text = item.title
+ nameItem.text = item.name
+ timeItem.text = item.time.timeSet()
+
+ val retriever = MediaMetadataRetriever()
+ try {
+ retriever.setDataSource(view.context, item.musicUri)
+
+ val art = retriever.embeddedPicture
+ if (art != null) {
+ val bitmap = BitmapFactory.decodeByteArray(art, 0, art.size)
+ imageItem.setImageBitmap(bitmap)
+ } else {
+ imageItem.setImageResource(R.drawable.album_image_default_aespa)
+ }
+ retriever.release()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ imageItem.setImageResource(R.drawable.album_image_default_aespa)
+ }
+
+ view.setOnClickListener{
+ itemClickListener.itemClick(item)
+ }
+ }
+ }
+
+ private fun Long.timeSet():String{
+ val hour = this/(1000*60*60)
+ val min = (this/(1000*60))%60
+ val sec = (this/1000)%60
+ return if(hour>0){
+ "%d:%02d:%02d".format(hour, min, sec)
+ }else{
+ "%02d:%02d".format(min,sec)
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/album_image_default_aespa.png b/app/src/main/res/drawable/album_image_default_aespa.png
new file mode 100644
index 0000000..4eaeb47
Binary files /dev/null and b/app/src/main/res/drawable/album_image_default_aespa.png differ
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 311f3cb..b11fae5 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -7,13 +7,53 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
+
+
+ app:layout_constraintTop_toTopOf="parent"/>
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_recyclerview.xml b/app/src/main/res/layout/item_recyclerview.xml
new file mode 100644
index 0000000..c591fc6
--- /dev/null
+++ b/app/src/main/res/layout/item_recyclerview.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..67bb5e5 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -2,4 +2,9 @@
#FF000000
#FFFFFFFF
+ #EADDFF
+ #D0BCFF
+ #6750A4
+ #381E72
+ #A2A0A0
\ 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..82ee4f7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,16 @@
BCSD_Android_2025-1
+ 파일을 보려면 권한을 허용해야합니다.
+ 설정으로 이동
+ 00:00
+ Unknown
+ 제목 없음
+ 권한 설정
+ 권한을 설정하세요.
+ 확인
+ 거부
+ ※배터리 잔량이 20%이하입니다
+ 재생 중인 음악 : %s
+ 재생 중인 음악 없음
+ 음악 재생중
\ No newline at end of file