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/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/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..97f0a8e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ 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..9027b72 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ 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/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..73708be 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,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() + + 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) } -} \ No newline at end of file +} 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..314800a --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicAdapter.kt @@ -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, + private val listener: OnItemClickListener +) : RecyclerView.Adapter() { + + 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) + } +} diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MusicItem.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MusicItem.kt new file mode 100644 index 0000000..a455656 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicItem.kt @@ -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 +) \ No newline at end of file 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..6b0afcd --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicService.kt @@ -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() + } +} diff --git a/app/src/main/res/drawable/ic_logo_google.png b/app/src/main/res/drawable/ic_logo_google.png new file mode 100644 index 0000000..f39297a Binary files /dev/null and b/app/src/main/res/drawable/ic_logo_google.png differ diff --git a/app/src/main/res/drawable/ic_logo_kakao.png b/app/src/main/res/drawable/ic_logo_kakao.png new file mode 100644 index 0000000..712c565 Binary files /dev/null and b/app/src/main/res/drawable/ic_logo_kakao.png differ diff --git a/app/src/main/res/drawable/ic_logo_naver.png b/app/src/main/res/drawable/ic_logo_naver.png new file mode 100644 index 0000000..ac4036e Binary files /dev/null and b/app/src/main/res/drawable/ic_logo_naver.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 311f3cb..c3dc41d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,62 @@ - + android:layout_height="match_parent"> + + + app:layout_constraintEnd_toEndOf="parent" /> + + + + + +