Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/AndroidProjectSystem.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/migrations.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions .idea/runConfigurations.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}

android {
namespace = "com.example.bcsd_android_2025_1"
compileSdk = 34
compileSdk = 35

defaultConfig {
applicationId = "com.example.bcsd_android_2025_1"
minSdk = 26
targetSdk = 34
targetSdk = 35
versionCode = 1
versionName = "1.0"

Expand All @@ -33,6 +34,9 @@ android {
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}

dependencies {
Expand All @@ -42,7 +46,18 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

본 커리큘럼에서는 compose를 사용하지 않습니다.

androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -12,12 +16,13 @@
android:supportsRtl="true"
android:theme="@style/Theme.BCSD_Android_20251"
tools:targetApi="31">


<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Expand Down
149 changes: 145 additions & 4 deletions app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,155 @@
package com.example.bcsd_android_2025_1

Choose a reason for hiding this comment

The 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() {

Choose a reason for hiding this comment

The 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))

Choose a reason for hiding this comment

The 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index를 하드코딩 하기보단, cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)를 이용해 인덱스를 가져오는게 바람직해 보입니다.

val artist = it.getString(1)
val duration = it.getLong(2)
musicList.add(MusicData(title, artist, duration))
}
}

musicAdapter.notifyDataSetChanged()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notifyDataSetChanged는 항상 모든 리스트를 새로고침하므로 diffutil을 사용하는 방식도 추천드립니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹은 notifyItemInserted를 사용해도 됩니다

}

override fun onResume() {
super.onResume()
if (hasPermission()) {
Copy link

@JaeYoung290 JaeYoung290 Jun 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설정에서 돌아왔을 때 onResume에서 권한을 다시 확인하는 방식은 좋은 것 같습니다. 다만 onCreate에서도 권한을 허용했는지 확인하므로 2번을 검사하게 되는 비효율이 발생합니다.(hasPermission 자체는 가벼운 연산이지만 이후에 실행하는 showMusicList()는 목록을 update하는 것이므로 상황에 따라서 성능 저하가 발생할 수 있습니다) 따라서 이럴 때는 권한에 대한 상태를 변수로 저장하고 권한이 변경되었을 때만 권한을 update하도록 분기 처리 하는 방법도 고려해 보시면 좋을 것 같습니다.

showMusicList()
}
}
}
}
39 changes: 39 additions & 0 deletions app/src/main/java/com/example/bcsd_android_2025_1/MusicAdapter.kt
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long.formatDuration()처럼 확장함수를 쓰는게 좋아보입니다

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
)
Loading