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/.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/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..97626ba
--- /dev/null
+++ b/.idea/encodings.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..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..b2c751a
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ 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/.kotlin/errors/errors-1751209781864.log b/.kotlin/errors/errors-1751209781864.log
new file mode 100644
index 0000000..1219b50
--- /dev/null
+++ b/.kotlin/errors/errors-1751209781864.log
@@ -0,0 +1,4 @@
+kotlin version: 2.0.21
+error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
+ 1. Kotlin compile daemon is ready
+
diff --git a/.kotlin/errors/errors-1751268238484.log b/.kotlin/errors/errors-1751268238484.log
new file mode 100644
index 0000000..1219b50
--- /dev/null
+++ b/.kotlin/errors/errors-1751268238484.log
@@ -0,0 +1,4 @@
+kotlin version: 2.0.21
+error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
+ 1. Kotlin compile daemon is ready
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5791375..81271fd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,11 +1,12 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.bcsd_android_2025_1"
- compileSdk = 34
+ compileSdk = 35
defaultConfig {
applicationId = "com.example.bcsd_android_2025_1"
@@ -33,6 +34,9 @@ android {
kotlinOptions {
jvmTarget = "11"
}
+ buildFeatures {
+ compose = true
+ }
}
dependencies {
@@ -42,7 +46,18 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4c80941..781f6ec 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,11 @@
+
+
+
+
+
-
+
+
\ 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..64d8f6a 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,232 @@
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.BatteryManager
+import android.os.Build
import android.os.Bundle
-import androidx.activity.enableEdgeToEdge
+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.AlertDialog
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.provider.MediaStore
+import androidx.compose.ui.unit.Constraints
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import androidx.core.app.ActivityCompat
+import android.widget.Toast
+import android.util.Log
class MainActivity : AppCompatActivity() {
+
+ private lateinit var settingsBtn: Button
+ private lateinit var explanationText: TextView
+ private lateinit var nowsongText: TextView
+
+ private val musicUpdateReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val title = intent?.getStringExtra(MusicPlayerService.SONG_TITLE) ?: return
+ Log.d("MainActivity", "Music update received: $title")
+ findViewById(R.id.textview_nowsong).text = title
+ }
+ }
+
+ private val batteryReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
+ val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
+
+ if (level >= 0 && scale > 0) {
+ val batterysize = level * 100 / scale
+ if (batterysize <= 15) {
+ Toast.makeText(context, R.string.battery_msg, Toast.LENGTH_SHORT).show()
+ stopService(Intent(context, MusicPlayerService::class.java))
+ }
+ }
+ }
+ }
+
+ private val permission =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ Manifest.permission.READ_MEDIA_AUDIO
+ else
+ Manifest.permission.READ_EXTERNAL_STORAGE
+
+ private val requestPermission =
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+ if (isGranted) {
+ showMusicList()
+ } else {
+ if (shouldShowRequestPermissionRationale(permission)) {
+ showPermissionDialog()
+ } else {
+ showSettingsButton()
+ }
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+
+ settingsBtn = findViewById(R.id.button_goto_settings)
+ explanationText = findViewById(R.id.textview_permission)
+ nowsongText = findViewById(R.id.textview_nowsong)
+
+ settingsBtn.setOnClickListener {
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.parse("package:$packageName")
+ }
+ startActivity(intent)
+ }
+ requestNotificationPermission()
+ checkPermissionAndRequest()
+
+ val batteryFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+ registerReceiver(batteryReceiver, batteryFilter)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val filter = IntentFilter(MusicPlayerService.MUSIC_UPDATE)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(
+ musicUpdateReceiver,
+ filter,
+ null,
+ null,
+ Context.RECEIVER_NOT_EXPORTED
+ )
+ } else {
+ registerReceiver(musicUpdateReceiver, filter)
+ }
+ }
+ override fun onPause() {
+ super.onPause()
+ unregisterReceiver(musicUpdateReceiver)
+ }
+
+
+ override fun onDestroy() {
+ super.onDestroy()
+ unregisterReceiver(batteryReceiver)
+ unregisterReceiver(musicUpdateReceiver)
}
-}
\ No newline at end of file
+
+ private fun requestNotificationPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ 2001
+ )
+ }
+ }
+ }
+
+ private fun checkPermissionAndRequest() {
+ if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
+ showMusicList()
+ } else {
+ requestPermission.launch(permission)
+ }
+ }
+
+ private fun showPermissionDialog() {
+ AlertDialog.Builder(this)
+ .setTitle(R.string.permdialog_title)
+ .setMessage(R.string.permdialog_msg)
+ .setPositiveButton(R.string.permdialog_pos) { _, _ ->
+ requestPermission.launch(permission)
+ }
+ .setNegativeButton(R.string.permdialog_neg, null)
+ .show()
+ }
+
+ private fun showSettingsButton() {
+ explanationText.visibility = View.VISIBLE
+ settingsBtn.visibility = View.VISIBLE
+ }
+
+ private fun showMusicList() {
+ lifecycleScope.launch {
+ val musicList = withContext(Dispatchers.IO) {
+ loadMusicFromMediaStore()
+ }
+ val recyclerView = findViewById(R.id.recycler_view)
+ recyclerView.adapter = MusicAdapter(musicList) { musicInfo ->
+ val intent = Intent(this@MainActivity, MusicPlayerService::class.java).apply {
+ putExtra(MusicPlayerService.SONG_TITLE, musicInfo.title)
+ putExtra(MusicPlayerService.SONG_URI, musicInfo.uri.toString())
+ }
+ ContextCompat.startForegroundService(this@MainActivity, intent)
+ }
+ recyclerView.layoutManager = LinearLayoutManager(this@MainActivity)
+ }
+ }
+
+ data class MusicInfo(
+ val title: String,
+ val artist: String,
+ val time: Long,
+ val uri: Uri
+ )
+
+ private fun loadMusicFromMediaStore(): List {
+ val musicList = mutableListOf()
+
+ val projection = arrayOf(
+ MediaStore.Audio.Media._ID,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.ARTIST,
+ MediaStore.Audio.Media.DURATION
+ )
+
+ val selection = "${MediaStore.Audio.Media.IS_MUSIC} != 0"
+ val sortOrder = "${MediaStore.Audio.Media.TITLE} ASC"
+
+ val cursor = contentResolver.query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ selection,
+ null,
+ sortOrder
+ )
+
+ cursor?.use {
+ val idIndex = it.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
+ val titleIndex = it.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
+ val artistIndex = it.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
+ val timeIndex = it.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
+
+ while (it.moveToNext()) {
+ val id = it.getLong(idIndex)
+ val title = it.getString(titleIndex) ?: "제목없음"
+ val artist = it.getString(artistIndex) ?: "가수없음"
+ val time = it.getLong(timeIndex)
+ val contentUri =
+ ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
+ musicList.add(MusicInfo(title, artist, time, contentUri))
+ }
+ }
+ return musicList
+ }
+}
+
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..22a2024
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicAdapter.kt
@@ -0,0 +1,49 @@
+package com.example.bcsd_android_2025_1
+import android.content.Intent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView.OnItemClickListener
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+
+class MusicAdapter(
+ private val musicList: List,
+ private val itemClickListener: (MainActivity.MusicInfo) -> Unit) :
+ RecyclerView.Adapter() {
+
+ inner class MusicViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val title = view.findViewById(R.id.tv_title)
+ val artist = view.findViewById(R.id.tv_artist)
+ val time = view.findViewById(R.id.tv_time)
+
+ init {
+ view.setOnClickListener {
+ val pos = adapterPosition
+ if (pos != RecyclerView.NO_POSITION){
+ itemClickListener(musicList[pos])
+ }
+ }
+ }
+ }
+
+ 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.time.text = music.time.toTimeString()
+ }
+
+ override fun getItemCount() = musicList.size
+
+ private fun Long.toTimeString(): String {
+ val min = this / 1000 / 60
+ val sec = (this / 1000 % 60)
+ return String.format("%02d:%02d", min, sec)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MusicPlayerService.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MusicPlayerService.kt
new file mode 100644
index 0000000..40daf2b
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicPlayerService.kt
@@ -0,0 +1,124 @@
+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.media.MediaPlayer
+import android.net.Uri
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import android.content.pm.ServiceInfo
+import android.util.Log
+
+class MusicPlayerService : Service() {
+
+ private var mediaPlayer: MediaPlayer? = null
+ private val CHANNEL_ID = "music_playback_channel"
+
+ companion object {
+ const val MUSIC_UPDATE = "com.example.bcsd_android_2025_1.MUSIC_UPDATE"
+ const val SONG_TITLE = "SONG_TITLE"
+ const val SONG_URI = "SONG_URI"
+ const val NOTIFICATION_ID = 1
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val songUriString = intent?.getStringExtra(SONG_URI) ?: return START_NOT_STICKY
+ val songTitle = intent.getStringExtra(SONG_TITLE) ?: "Unknown"
+ val songUri = Uri.parse(songUriString)
+
+ mediaPlayer?.release()
+ mediaPlayer = MediaPlayer().apply {
+ setOnPreparedListener{
+ start()
+ Log.d("MusicPlayerService", "Starting playback of: $songTitle")
+ val updateIntent = Intent(MUSIC_UPDATE)
+ updateIntent.setPackage(packageName)
+ updateIntent.putExtra(SONG_TITLE, songTitle)
+ sendBroadcast(updateIntent)
+ }
+ setOnCompletionListener {
+ stopSelf()
+ }
+ setOnErrorListener { _, _, _ ->
+ stopSelf()
+ true
+ }
+ }
+
+ try {
+ mediaPlayer?.setDataSource(applicationContext, songUri)
+ val notification = buildNotification(songTitle)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(
+ 1,
+ notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
+ )
+ } else {
+ startForeground(1, notification)
+ }
+
+ mediaPlayer?.prepareAsync()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ stopSelf()
+ return START_NOT_STICKY
+ }
+ return START_STICKY
+ }
+
+ private fun buildNotification(songTitle: String): Notification {
+ val notificationIntent = Intent(this, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ notificationIntent,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ else
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ return NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("현재 재생 음악")
+ .setContentText(songTitle)
+ .setSmallIcon(android.R.drawable.ic_media_play)
+ .setOngoing(true)
+ .setContentIntent(pendingIntent)
+ .setOnlyAlertOnce(true)
+ .setAutoCancel(false)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .build()
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "Music Playback",
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ val manager = getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ override fun onDestroy() {
+ mediaPlayer?.release()
+ mediaPlayer = null
+ super.onDestroy()
+ }
+ override fun onBind(intent: Intent?): IBinder? = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/ui/theme/Color.kt b/app/src/main/java/com/example/bcsd_android_2025_1/ui/theme/Color.kt
new file mode 100644
index 0000000..e897c79
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.example.bcsd_android_2025_1.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/ui/theme/Theme.kt b/app/src/main/java/com/example/bcsd_android_2025_1/ui/theme/Theme.kt
new file mode 100644
index 0000000..9a49c51
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+package com.example.bcsd_android_2025_1.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun BCSD_Android_20251Theme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/ui/theme/Type.kt b/app/src/main/java/com/example/bcsd_android_2025_1/ui/theme/Type.kt
new file mode 100644
index 0000000..e350104
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.example.bcsd_android_2025_1.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 311f3cb..dae3e08 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,19 +1,54 @@
-
+ android:layout_height="match_parent">
+
+
+
+
+
+
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginBottom="16dp"/>
\ 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..b07f1b0
--- /dev/null
+++ b/app/src/main/res/layout/item_music.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
\ 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..24d9dab 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,18 @@
BCSD_Android_2025-1
+ MainActivity.kt
+ 권한이 필요합니다!
+ 허용
+ 취소
+ 버튼을 눌러 권한을 허용하세요
+ 권한을 허용해야합니다
+ 설정으로 이동
+ 제목
+ 가수
+ 분:초
+ 현재음악
+ 배터리가 부족하여 음악이 중지됩니다.
+ music
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 922f551..952b930 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,4 +2,5 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 283fec9..3aa46c8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp = "8.9.0"
+agp = "8.11.0"
kotlin = "2.0.21"
coreKtx = "1.13.1"
junit = "4.13.2"
@@ -9,6 +9,9 @@ appcompat = "1.7.0"
material = "1.12.0"
activity = "1.9.3"
constraintlayout = "2.2.1"
+lifecycleRuntimeKtx = "2.6.1"
+activityCompose = "1.10.1"
+composeBom = "2024.09.00"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -19,8 +22,19 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index fb05014..1528844 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Sun Mar 30 14:34:16 KST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists