-
Notifications
You must be signed in to change notification settings - Fork 12
[김보성_Android] 9주차 과제 제출 #66
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
e2f045d
c996e0f
a3be816
a8ca405
7df5134
baa5d35
b10b633
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.
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,155 @@ | ||
| package com.example.bcsd_android_2025_1 | ||
|
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. 패키지명에 언더바는 사용하지 않는 것을 권장드립니다! lower camel case를 추천드려요 |
||
|
|
||
| import android.content.Intent | ||
| import android.content.pm.PackageManager | ||
| import android.net.Uri | ||
| import android.os.Build | ||
| import android.os.Bundle | ||
| import androidx.activity.enableEdgeToEdge | ||
| 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.appcompat.app.AlertDialog | ||
| import androidx.core.content.ContextCompat | ||
| import androidx.recyclerview.widget.LinearLayoutManager | ||
| import androidx.recyclerview.widget.RecyclerView | ||
| import com.example.bcsd_android_2025_1.model.MusicData | ||
|
|
||
| class MainActivity : AppCompatActivity() { | ||
|
|
||
| private lateinit var recyclerView: RecyclerView | ||
| private lateinit var permissionMessage: TextView | ||
| private lateinit var openSettingsButton: Button | ||
| private lateinit var musicAdapter: MusicAdapter | ||
|
|
||
| private val musicList = mutableListOf<MusicData>() | ||
|
|
||
| private val requestPermissionLauncher = registerForActivityResult( | ||
| ActivityResultContracts.RequestPermission() | ||
| ) { isGranted -> | ||
| if (isGranted) { | ||
| showMusicList() | ||
| } else { | ||
| showPermissionDialog() | ||
| } | ||
| } | ||
|
|
||
| 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) | ||
|
|
||
| musicAdapter = MusicAdapter(musicList) | ||
| recyclerView.adapter = musicAdapter | ||
| recyclerView.layoutManager = LinearLayoutManager(this) | ||
|
|
||
| recyclerView.visibility = View.GONE | ||
| permissionMessage.visibility = View.GONE | ||
| openSettingsButton.visibility = View.GONE | ||
|
|
||
| if (hasPermission()) { | ||
| showMusicList() | ||
| } else { | ||
| 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) | ||
| } | ||
| } | ||
|
|
||
| private fun hasPermission(): Boolean { | ||
| val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||
| android.Manifest.permission.READ_MEDIA_AUDIO | ||
| } else { | ||
| android.Manifest.permission.READ_EXTERNAL_STORAGE | ||
| } | ||
| return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED | ||
| } | ||
|
|
||
| private fun requestPermission() { | ||
|
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 permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||
| android.Manifest.permission.READ_MEDIA_AUDIO | ||
| } else { | ||
| android.Manifest.permission.READ_EXTERNAL_STORAGE | ||
| } | ||
|
|
||
| when { | ||
| ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED -> { | ||
| showMusicList() | ||
| } | ||
| shouldShowRequestPermissionRationale(permission) -> { | ||
| showPermissionDialog() | ||
| } | ||
| else -> { | ||
| requestPermissionLauncher.launch(permission) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun showPermissionDialog() { | ||
| AlertDialog.Builder(this) | ||
| .setTitle(getString(R.string.permission_dialog_title)) | ||
|
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. string resource 사용하신 부분이 좋은 것 같아요! |
||
| .setMessage(getString(R.string.permission_dialog_message)) | ||
| .setPositiveButton(getString(R.string.ok)) { dialog, _ -> | ||
| dialog.dismiss() | ||
| showPermissionUI() | ||
| } | ||
| .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> | ||
| dialog.dismiss() | ||
| } | ||
| .show() | ||
| } | ||
|
|
||
| private fun showPermissionUI() { | ||
| recyclerView.visibility = View.GONE | ||
| permissionMessage.visibility = View.VISIBLE | ||
| openSettingsButton.visibility = View.VISIBLE | ||
| } | ||
|
|
||
| private fun showMusicList() { | ||
| recyclerView.visibility = View.VISIBLE | ||
| permissionMessage.visibility = View.GONE | ||
| openSettingsButton.visibility = View.GONE | ||
|
|
||
| loadMusicList() | ||
| } | ||
|
|
||
| private fun loadMusicList() { | ||
| musicList.clear() | ||
|
|
||
| val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | ||
| val projection = arrayOf( | ||
| MediaStore.Audio.Media.TITLE, | ||
| MediaStore.Audio.Media.ARTIST, | ||
| MediaStore.Audio.Media.DURATION | ||
| ) | ||
|
|
||
| val cursor = contentResolver.query(uri, projection, null, null, null) | ||
| cursor?.use { | ||
| while (it.moveToNext()) { | ||
| val title = it.getString(0) | ||
|
Contributor
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. index를 하드코딩 하기보단, |
||
| val artist = it.getString(1) | ||
| val duration = it.getLong(2) | ||
| musicList.add(MusicData(title, artist, duration)) | ||
| } | ||
| } | ||
|
|
||
| musicAdapter.notifyDataSetChanged() | ||
|
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. notifyDataSetChanged는 항상 모든 리스트를 새로고침하므로 diffutil을 사용하는 방식도 추천드립니다!
Contributor
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. 혹은 |
||
| } | ||
|
|
||
| override fun onResume() { | ||
| super.onResume() | ||
| if (hasPermission()) { | ||
|
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. 설정에서 돌아왔을 때 onResume에서 권한을 다시 확인하는 방식은 좋은 것 같습니다. 다만 onCreate에서도 권한을 허용했는지 확인하므로 2번을 검사하게 되는 비효율이 발생합니다.(hasPermission 자체는 가벼운 연산이지만 이후에 실행하는 showMusicList()는 목록을 update하는 것이므로 상황에 따라서 성능 저하가 발생할 수 있습니다) 따라서 이럴 때는 권한에 대한 상태를 변수로 저장하고 권한이 변경되었을 때만 권한을 update하도록 분기 처리 하는 방법도 고려해 보시면 좋을 것 같습니다. |
||
| showMusicList() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| 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 | ||
| import com.example.bcsd_android_2025_1.model.MusicData | ||
|
|
||
| class MusicAdapter(private val musicList: List<MusicData>) : | ||
| RecyclerView.Adapter<MusicAdapter.MusicViewHolder>() { | ||
|
|
||
| inner class MusicViewHolder(view: View) : RecyclerView.ViewHolder(view) { | ||
| val title: TextView = view.findViewById(R.id.titleText) | ||
| val artist: TextView = view.findViewById(R.id.artistText) | ||
| val duration: TextView = view.findViewById(R.id.durationText) | ||
| } | ||
|
|
||
| override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MusicViewHolder { | ||
| val view = LayoutInflater.from(parent.context) | ||
| .inflate(R.layout.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) | ||
| } | ||
|
|
||
| override fun getItemCount(): Int = musicList.size | ||
|
|
||
| private fun formatDuration(ms: Long): String { | ||
|
Contributor
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 minutes = ms / 1000 / 60 | ||
| val seconds = (ms / 1000) % 60 | ||
| return "%d:%02d".format(minutes, seconds) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.bcsd_android_2025_1.model | ||
|
|
||
| data class MusicData( | ||
| val title: String, | ||
| val artist: String, | ||
| val duration: Long | ||
| ) |
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.
본 커리큘럼에서는 compose를 사용하지 않습니다.