diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c80941..64509f3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,12 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.example.bcsd_android_2025_1"> + + + + + - - + + \ 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..1889f3b 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,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) { + 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() + 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) + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/Music.kt b/app/src/main/java/com/example/bcsd_android_2025_1/Music.kt new file mode 100644 index 0000000..829b5d1 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/Music.kt @@ -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 +) 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..4f3a793 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicAdapter.kt @@ -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, + private val onItemClick: (Music) -> Unit +) : RecyclerView.Adapter() { + + 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 { + 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) + } +} 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..23f5317 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/MusicPlayerService.kt @@ -0,0 +1,142 @@ +package com.example.bcsd_android_2025_1 + +import android.app.* +import android.content.* +import android.media.MediaPlayer +import android.net.Uri +import android.os.IBinder +import androidx.core.app.NotificationCompat +import android.os.Build + + +class MusicPlayerService : Service() { + + private var mediaPlayer: MediaPlayer? = null + private var currentTitle: String = "" + private var currentArtist: String = "" + + companion object { + const val CHANNEL_ID = "MusicPlayerChannel" + const val EXTRA_MUSIC_URI = "music_uri" + const val EXTRA_TITLE = "music_title" + const val EXTRA_ARTIST = "music_artist" + } + + private val commandReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + "com.example.bcsd_android_2025_1.ACTION_TOGGLE_PLAY" -> { + if (mediaPlayer?.isPlaying == true) { + mediaPlayer?.pause() + } else { + mediaPlayer?.start() + } + broadcastNowPlaying() + } + "com.example.bcsd_android_2025_1.REQUEST_NOW_PLAYING" -> { + broadcastNowPlaying() + } + } + } + } + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + val filter = IntentFilter().apply { + addAction("com.example.bcsd_android_2025_1.ACTION_TOGGLE_PLAY") + addAction("com.example.bcsd_android_2025_1.REQUEST_NOW_PLAYING") + } + registerReceiver(commandReceiver, filter) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + intent?.let { + val musicUri = it.getParcelableExtra(EXTRA_MUSIC_URI) + currentTitle = it.getStringExtra(EXTRA_TITLE) ?: "Unknown" + currentArtist = it.getStringExtra(EXTRA_ARTIST) ?: "Unknown" + + musicUri?.let { uri -> + playMusic(uri) + } + } + return START_STICKY + } + + private fun playMusic(uri: Uri) { + mediaPlayer?.release() + try { + val fd = contentResolver.openFileDescriptor(uri, "r")?.fileDescriptor + if (fd != null) { + mediaPlayer = MediaPlayer().apply { + setDataSource(fd) + prepare() + start() + } + showNotification() + broadcastNowPlaying() + } else { + stopSelf() + } + } catch (e: Exception) { + e.printStackTrace() + stopSelf() + } + } + + + private fun showNotification() { + val openAppIntent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + val pendingIntent = PendingIntent.getActivity( + this, + 0, + openAppIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(currentTitle) + .setContentText(currentArtist) + .setSmallIcon(android.R.drawable.ic_media_play) + .setOngoing(true) + .setContentIntent(pendingIntent) + .build() + + startForeground(1, notification) + } + + private fun broadcastNowPlaying() { + val isPlaying = mediaPlayer?.isPlaying == true + val intent = Intent("com.example.bcsd_android_2025_1.NOW_PLAYING").apply { + putExtra("title", currentTitle) + putExtra("artist", currentArtist) + putExtra("isPlaying", isPlaying) + } + sendBroadcast(intent) + } + + override fun onDestroy() { + mediaPlayer?.release() + unregisterReceiver(commandReceiver) + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? = null + + private fun createNotificationChannel() { + val channel = NotificationChannel( + CHANNEL_ID, + "음악 재생 채널", + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "음악 재생 상태를 보여주는 채널" + } + + val manager = getSystemService(NotificationManager::class.java) + manager?.createNotificationChannel(channel) + } + +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 311f3cb..064a884 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,83 @@ - + android:layout_height="match_parent"> - + + + + + +