-
Notifications
You must be signed in to change notification settings - Fork 12
[김예란_Android] 10주차 과제 제출 #75
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MusicData>() | ||
|
|
||
| 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) | ||
|
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. 메인 xml layout에서는 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 | ||
|
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. 위의 뷰를 초기화하는 4개 줄은 xml에서 visibility에 초기값을 주는 방식으로 개선해도 괜찮을 것으로 보입니다 |
||
|
|
||
| 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() | ||
|
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. oncreate에서 해당 로직을 사용하셨는데 onResume에서도 사용하는 이유가 있을까요? 생명주기에 다시 생각해보셨으면 좋겠습니다. |
||
| } | ||
|
|
||
| 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) | ||
| } | ||
|
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. 권한 체크하는 로직이 중복되었네요. |
||
|
|
||
| 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 | ||
|
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. Visibility를 변경하는 로직은 따로 빼주는 게 좋을 것 같네요 중복되는 부분이 많습니다. |
||
| loadMusicList() | ||
| } | ||
|
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. showPermissionUI와 showMusicList에서 UI 관련 코드가 중복되었는데 이럴때는 따로 확장 함수로 구현하시는 것을 추천드립니다 |
||
|
|
||
| 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) | ||
|
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. getColumnIndex는 해당 column이 없을 경우 -1을 반환하는 함수입니다 이때에 대한 예외 처리 로직도 추가해주시면 좋을 것 같습니다 |
||
| 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) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MusicData>, | ||
| private val listener: OnItemClickListener | ||
| ) : RecyclerView.Adapter<MusicAdapter.MusicViewHolder>() { | ||
|
|
||
| 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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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() { | ||||||
|
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.
Suggested change
|
||||||
|
|
||||||
| 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") | ||||||
|
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. 채널 id는 상수를 사용해주세요. |
||||||
| .setContentTitle("음악 재생") | ||||||
| .setContentText(title) | ||||||
| .setContentIntent(pendingIntent) | ||||||
| .setOngoing(true) | ||||||
| .build() | ||||||
| startForeground(1, notification) | ||||||
| } | ||||||
|
|
||||||
| override fun onDestroy() { | ||||||
| mediaPlayer?.release() | ||||||
| mediaPlayer = null | ||||||
| stopForeground(STOP_FOREGROUND_REMOVE) | ||||||
| super.onDestroy() | ||||||
| } | ||||||
| } | ||||||
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.
현재 코드에서는 뷰를 조절하는 것으로 권한 관련 UI를 표시하는 것으로 보입니다 visible로 조절하는 것 보다는 shouldShowRequestPermissionRationale() 등으로 분기처리 해주시는 것을 권장드립니다