-
Notifications
You must be signed in to change notification settings - Fork 1
[김예란_Android] 5주차 과제 제출 #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,184 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import android.Manifest | ||
| import android.app.NotificationChannel | ||
| import android.app.NotificationManager | ||
| import android.content.ComponentName | ||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.content.ServiceConnection | ||
| import android.content.pm.PackageManager | ||
| import android.net.Uri | ||
| import android.os.Build | ||
| import android.os.Bundle | ||
| import androidx.activity.enableEdgeToEdge | ||
| import android.os.IBinder | ||
| import android.provider.MediaStore | ||
| import android.provider.Settings | ||
| import android.widget.TextView | ||
| import androidx.appcompat.app.AppCompatActivity | ||
| import androidx.constraintlayout.widget.ConstraintLayout | ||
| import androidx.core.app.ActivityCompat | ||
| import androidx.core.content.ContextCompat | ||
| import androidx.core.view.ViewCompat | ||
| import androidx.core.view.WindowInsetsCompat | ||
| import androidx.recyclerview.widget.LinearLayoutManager | ||
| import androidx.recyclerview.widget.RecyclerView | ||
|
|
||
| class MainActivity : AppCompatActivity() { | ||
| private var isBound = false | ||
| private var musicService: MusicService? = null | ||
| private lateinit var player: ConstraintLayout | ||
| private lateinit var tvTitle: TextView | ||
| private lateinit var tvArtist: TextView | ||
|
|
||
| private val serviceConnection = object : ServiceConnection{ | ||
| override fun onServiceConnected(name: ComponentName?, service: IBinder?){ | ||
| val binder = service as MusicService.MusicBinder | ||
| musicService = binder.getService() | ||
| isBound = true | ||
|
|
||
| musicService?.setOnMusicChangeListener { music -> | ||
| updatePlayer(music) | ||
| } | ||
| updatePlayer(musicService?.currentMusic) | ||
| } | ||
|
|
||
| override fun onServiceDisconnected(p0: ComponentName?) { | ||
| musicService = null | ||
| isBound = false | ||
| } | ||
| } | ||
| fun updatePlayer(music: MusicItem?) { | ||
| if (music != null) { | ||
| player.visibility = ConstraintLayout.VISIBLE | ||
| tvTitle.text = music.displayName | ||
| tvArtist.text = music.artist | ||
| } else { | ||
| player.visibility = ConstraintLayout.GONE | ||
| } | ||
| } | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
| enableEdgeToEdge() | ||
| setContentView(R.layout.activity_main) | ||
| ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> | ||
| val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) | ||
| insets | ||
| } | ||
|
|
||
| player = findViewById(R.id.player) | ||
| tvTitle = findViewById(R.id.player_title) | ||
| tvArtist = findViewById(R.id.player_artist) | ||
|
|
||
| permissionCheck() | ||
| createNotificationChannel() | ||
|
|
||
| val musicList = mutableListOf<MusicItem>() | ||
|
|
||
| val projection = arrayOf( | ||
| MediaStore.Audio.Media.DISPLAY_NAME, | ||
| MediaStore.Audio.Media.DURATION, | ||
| MediaStore.Audio.Media.ARTIST, | ||
| MediaStore.Audio.Media._ID | ||
| ) | ||
|
|
||
| applicationContext.contentResolver.query( | ||
| MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, | ||
| projection, | ||
| "", | ||
| arrayOf(), | ||
| "" | ||
| )?.use { cursor -> | ||
| val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME) | ||
| val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION) | ||
| val artistsColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST) | ||
| val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID) | ||
| while (cursor.moveToNext()){ | ||
| val displayName = cursor.getString(displayNameColumn) | ||
| val duration = cursor.getInt(durationColumn) | ||
| val artists = cursor.getString(artistsColumn) | ||
| val id = cursor.getString(idColumn) | ||
| val uri = Uri.withAppendedPath( | ||
| MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, | ||
| id.toString() | ||
| ) | ||
| musicList.add(MusicItem(displayName, duration, artists, uri)) | ||
| } | ||
| } | ||
|
|
||
| val recyclerView = findViewById<RecyclerView>(R.id.recyclerView) | ||
| val musicAdapter = MusicAdapter(musicList) { music -> | ||
| playMusicWithService(music) | ||
| } | ||
| recyclerView.layoutManager = LinearLayoutManager(this) | ||
| recyclerView.adapter = musicAdapter | ||
| } | ||
|
|
||
| private fun createNotificationChannel() { | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ | ||
| val channel = NotificationChannel( | ||
| "music_channel", | ||
| "Music Playback", | ||
| NotificationManager.IMPORTANCE_LOW | ||
| ) | ||
| val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||
| notificationManager.createNotificationChannel(channel) | ||
| } | ||
| } | ||
|
|
||
| private fun playMusicWithService(music: MusicItem) { | ||
| val intent = Intent(this, MusicService::class.java).apply { | ||
| action = "ACTION_PLAY" | ||
| putExtra("MUSIC_TITLE", music.displayName) | ||
| putExtra("MUSIC_ARTIST", music.artist) | ||
| putExtra("MUSIC_URI", music.uri.toString()) | ||
| } | ||
|
|
||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||
| startForegroundService(intent) | ||
| } else { | ||
| startService(intent) | ||
| } | ||
| bindService(Intent(this, MusicService::class.java), serviceConnection, Context.BIND_AUTO_CREATE) | ||
| } | ||
|
|
||
| private fun permissionCheck() { | ||
| val mediaPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||
| Manifest.permission.READ_MEDIA_AUDIO | ||
| } else { | ||
| Manifest.permission.READ_EXTERNAL_STORAGE | ||
| } | ||
| if (ContextCompat.checkSelfPermission(this, mediaPermission) | ||
| != PackageManager.PERMISSION_GRANTED) { | ||
| ActivityCompat.requestPermissions( | ||
| this, arrayOf(mediaPermission), 1000 | ||
| ) | ||
| 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 | ||
| ), 1000 | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| if(!ActivityCompat.shouldShowRequestPermissionRationale(this, mediaPermission)) { | ||
| val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse("package:${packageName}")) | ||
| startActivity(intent) | ||
| } | ||
| if(!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) { | ||
| val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse("package:${packageName}")) | ||
| startActivity(intent) | ||
| } | ||
| } | ||
| } | ||
| override fun onDestroy() { | ||
| super.onDestroy() | ||
| if (isBound) { | ||
| unbindService(serviceConnection) | ||
| isBound = false | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import android.content.Context | ||
| import android.media.MediaPlayer | ||
| import android.view.LayoutInflater | ||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import android.widget.TextView | ||
| import androidx.constraintlayout.motion.widget.MotionScene.Transition.TransitionOnClick | ||
| import androidx.recyclerview.widget.RecyclerView | ||
| import androidx.recyclerview.widget.RecyclerView.ViewHolder | ||
|
|
||
| class MusicAdapter(private val musicList: List<MusicItem>, private val onClick: (MusicItem) -> Unit): RecyclerView.Adapter<ViewHolder>(){ | ||
| private var mediaPlayer: MediaPlayer? = null | ||
|
|
||
| class MusicViewHolder(view: View) : RecyclerView.ViewHolder(view){ | ||
| } | ||
|
|
||
| override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||
| val view = LayoutInflater.from(parent.context) | ||
| .inflate(R.layout.item, parent, false) | ||
|
|
||
| return MusicViewHolder(view) | ||
| } | ||
|
|
||
| override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||
| val music = musicList[position] | ||
| val displayName: TextView = holder.itemView.findViewById(R.id.text_display_name) | ||
| val artists: TextView = holder.itemView.findViewById(R.id.text_artist) | ||
| val duration: TextView = holder.itemView.findViewById(R.id.text_duration) | ||
| displayName.text = music.displayName | ||
| artists.text = music.artist | ||
| duration.text = music.duration.toString() | ||
|
|
||
| holder.itemView.setOnClickListener{ | ||
| mediaPlayer?.stop() | ||
| mediaPlayer?.release() | ||
| onClick(music) | ||
|
|
||
| mediaPlayer = MediaPlayer().apply { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 앞에서 onClick(music) 을 통해 music 을 실행하고 있습니다. 여기서 한번 더 MediaPlayer 를 만들고 start() 를 한다면 |
||
| setDataSource(holder.itemView.context, music.uri) | ||
| prepare() | ||
| start() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun getItemCount() = musicList.size | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import android.net.Uri | ||
|
|
||
| data class MusicItem ( | ||
| val displayName: String, | ||
| val duration: Int, | ||
| val artist: String, | ||
| val uri: Uri | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import android.app.Notification | ||
| import android.app.PendingIntent | ||
| import android.app.Service | ||
| 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 var mediaPlayer: MediaPlayer? = null | ||
| var currentMusic: MusicItem? = null | ||
| private val binder = MusicBinder() | ||
| private var musicChangeListener: ((MusicItem?) -> Unit)? = null | ||
|
|
||
| inner class MusicBinder : Binder() { | ||
| fun getService(): MusicService = this@MusicService | ||
| } | ||
|
|
||
| fun setOnMusicChangeListener(listener: (MusicItem?) -> Unit) { | ||
| musicChangeListener = listener | ||
| } | ||
|
|
||
| override fun onStartCommand(intent: Intent?, flags: Int, startID: Int): Int{ | ||
| when (intent?.action) { | ||
| "ACTION_PLAY" -> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이런 부분도 상수 관리가 좋겠네요. |
||
| val title = intent.getStringExtra("MUSIC_TITLE")?: "" | ||
| val artist = intent.getStringExtra("MUSIC_ARTIST")?: "" | ||
| val uriString = intent.getStringExtra("MUSIC_URI")?: "" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MUSIC_TITLE, MUSIC_ARTIST, MUSIC_URI 는 상수 관리를 한다면 좋을 거 같습니다. |
||
| val uri = Uri.parse(uriString) | ||
|
|
||
| val music = MusicItem(title, 0, artist, uri) | ||
| playMusic(music) | ||
| } | ||
| "ACTION_STOP" -> { | ||
| stopMusic() | ||
| stopForeground(STOP_FOREGROUND_REMOVE) | ||
| stopSelf() | ||
| } | ||
| } | ||
| return START_STICKY | ||
| } | ||
|
|
||
| private fun playMusic(music: MusicItem) { | ||
| try { | ||
| mediaPlayer?.stop() | ||
| mediaPlayer?.release() | ||
|
|
||
| mediaPlayer = MediaPlayer().apply { | ||
| setDataSource(applicationContext, music.uri) | ||
| prepare() | ||
| start() | ||
| } | ||
|
|
||
| currentMusic = music | ||
| musicChangeListener?.invoke(music) | ||
| startForeground(1, createNotification(music)) | ||
|
|
||
| } catch (e: Exception) { | ||
| e.printStackTrace() | ||
| } | ||
| } | ||
|
|
||
| private fun stopMusic() { | ||
| mediaPlayer?.stop() | ||
| mediaPlayer?.release() | ||
| mediaPlayer = null | ||
| currentMusic = null | ||
| musicChangeListener?.invoke(null) | ||
| } | ||
|
|
||
| private fun createNotification(music: MusicItem): Notification { | ||
| val intent = Intent(this, MainActivity::class.java).apply { | ||
| flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK | ||
| } | ||
| val pendingIntent = PendingIntent.getActivity( | ||
| this, | ||
| 0, | ||
| intent, | ||
| PendingIntent.FLAG_IMMUTABLE | ||
| ) | ||
|
|
||
| val stopIntent = Intent(this, MusicService::class.java).apply { | ||
| action = "ACTION_STOP" | ||
| } | ||
| val stopPendingIntent = PendingIntent.getService( | ||
| this, | ||
| 1, | ||
| stopIntent, | ||
| PendingIntent.FLAG_IMMUTABLE | ||
| ) | ||
|
|
||
| return NotificationCompat.Builder(this, "music_channel") | ||
| .setContentTitle(music.displayName) | ||
| .setContentText(music.artist) | ||
| .setSmallIcon(android.R.drawable.ic_media_play) | ||
| .setContentIntent(pendingIntent) | ||
| .addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent) | ||
| .setOngoing(true) | ||
| .build() | ||
| } | ||
|
|
||
| override fun onBind(intent: Intent?): IBinder { | ||
| return binder | ||
| } | ||
|
|
||
| override fun onDestroy() { | ||
| super.onDestroy() | ||
| stopMusic() | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 if 문이
if (ContextCompat.checkSelfPermission(this, mediaPermission) != PackageManager.PERMISSION_GRANTED)
안에 있으면 안됩니다.
이유는 mediaPermission 이 거절 되어있을 경우에만 POST_NOTIFICATIONS 검사를 하게 됩니다.
그러니 media 권한은 있는데 notification 권한이 없어도 정상 작동하게 됩니다.